fontisan 0.2.17 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/.rubocop_todo.yml +86 -86
- data/CHANGELOG.md +12 -2
- data/README.adoc +53 -1
- data/docs/.vitepress/config.ts +68 -8
- data/docs/.vitepress/theme/style.css +570 -272
- data/docs/CONVERSION_GUIDE.adoc +31 -8
- data/docs/EXTRACT_TTC_MIGRATION.md +1 -1
- data/docs/WOFF_WOFF2_FORMATS.adoc +53 -0
- data/docs/api/conversion-options.md +37 -14
- data/docs/cli/audit.md +337 -0
- data/docs/cli/convert.md +20 -1
- data/docs/cli/index.md +31 -0
- data/docs/guide/color.md +1 -1
- data/docs/guide/conversion/options.md +32 -3
- data/docs/guide/conversion/ttf-otf.md +1 -1
- data/docs/guide/conversion/type1.md +1 -1
- data/docs/guide/conversion/web.md +91 -32
- data/docs/guide/conversion.md +6 -5
- data/docs/guide/formats/woff.md +35 -11
- data/docs/guide/index.md +2 -2
- data/docs/guide/migrations/extract-ttc.md +1 -1
- data/docs/guide/quick-start.md +4 -4
- data/docs/guide/type1.md +4 -4
- data/docs/guide/woff.md +19 -17
- data/docs/index.md +2 -0
- data/docs/lychee.toml +5 -1
- data/docs/package.json +1 -1
- data/docs/public/robots.txt +4 -0
- data/docs/scripts/post-build.mjs +81 -0
- data/lib/fontisan/base_collection.rb +23 -9
- data/lib/fontisan/binary/structures.rb +0 -2
- data/lib/fontisan/binary.rb +11 -0
- data/lib/fontisan/cldr/aggregator.rb +33 -0
- data/lib/fontisan/cldr/cache_manager.rb +110 -0
- data/lib/fontisan/cldr/config.rb +59 -0
- data/lib/fontisan/cldr/download_error.rb +9 -0
- data/lib/fontisan/cldr/downloader.rb +79 -0
- data/lib/fontisan/cldr/error.rb +8 -0
- data/lib/fontisan/cldr/index.rb +64 -0
- data/lib/fontisan/cldr/index_builder.rb +72 -0
- data/lib/fontisan/cldr/unicode_set_parser.rb +189 -0
- data/lib/fontisan/cldr/unknown_version_error.rb +9 -0
- data/lib/fontisan/cldr/version_resolver.rb +91 -0
- data/lib/fontisan/cldr.rb +23 -0
- data/lib/fontisan/cli/cldr_cli.rb +85 -0
- data/lib/fontisan/cli.rb +31 -2
- data/lib/fontisan/collection/builder.rb +0 -4
- data/lib/fontisan/collection/dfont_builder.rb +0 -4
- data/lib/fontisan/collection/shared_logic.rb +0 -2
- data/lib/fontisan/collection/writer.rb +0 -3
- data/lib/fontisan/collection.rb +15 -0
- data/lib/fontisan/commands/base_command.rb +0 -3
- data/lib/fontisan/commands/convert_command.rb +25 -20
- data/lib/fontisan/commands/dump_table_command.rb +0 -3
- data/lib/fontisan/commands/export_command.rb +0 -4
- data/lib/fontisan/commands/features_command.rb +0 -3
- data/lib/fontisan/commands/instance_command.rb +0 -5
- data/lib/fontisan/commands/ls_command.rb +0 -6
- data/lib/fontisan/commands/optical_size_command.rb +0 -3
- data/lib/fontisan/commands/pack_command.rb +0 -5
- data/lib/fontisan/commands/scripts_command.rb +0 -2
- data/lib/fontisan/commands/subset_command.rb +0 -3
- data/lib/fontisan/commands/unicode_command.rb +0 -3
- data/lib/fontisan/commands/unpack_command.rb +0 -7
- data/lib/fontisan/commands/validate_command.rb +0 -8
- data/lib/fontisan/commands/variable_command.rb +0 -3
- data/lib/fontisan/commands.rb +26 -0
- data/lib/fontisan/config/cldr.yml +22 -0
- data/lib/fontisan/config/conversion_matrix.yml +38 -0
- data/lib/fontisan/constants.rb +19 -0
- data/lib/fontisan/conversion_options.rb +30 -19
- data/lib/fontisan/converters/cff_table_builder.rb +0 -3
- data/lib/fontisan/converters/collection_converter.rb +0 -8
- data/lib/fontisan/converters/conversion_strategy.rb +161 -46
- data/lib/fontisan/converters/format_converter.rb +143 -32
- data/lib/fontisan/converters/glyf_table_builder.rb +0 -2
- data/lib/fontisan/converters/outline_converter.rb +0 -19
- data/lib/fontisan/converters/outline_extraction.rb +0 -5
- data/lib/fontisan/converters/outline_optimizer.rb +0 -5
- data/lib/fontisan/converters/svg_generator.rb +0 -4
- data/lib/fontisan/converters/table_copier.rb +0 -2
- data/lib/fontisan/converters/type1_converter.rb +0 -11
- data/lib/fontisan/converters/woff2_encoder.rb +49 -20
- data/lib/fontisan/converters/woff_writer.rb +211 -282
- data/lib/fontisan/converters.rb +21 -0
- data/lib/fontisan/dfont_collection.rb +29 -10
- data/lib/fontisan/export/exporter.rb +0 -6
- data/lib/fontisan/export/transformers/font_to_ttx.rb +0 -9
- data/lib/fontisan/export/transformers/head_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/hhea_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/maxp_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/name_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/os2_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/post_transformer.rb +0 -2
- data/lib/fontisan/export/transformers.rb +17 -0
- data/lib/fontisan/export.rb +13 -0
- data/lib/fontisan/font_loader.rb +14 -19
- data/lib/fontisan/font_writer.rb +0 -1
- data/lib/fontisan/formatters.rb +9 -0
- data/lib/fontisan/hints/hint_converter.rb +0 -1
- data/lib/fontisan/hints/postscript_hint_applier.rb +0 -9
- data/lib/fontisan/hints/postscript_hint_extractor.rb +0 -2
- data/lib/fontisan/hints/truetype_hint_extractor.rb +0 -2
- data/lib/fontisan/hints.rb +19 -0
- data/lib/fontisan/metrics_calculator.rb +0 -2
- data/lib/fontisan/models/all_scripts_features_info.rb +0 -1
- data/lib/fontisan/models/cldr/language_coverage.rb +31 -0
- data/lib/fontisan/models/cldr.rb +12 -0
- data/lib/fontisan/models/collection_brief_info.rb +0 -1
- data/lib/fontisan/models/collection_info.rb +0 -2
- data/lib/fontisan/models/collection_list_info.rb +0 -1
- data/lib/fontisan/models/collection_validation_report.rb +0 -2
- data/lib/fontisan/models/color_glyph.rb +0 -1
- data/lib/fontisan/models/font_report.rb +0 -1
- data/lib/fontisan/models/ttx/tables.rb +21 -0
- data/lib/fontisan/models/ttx/ttfont.rb +0 -8
- data/lib/fontisan/models/ttx.rb +14 -0
- data/lib/fontisan/models.rb +47 -0
- data/lib/fontisan/open_type_collection.rb +6 -5
- data/lib/fontisan/open_type_font.rb +8 -2
- data/lib/fontisan/open_type_font_extensions.rb +9 -9
- data/lib/fontisan/optimizers/pattern_analyzer.rb +0 -1
- data/lib/fontisan/optimizers.rb +14 -0
- data/lib/fontisan/outline_extractor.rb +0 -2
- data/lib/fontisan/parsers/dfont_parser.rb +0 -1
- data/lib/fontisan/parsers.rb +10 -0
- data/lib/fontisan/pipeline/format_detector.rb +29 -102
- data/lib/fontisan/pipeline/output_writer.rb +11 -9
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +0 -4
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +0 -4
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +0 -2
- data/lib/fontisan/pipeline/strategies.rb +16 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +0 -7
- data/lib/fontisan/pipeline/variation_resolver.rb +0 -7
- data/lib/fontisan/pipeline.rb +14 -0
- data/lib/fontisan/sfnt_font.rb +29 -14
- data/lib/fontisan/sfnt_table.rb +0 -4
- data/lib/fontisan/subset/builder.rb +0 -6
- data/lib/fontisan/subset.rb +13 -0
- data/lib/fontisan/svg/font_generator.rb +0 -4
- data/lib/fontisan/svg/glyph_generator.rb +0 -2
- data/lib/fontisan/svg.rb +12 -0
- data/lib/fontisan/tables/cbdt.rb +0 -1
- data/lib/fontisan/tables/cblc.rb +0 -1
- data/lib/fontisan/tables/cff/charset.rb +0 -1
- data/lib/fontisan/tables/cff/charstring.rb +0 -1
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +0 -4
- data/lib/fontisan/tables/cff/charstrings_index.rb +0 -3
- data/lib/fontisan/tables/cff/dict.rb +0 -1
- data/lib/fontisan/tables/cff/encoding.rb +0 -1
- data/lib/fontisan/tables/cff/header.rb +0 -2
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +0 -2
- data/lib/fontisan/tables/cff/index.rb +0 -1
- data/lib/fontisan/tables/cff/private_dict.rb +0 -2
- data/lib/fontisan/tables/cff/private_dict_writer.rb +0 -2
- data/lib/fontisan/tables/cff/table_builder.rb +0 -6
- data/lib/fontisan/tables/cff/top_dict.rb +0 -2
- data/lib/fontisan/tables/cff.rb +23 -15
- data/lib/fontisan/tables/cff2/charstring_parser.rb +0 -2
- data/lib/fontisan/tables/cff2/table_builder.rb +0 -11
- data/lib/fontisan/tables/cff2/table_reader.rb +0 -2
- data/lib/fontisan/tables/cff2.rb +13 -14
- data/lib/fontisan/tables/cmap.rb +24 -2
- data/lib/fontisan/tables/cmap_table.rb +0 -3
- data/lib/fontisan/tables/colr.rb +0 -1
- data/lib/fontisan/tables/cpal.rb +0 -1
- data/lib/fontisan/tables/cvar.rb +0 -2
- data/lib/fontisan/tables/fvar.rb +0 -1
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +0 -2
- data/lib/fontisan/tables/glyf/glyph_builder.rb +0 -3
- data/lib/fontisan/tables/glyf.rb +0 -6
- data/lib/fontisan/tables/glyf_table.rb +0 -3
- data/lib/fontisan/tables/gpos.rb +0 -2
- data/lib/fontisan/tables/gsub.rb +0 -2
- data/lib/fontisan/tables/gvar.rb +0 -2
- data/lib/fontisan/tables/head.rb +0 -2
- data/lib/fontisan/tables/head_table.rb +0 -3
- data/lib/fontisan/tables/hhea.rb +0 -2
- data/lib/fontisan/tables/hhea_table.rb +0 -3
- data/lib/fontisan/tables/hmtx.rb +0 -2
- data/lib/fontisan/tables/hmtx_table.rb +0 -3
- data/lib/fontisan/tables/hvar.rb +0 -3
- data/lib/fontisan/tables/loca.rb +0 -2
- data/lib/fontisan/tables/loca_table.rb +0 -3
- data/lib/fontisan/tables/maxp.rb +0 -2
- data/lib/fontisan/tables/maxp_table.rb +0 -3
- data/lib/fontisan/tables/mvar.rb +0 -3
- data/lib/fontisan/tables/name.rb +0 -2
- data/lib/fontisan/tables/name_table.rb +0 -3
- data/lib/fontisan/tables/os2_table.rb +0 -3
- data/lib/fontisan/tables/post_table.rb +0 -3
- data/lib/fontisan/tables/sbix.rb +0 -1
- data/lib/fontisan/tables/svg.rb +0 -1
- data/lib/fontisan/tables/variation_common.rb +0 -1
- data/lib/fontisan/tables/vvar.rb +0 -3
- data/lib/fontisan/tables.rb +55 -0
- data/lib/fontisan/true_type_collection.rb +6 -14
- data/lib/fontisan/true_type_font.rb +8 -2
- data/lib/fontisan/true_type_font_extensions.rb +9 -9
- data/lib/fontisan/type1/afm_generator.rb +0 -4
- data/lib/fontisan/type1/conversion_options.rb +0 -2
- data/lib/fontisan/type1/encodings.rb +0 -2
- data/lib/fontisan/type1/generator.rb +0 -8
- data/lib/fontisan/type1/pfa_generator.rb +0 -3
- data/lib/fontisan/type1/pfb_generator.rb +0 -5
- data/lib/fontisan/type1/pfm_generator.rb +0 -4
- data/lib/fontisan/type1.rb +42 -69
- data/lib/fontisan/type1_font.rb +40 -11
- data/lib/fontisan/utilities/checksum_calculator.rb +0 -1
- data/lib/fontisan/utilities.rb +10 -0
- data/lib/fontisan/utils.rb +10 -0
- data/lib/fontisan/validation/collection_validator.rb +0 -2
- data/lib/fontisan/validation.rb +9 -0
- data/lib/fontisan/validators/basic_validator.rb +0 -2
- data/lib/fontisan/validators/font_book_validator.rb +0 -2
- data/lib/fontisan/validators/opentype_validator.rb +0 -2
- data/lib/fontisan/validators/profile_loader.rb +0 -5
- data/lib/fontisan/validators/validator.rb +0 -2
- data/lib/fontisan/validators/web_font_validator.rb +0 -2
- data/lib/fontisan/validators.rb +14 -0
- data/lib/fontisan/variable/delta_applicator.rb +0 -4
- data/lib/fontisan/variable/instancer.rb +0 -3
- data/lib/fontisan/variable/static_font_builder.rb +0 -3
- data/lib/fontisan/variable.rb +16 -0
- data/lib/fontisan/variation/blend_applier.rb +0 -2
- data/lib/fontisan/variation/cache.rb +0 -2
- data/lib/fontisan/variation/converter.rb +0 -3
- data/lib/fontisan/variation/data_extractor.rb +0 -2
- data/lib/fontisan/variation/delta_applier.rb +0 -5
- data/lib/fontisan/variation/inspector.rb +0 -1
- data/lib/fontisan/variation/instance_generator.rb +0 -6
- data/lib/fontisan/variation/instance_writer.rb +0 -5
- data/lib/fontisan/variation/metrics_adjuster.rb +0 -4
- data/lib/fontisan/variation/optimizer.rb +0 -3
- data/lib/fontisan/variation/parallel_generator.rb +0 -3
- data/lib/fontisan/variation/subsetter.rb +0 -4
- data/lib/fontisan/variation/tuple_variation_header.rb +0 -2
- data/lib/fontisan/variation/variable_svg_generator.rb +0 -3
- data/lib/fontisan/variation/variation_context.rb +0 -3
- data/lib/fontisan/variation/variation_preserver.rb +0 -3
- data/lib/fontisan/variation.rb +31 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2.rb +13 -0
- data/lib/fontisan/woff2_font.rb +31 -9
- data/lib/fontisan/woff_font.rb +31 -2
- data/lib/fontisan.rb +121 -196
- metadata +46 -2
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Cldr
|
|
5
|
+
# Parses ICU UnicodeSet bracket notation as used in CLDR
|
|
6
|
+
# exemplarCharacters fields.
|
|
7
|
+
#
|
|
8
|
+
# Supported syntax (sufficient for exemplar sets):
|
|
9
|
+
# - Single chars: `a`, `à`, any BMP or supplementary codepoint
|
|
10
|
+
# - Ranges: `a-z`, `A-Z`
|
|
11
|
+
# - Escapes: `\uXXXX`, `\UXXXXXXXX`, `\u{XXXX...}`
|
|
12
|
+
# - Negation: `[^...]` (inverts against 0..0x10FFFF)
|
|
13
|
+
#
|
|
14
|
+
# Unsupported (CLDR exemplars do not use these; raise ParseError):
|
|
15
|
+
# - Property syntax `[:script=Latin:]`
|
|
16
|
+
# - Set operations `[a-z & [b-c]]`
|
|
17
|
+
# - Nested sets `[a[b-c]]`
|
|
18
|
+
# - Named sequences `{a b c}`
|
|
19
|
+
#
|
|
20
|
+
# Output: sorted, deduplicated Array<Integer> of codepoints.
|
|
21
|
+
module UnicodeSetParser
|
|
22
|
+
class ParseError < Cldr::Error; end
|
|
23
|
+
MAX_CODEPOINT = 0x10FFFF
|
|
24
|
+
private_constant :MAX_CODEPOINT
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# @param set_string [String] bracketed ICU UnicodeSet, e.g. "[a-zà]"
|
|
29
|
+
# @return [Array<Integer>] sorted, deduplicated codepoints
|
|
30
|
+
def call(set_string)
|
|
31
|
+
unless set_string.start_with?("[") && set_string.end_with?("]")
|
|
32
|
+
raise ParseError,
|
|
33
|
+
"input must be bracketed"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
body = set_string[1..-2]
|
|
37
|
+
negate = body.start_with?("^")
|
|
38
|
+
body = body[1..] if negate
|
|
39
|
+
|
|
40
|
+
cps = parse_body(body)
|
|
41
|
+
cps = invert(cps) if negate
|
|
42
|
+
cps.sort.uniq
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Walk the body char by char, emitting codepoints and ranges.
|
|
46
|
+
def parse_body(body)
|
|
47
|
+
cps = []
|
|
48
|
+
chars = body.chars.to_a
|
|
49
|
+
i = 0
|
|
50
|
+
prev_cp = nil
|
|
51
|
+
|
|
52
|
+
while i < chars.length
|
|
53
|
+
ch = chars[i]
|
|
54
|
+
|
|
55
|
+
case ch
|
|
56
|
+
when "\\"
|
|
57
|
+
cp, advance = parse_escape(chars, i)
|
|
58
|
+
cps << cp
|
|
59
|
+
prev_cp = cp
|
|
60
|
+
i += advance
|
|
61
|
+
when "-"
|
|
62
|
+
raise ParseError, "dangling '-' at start" if prev_cp.nil?
|
|
63
|
+
raise ParseError, "dangling '-' at end" if i + 1 >= chars.length
|
|
64
|
+
|
|
65
|
+
next_cp, advance = read_next_codepoint(chars, i + 1)
|
|
66
|
+
raise ParseError, "range with no upper bound" if next_cp.nil?
|
|
67
|
+
|
|
68
|
+
cps.concat(((prev_cp + 1)..next_cp).to_a)
|
|
69
|
+
prev_cp = next_cp
|
|
70
|
+
i += 1 + advance
|
|
71
|
+
when "[", "]"
|
|
72
|
+
raise ParseError, "nested set syntax is not supported"
|
|
73
|
+
when "{"
|
|
74
|
+
raise ParseError, "named sequences ({...}) are not supported"
|
|
75
|
+
when ":"
|
|
76
|
+
raise ParseError, "property syntax ([:...:]) is not supported"
|
|
77
|
+
else
|
|
78
|
+
cps << ch.ord
|
|
79
|
+
prev_cp = ch.ord
|
|
80
|
+
i += 1
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
cps
|
|
85
|
+
end
|
|
86
|
+
private_class_method :parse_body
|
|
87
|
+
|
|
88
|
+
# Read the next codepoint starting at index `start`. Handles escapes.
|
|
89
|
+
# @return [Array(Integer, Integer)] codepoint + chars consumed, or
|
|
90
|
+
# [nil, 0] if no codepoint is available.
|
|
91
|
+
def read_next_codepoint(chars, start)
|
|
92
|
+
return [nil, 0] if start >= chars.length
|
|
93
|
+
|
|
94
|
+
ch = chars[start]
|
|
95
|
+
if ch == "\\"
|
|
96
|
+
parse_escape(chars, start)
|
|
97
|
+
else
|
|
98
|
+
[ch.ord, 1]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
private_class_method :read_next_codepoint
|
|
102
|
+
|
|
103
|
+
# Parse a backslash escape sequence.
|
|
104
|
+
# Supports \uXXXX, \UXXXXXXXX, \u{XXXX...}, and standard backslash
|
|
105
|
+
# escapes (\\, \[, \], \-, \^).
|
|
106
|
+
# @return [Array(Integer, Integer)] codepoint + chars consumed
|
|
107
|
+
def parse_escape(chars, start)
|
|
108
|
+
# chars[start] is "\\"
|
|
109
|
+
return [0, 1] if start + 1 >= chars.length
|
|
110
|
+
|
|
111
|
+
marker = chars[start + 1]
|
|
112
|
+
case marker
|
|
113
|
+
when "u"
|
|
114
|
+
brace_form(chars, start) || four_hex(chars, start, "u")
|
|
115
|
+
when "U"
|
|
116
|
+
eight_hex(chars, start)
|
|
117
|
+
when "\\"
|
|
118
|
+
[0x5C, 2]
|
|
119
|
+
when "[", "]", "-", "^"
|
|
120
|
+
[marker.ord, 2]
|
|
121
|
+
else
|
|
122
|
+
raise ParseError, "unknown escape sequence \\#{marker}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
private_class_method :parse_escape
|
|
126
|
+
|
|
127
|
+
def brace_form(chars, start)
|
|
128
|
+
return nil unless chars[start + 2] == "{"
|
|
129
|
+
|
|
130
|
+
# \u{XXX...} variable hex
|
|
131
|
+
end_idx = (start + 3..).find do |j|
|
|
132
|
+
j >= chars.length || chars[j] == "}"
|
|
133
|
+
end
|
|
134
|
+
if end_idx.nil? || chars[end_idx] != "}"
|
|
135
|
+
raise ParseError,
|
|
136
|
+
"unclosed \\u{ escape"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
hex = chars[(start + 3)...end_idx].join
|
|
140
|
+
cp = hex.to_i(16)
|
|
141
|
+
raise ParseError, "\\u{ escape with no digits" if cp.zero? && hex.empty?
|
|
142
|
+
|
|
143
|
+
[cp, (end_idx - start) + 1]
|
|
144
|
+
end
|
|
145
|
+
private_class_method :brace_form
|
|
146
|
+
|
|
147
|
+
def four_hex(chars, start, marker)
|
|
148
|
+
# \uXXXX — exactly 4 hex digits
|
|
149
|
+
hex = chars[(start + 2), 4]&.join
|
|
150
|
+
if hex.nil? || hex.length < 4
|
|
151
|
+
raise ParseError,
|
|
152
|
+
"truncated \\#{marker} escape"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
cp = hex.to_i(16)
|
|
156
|
+
if cp.zero? && !hex.match?(/\A0+\z/)
|
|
157
|
+
raise ParseError,
|
|
158
|
+
"\\#{marker} escape with non-hex digits"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
[cp, 6]
|
|
162
|
+
end
|
|
163
|
+
private_class_method :four_hex
|
|
164
|
+
|
|
165
|
+
def eight_hex(chars, start)
|
|
166
|
+
# \UXXXXXXXX — exactly 8 hex digits
|
|
167
|
+
hex = chars[(start + 2), 8]&.join
|
|
168
|
+
raise ParseError, "truncated \\U escape" if hex.nil? || hex.length < 8
|
|
169
|
+
|
|
170
|
+
cp = hex.to_i(16)
|
|
171
|
+
if cp.zero? && !hex.match?(/\A0+\z/)
|
|
172
|
+
raise ParseError,
|
|
173
|
+
"\\U escape with non-hex digits"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
[cp, 10]
|
|
177
|
+
end
|
|
178
|
+
private_class_method :eight_hex
|
|
179
|
+
|
|
180
|
+
def invert(cps)
|
|
181
|
+
set = cps.to_set
|
|
182
|
+
(0..MAX_CODEPOINT).each_with_object([]) do |cp, arr|
|
|
183
|
+
arr << cp unless set.include?(cp)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
private_class_method :invert
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "rubygems"
|
|
7
|
+
|
|
8
|
+
module Fontisan
|
|
9
|
+
module Cldr
|
|
10
|
+
# Resolves a user-supplied version intent to a concrete CLDR version.
|
|
11
|
+
#
|
|
12
|
+
# Three input modes:
|
|
13
|
+
#
|
|
14
|
+
# resolve(nil) # default_version from config
|
|
15
|
+
# resolve(:default) # default_version from config
|
|
16
|
+
# resolve("46.0.0") # explicit; validated against known_versions
|
|
17
|
+
# resolve(:latest) # probes GitHub releases, picks highest;
|
|
18
|
+
# # falls back to default on failure
|
|
19
|
+
module VersionResolver
|
|
20
|
+
GITHUB_RELEASE_TAG = %r{ref/tags/(\d+(?:\.\d+)+)}
|
|
21
|
+
private_constant :GITHUB_RELEASE_TAG
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# @param intent [nil, :default, :latest, String]
|
|
25
|
+
# @return [String] a concrete version string
|
|
26
|
+
def resolve(intent)
|
|
27
|
+
case intent
|
|
28
|
+
when nil, :default
|
|
29
|
+
Config.default_version
|
|
30
|
+
when :latest
|
|
31
|
+
probe_latest
|
|
32
|
+
else
|
|
33
|
+
validate!(intent)
|
|
34
|
+
intent
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Raise UnknownVersionError unless `version` is in known_versions.
|
|
39
|
+
# @param version [String]
|
|
40
|
+
# @return [void]
|
|
41
|
+
def validate!(version)
|
|
42
|
+
return if Config.known?(version)
|
|
43
|
+
|
|
44
|
+
raise UnknownVersionError,
|
|
45
|
+
"CLDR version #{version.inspect} is not recognized. " \
|
|
46
|
+
"Known versions: #{Config.known_versions.join(', ')}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Best-effort probe of the GitHub releases API for cldr-json.
|
|
52
|
+
# Returns the highest semver found among tagged releases, or
|
|
53
|
+
# Config.default_version on any failure.
|
|
54
|
+
def probe_latest
|
|
55
|
+
versions = fetch_release_versions
|
|
56
|
+
return fallback_latest("releases listing was empty") if versions.empty?
|
|
57
|
+
|
|
58
|
+
highest = versions.max_by { |v| Gem::Version.new(v) }
|
|
59
|
+
if Config.known?(highest)
|
|
60
|
+
highest
|
|
61
|
+
else
|
|
62
|
+
fallback_latest("#{highest.inspect} is not in known_versions; using default")
|
|
63
|
+
end
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
fallback_latest(e.message)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fallback_latest(reason)
|
|
69
|
+
warn "Cldr::VersionResolver: --latest probe failed (#{reason}); " \
|
|
70
|
+
"falling back to default #{Config.default_version.inspect}"
|
|
71
|
+
Config.default_version
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def fetch_release_versions
|
|
75
|
+
uri = URI(Config.listing_url)
|
|
76
|
+
response = Net::HTTP.get_response(uri)
|
|
77
|
+
return [] unless response.is_a?(Net::HTTPSuccess)
|
|
78
|
+
|
|
79
|
+
releases = JSON.parse(response.body || "[]")
|
|
80
|
+
releases.filter_map do |release|
|
|
81
|
+
tag = release["tag_name"]
|
|
82
|
+
next unless tag
|
|
83
|
+
|
|
84
|
+
match = tag.match(/\A(\d+(?:\.\d+)+)\z/)
|
|
85
|
+
match && match[1]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Namespace hub for CLDR (Common Locale Data Repository) support.
|
|
4
|
+
#
|
|
5
|
+
# Provides per-language exemplar character sets so the audit can
|
|
6
|
+
# compute "this font covers X% of language Y". All Cldr::* constants
|
|
7
|
+
# are autoloaded from here.
|
|
8
|
+
|
|
9
|
+
module Fontisan
|
|
10
|
+
module Cldr
|
|
11
|
+
autoload :Config, "fontisan/cldr/config"
|
|
12
|
+
autoload :CacheManager, "fontisan/cldr/cache_manager"
|
|
13
|
+
autoload :Downloader, "fontisan/cldr/downloader"
|
|
14
|
+
autoload :VersionResolver, "fontisan/cldr/version_resolver"
|
|
15
|
+
autoload :IndexBuilder, "fontisan/cldr/index_builder"
|
|
16
|
+
autoload :Index, "fontisan/cldr/index"
|
|
17
|
+
autoload :Aggregator, "fontisan/cldr/aggregator"
|
|
18
|
+
autoload :UnicodeSetParser, "fontisan/cldr/unicode_set_parser"
|
|
19
|
+
autoload :Error, "fontisan/cldr/error"
|
|
20
|
+
autoload :DownloadError, "fontisan/cldr/download_error"
|
|
21
|
+
autoload :UnknownVersionError, "fontisan/cldr/unknown_version_error"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
# Thor subcommand for managing the local CLDR (Common Locale Data
|
|
7
|
+
# Repository) cache used by `fontisan audit` for per-language coverage.
|
|
8
|
+
#
|
|
9
|
+
# fontisan cldr download [VERSION] fetch + index CLDR exemplars
|
|
10
|
+
# fontisan cldr status show what's cached
|
|
11
|
+
# fontisan cldr path [VERSION] print local cache path
|
|
12
|
+
# fontisan cldr list list known versions
|
|
13
|
+
# fontisan cldr remove VERSION delete a cached version
|
|
14
|
+
#
|
|
15
|
+
# With no arguments, `download` resolves the configured default version
|
|
16
|
+
# (see lib/fontisan/config/cldr.yml).
|
|
17
|
+
class CldrCli < Thor
|
|
18
|
+
desc "download [VERSION]",
|
|
19
|
+
"Download and index CLDR exemplar characters (default: configured default version)"
|
|
20
|
+
option :force, type: :boolean, default: false,
|
|
21
|
+
desc: "Re-download even if already cached"
|
|
22
|
+
option :latest, type: :boolean, default: false,
|
|
23
|
+
desc: "Probe GitHub releases for the latest version"
|
|
24
|
+
def download(version = nil)
|
|
25
|
+
intent = resolve_intent(version, options[:latest])
|
|
26
|
+
actual = Cldr::VersionResolver.resolve(intent)
|
|
27
|
+
|
|
28
|
+
Cldr::Downloader.download(actual, force: options[:force])
|
|
29
|
+
Cldr::IndexBuilder.build(actual) unless index_present?(actual)
|
|
30
|
+
puts "CLDR #{actual} ready at: #{Cldr::CacheManager.version_dir(actual)}"
|
|
31
|
+
rescue Cldr::Error => e
|
|
32
|
+
warn "ERROR: #{e.message}"
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "status", "Show cached CLDR versions and default version"
|
|
37
|
+
def status
|
|
38
|
+
cached = Cldr::CacheManager.cached_versions
|
|
39
|
+
puts "Default version: #{Cldr::Config.default_version}"
|
|
40
|
+
puts "Cache root: #{Cldr::CacheManager.root}"
|
|
41
|
+
puts "Cached versions: #{cached.empty? ? '(none)' : cached.join(', ')}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
desc "path [VERSION]", "Print local cache directory for a version"
|
|
45
|
+
def path(version = nil)
|
|
46
|
+
actual = Cldr::VersionResolver.resolve(version)
|
|
47
|
+
puts Cldr::CacheManager.version_dir(actual)
|
|
48
|
+
rescue Cldr::UnknownVersionError => e
|
|
49
|
+
warn "ERROR: #{e.message}"
|
|
50
|
+
exit 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "list", "List CLDR versions known to this Fontisan release"
|
|
54
|
+
def list
|
|
55
|
+
Cldr::Config.known_versions.each { |v| puts v }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc "remove VERSION", "Remove a cached CLDR version"
|
|
59
|
+
def remove(version)
|
|
60
|
+
Cldr::VersionResolver.validate!(version)
|
|
61
|
+
unless Cldr::CacheManager.cached?(version)
|
|
62
|
+
warn "Version #{version} is not cached; nothing to remove."
|
|
63
|
+
return
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
Cldr::CacheManager.remove_version(version)
|
|
67
|
+
puts "Removed CLDR #{version}."
|
|
68
|
+
rescue Cldr::UnknownVersionError => e
|
|
69
|
+
warn "ERROR: #{e.message}"
|
|
70
|
+
exit 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def resolve_intent(version, latest)
|
|
76
|
+
return :latest if latest && version.nil?
|
|
77
|
+
|
|
78
|
+
version
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def index_present?(version)
|
|
82
|
+
Cldr::CacheManager.languages_index_path(version).exist?
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/fontisan/cli.rb
CHANGED
|
@@ -25,6 +25,9 @@ module Fontisan
|
|
|
25
25
|
desc: "Suppress non-error output",
|
|
26
26
|
aliases: "-q"
|
|
27
27
|
|
|
28
|
+
desc "cldr", "Manage local CLDR cache (subcommands)", hide: true
|
|
29
|
+
subcommand "cldr", CldrCli
|
|
30
|
+
|
|
28
31
|
desc "info PATH", "Display font information"
|
|
29
32
|
option :brief, type: :boolean, default: false,
|
|
30
33
|
desc: "Brief mode - only essential info (5x faster, uses metadata loading)",
|
|
@@ -203,7 +206,9 @@ module Fontisan
|
|
|
203
206
|
|
|
204
207
|
desc "convert FONT_FILE", "Convert font to different format"
|
|
205
208
|
option :to, type: :string, required: true,
|
|
206
|
-
desc: "Target format
|
|
209
|
+
desc: "Target format: ttf, otf, type1, t1, ttc, otc, dfont, svg, " \
|
|
210
|
+
"woff (zlib — works on all browsers incl. legacy), " \
|
|
211
|
+
"woff2 (Brotli — ~30% smaller, modern browsers only)",
|
|
207
212
|
aliases: "-t"
|
|
208
213
|
option :output, type: :string,
|
|
209
214
|
desc: "Output file path (required unless --show-options)",
|
|
@@ -255,6 +260,19 @@ module Fontisan
|
|
|
255
260
|
desc: "Enable table optimization"
|
|
256
261
|
option :decompose_on_output, type: :boolean,
|
|
257
262
|
desc: "Decompose on output (generating option)"
|
|
263
|
+
# Compression knobs — declared by each strategy (single source of truth:
|
|
264
|
+
# Converters::* strategies). Cross-format misuse is caught at convert time
|
|
265
|
+
# by FormatConverter.validate_options_for_target! with a clear ArgumentError.
|
|
266
|
+
option :zlib_level, type: :numeric,
|
|
267
|
+
desc: "WOFF only: zlib compression level (0–9, default 6)"
|
|
268
|
+
option :uncompressed, type: :boolean,
|
|
269
|
+
desc: "WOFF only: store tables uncompressed (legal per WOFF 1.0 §5.1)"
|
|
270
|
+
option :compression_threshold, type: :numeric,
|
|
271
|
+
desc: "WOFF only: skip compression for tables smaller than N bytes (default 100)"
|
|
272
|
+
option :brotli_quality, type: :numeric,
|
|
273
|
+
desc: "WOFF2 only: Brotli quality (0–11, default 11)"
|
|
274
|
+
option :transform_tables, type: :boolean,
|
|
275
|
+
desc: "WOFF2 only: apply glyf/loca and hmtx transformations"
|
|
258
276
|
# Convert a font to a different format using the universal transformation pipeline.
|
|
259
277
|
#
|
|
260
278
|
# Supported conversions:
|
|
@@ -264,6 +282,14 @@ module Fontisan
|
|
|
264
282
|
# - Variable fonts: Automatic variation preservation or instance generation
|
|
265
283
|
# - Collections (TTC/OTC/dfont): Preserve mixed TTF+OTF by default, or standardize with --target-format
|
|
266
284
|
#
|
|
285
|
+
# Web fonts — WOFF vs WOFF2:
|
|
286
|
+
# WOFF uses zlib compression and works on every browser that supports
|
|
287
|
+
# web fonts at all (IE9+, all evergreen browsers). WOFF2 uses Brotli and
|
|
288
|
+
# is ~30% smaller but requires modern browsers (Chrome 36+, Firefox 39+,
|
|
289
|
+
# Safari 12+, Edge 14+). Old browsers cannot decode Brotli, so for legacy
|
|
290
|
+
# support serve WOFF. Tune knobs with --zlib-level / --brotli-quality /
|
|
291
|
+
# --uncompressed (WOFF only) / --transform-tables (WOFF2 only).
|
|
292
|
+
#
|
|
267
293
|
# Collection Format Support:
|
|
268
294
|
# TTC, OTC, and dfont all support mixed TrueType and OpenType fonts. By default, original font formats
|
|
269
295
|
# are preserved during collection conversion (--target-format preserve). Use --target-format ttf to
|
|
@@ -678,6 +704,10 @@ module Fontisan
|
|
|
678
704
|
puts output unless options[:quiet]
|
|
679
705
|
end
|
|
680
706
|
|
|
707
|
+
def serialize_report(report, format)
|
|
708
|
+
format == :json ? report.to_json : report.to_yaml
|
|
709
|
+
end
|
|
710
|
+
|
|
681
711
|
# Format result as human-readable text.
|
|
682
712
|
#
|
|
683
713
|
# @param result [Object] The result object to format
|
|
@@ -720,7 +750,6 @@ module Fontisan
|
|
|
720
750
|
#
|
|
721
751
|
# @return [void]
|
|
722
752
|
def list_available_tests
|
|
723
|
-
require_relative "validators/profile_loader"
|
|
724
753
|
profiles = Validators::ProfileLoader.all_profiles
|
|
725
754
|
puts "Available validation profiles:"
|
|
726
755
|
profiles.each do |profile_name, config|
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../font_writer"
|
|
4
|
-
require_relative "../error"
|
|
5
|
-
require_relative "../validation/collection_validator"
|
|
6
|
-
|
|
7
3
|
module Fontisan
|
|
8
4
|
module Collection
|
|
9
5
|
# DfontBuilder creates Apple dfont (Data Fork Font) resource fork structures
|
|
@@ -15,8 +15,6 @@ module Fontisan
|
|
|
15
15
|
# @param fonts [Array<TrueTypeFont, OpenTypeFont>] Array of fonts
|
|
16
16
|
# @return [Models::TableSharingInfo] Sharing statistics
|
|
17
17
|
def calculate_table_sharing_for_fonts(fonts)
|
|
18
|
-
require_relative "../models/table_sharing_info"
|
|
19
|
-
|
|
20
18
|
# Build table hash map (checksum -> size)
|
|
21
19
|
table_map = {}
|
|
22
20
|
total_table_size = 0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Autoload hub for the Fontisan::Collection namespace.
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Collection
|
|
7
|
+
autoload :Builder, "fontisan/collection/builder"
|
|
8
|
+
autoload :DfontBuilder, "fontisan/collection/dfont_builder"
|
|
9
|
+
autoload :OffsetCalculator, "fontisan/collection/offset_calculator"
|
|
10
|
+
autoload :SharedLogic, "fontisan/collection/shared_logic"
|
|
11
|
+
autoload :TableAnalyzer, "fontisan/collection/table_analyzer"
|
|
12
|
+
autoload :TableDeduplicator, "fontisan/collection/table_deduplicator"
|
|
13
|
+
autoload :Writer, "fontisan/collection/writer"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "base_command"
|
|
4
|
-
require_relative "../pipeline/transformation_pipeline"
|
|
5
|
-
require_relative "../converters/collection_converter"
|
|
6
|
-
require_relative "../conversion_options"
|
|
7
|
-
require_relative "../font_loader"
|
|
8
|
-
|
|
9
3
|
module Fontisan
|
|
10
4
|
module Commands
|
|
11
5
|
# Command for converting fonts between formats
|
|
@@ -67,29 +61,33 @@ module Fontisan
|
|
|
67
61
|
def initialize(font_path, options = {})
|
|
68
62
|
super(font_path, options)
|
|
69
63
|
|
|
70
|
-
# Convert string keys to symbols for Thor compatibility
|
|
71
|
-
|
|
64
|
+
# Convert string keys to symbols for Thor compatibility. All command
|
|
65
|
+
# code reads @options with symbol keys (e.g., @options[:quiet]);
|
|
66
|
+
# normalizing once at construction is cleaner than threading a
|
|
67
|
+
# second opts hash alongside @options.
|
|
68
|
+
@options = options.transform_keys(&:to_sym)
|
|
72
69
|
|
|
73
|
-
@output_path =
|
|
70
|
+
@output_path = @options[:output]
|
|
74
71
|
|
|
75
72
|
# Parse target format
|
|
76
|
-
@target_format = parse_target_format(
|
|
73
|
+
@target_format = parse_target_format(@options[:to])
|
|
77
74
|
|
|
78
75
|
# Extract ConversionOptions if provided
|
|
79
|
-
@conv_options = extract_conversion_options(
|
|
76
|
+
@conv_options = extract_conversion_options(@options)
|
|
80
77
|
|
|
81
78
|
# Parse coordinates if string provided
|
|
82
|
-
@coordinates = if
|
|
83
|
-
parse_coordinates(
|
|
84
|
-
elsif
|
|
85
|
-
|
|
79
|
+
@coordinates = if @options[:coordinates]
|
|
80
|
+
parse_coordinates(@options[:coordinates])
|
|
81
|
+
elsif @options[:instance_coordinates]
|
|
82
|
+
@options[:instance_coordinates]
|
|
86
83
|
end
|
|
87
84
|
|
|
88
|
-
@instance_index =
|
|
89
|
-
@preserve_variation =
|
|
90
|
-
@preserve_hints =
|
|
91
|
-
@collection_target_format =
|
|
92
|
-
|
|
85
|
+
@instance_index = @options[:instance_index]
|
|
86
|
+
@preserve_variation = @options[:preserve_variation]
|
|
87
|
+
@preserve_hints = @options.fetch(:preserve_hints, false)
|
|
88
|
+
@collection_target_format = @options.fetch(:target_format,
|
|
89
|
+
"preserve").to_s
|
|
90
|
+
@validate = !@options[:no_validate]
|
|
93
91
|
end
|
|
94
92
|
|
|
95
93
|
# Execute the conversion
|
|
@@ -152,6 +150,13 @@ module Fontisan
|
|
|
152
150
|
# Add hint preservation option
|
|
153
151
|
pipeline_options[:preserve_hints] = @preserve_hints if @preserve_hints
|
|
154
152
|
|
|
153
|
+
# Forward format-specific compression knobs declared by strategies
|
|
154
|
+
# (zlib_level, brotli_quality, etc.). Cross-format misuse is caught
|
|
155
|
+
# downstream by FormatConverter.validate_options_for_target!.
|
|
156
|
+
Converters::FormatConverter.all_strategy_option_names.each do |opt|
|
|
157
|
+
pipeline_options[opt] = @options[opt] if @options.key?(opt)
|
|
158
|
+
end
|
|
159
|
+
|
|
155
160
|
# Use TransformationPipeline for universal conversion
|
|
156
161
|
pipeline = Pipeline::TransformationPipeline.new(
|
|
157
162
|
font_path,
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
|
-
require_relative "base_command"
|
|
5
|
-
require_relative "../variation/instance_generator"
|
|
6
|
-
require_relative "../variation/instance_writer"
|
|
7
|
-
require_relative "../variation/validator"
|
|
8
|
-
require_relative "../error"
|
|
9
4
|
|
|
10
5
|
module Fontisan
|
|
11
6
|
module Commands
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../font_loader"
|
|
4
|
-
require_relative "../models/collection_list_info"
|
|
5
|
-
require_relative "../models/font_summary"
|
|
6
|
-
require_relative "../tables/name"
|
|
7
|
-
require_relative "../error"
|
|
8
|
-
|
|
9
3
|
module Fontisan
|
|
10
4
|
module Commands
|
|
11
5
|
# Command to list contents of font files (collections or individual fonts).
|