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.
Files changed (249) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/.rubocop_todo.yml +86 -86
  4. data/CHANGELOG.md +12 -2
  5. data/README.adoc +53 -1
  6. data/docs/.vitepress/config.ts +68 -8
  7. data/docs/.vitepress/theme/style.css +570 -272
  8. data/docs/CONVERSION_GUIDE.adoc +31 -8
  9. data/docs/EXTRACT_TTC_MIGRATION.md +1 -1
  10. data/docs/WOFF_WOFF2_FORMATS.adoc +53 -0
  11. data/docs/api/conversion-options.md +37 -14
  12. data/docs/cli/audit.md +337 -0
  13. data/docs/cli/convert.md +20 -1
  14. data/docs/cli/index.md +31 -0
  15. data/docs/guide/color.md +1 -1
  16. data/docs/guide/conversion/options.md +32 -3
  17. data/docs/guide/conversion/ttf-otf.md +1 -1
  18. data/docs/guide/conversion/type1.md +1 -1
  19. data/docs/guide/conversion/web.md +91 -32
  20. data/docs/guide/conversion.md +6 -5
  21. data/docs/guide/formats/woff.md +35 -11
  22. data/docs/guide/index.md +2 -2
  23. data/docs/guide/migrations/extract-ttc.md +1 -1
  24. data/docs/guide/quick-start.md +4 -4
  25. data/docs/guide/type1.md +4 -4
  26. data/docs/guide/woff.md +19 -17
  27. data/docs/index.md +2 -0
  28. data/docs/lychee.toml +5 -1
  29. data/docs/package.json +1 -1
  30. data/docs/public/robots.txt +4 -0
  31. data/docs/scripts/post-build.mjs +81 -0
  32. data/lib/fontisan/base_collection.rb +23 -9
  33. data/lib/fontisan/binary/structures.rb +0 -2
  34. data/lib/fontisan/binary.rb +11 -0
  35. data/lib/fontisan/cldr/aggregator.rb +33 -0
  36. data/lib/fontisan/cldr/cache_manager.rb +110 -0
  37. data/lib/fontisan/cldr/config.rb +59 -0
  38. data/lib/fontisan/cldr/download_error.rb +9 -0
  39. data/lib/fontisan/cldr/downloader.rb +79 -0
  40. data/lib/fontisan/cldr/error.rb +8 -0
  41. data/lib/fontisan/cldr/index.rb +64 -0
  42. data/lib/fontisan/cldr/index_builder.rb +72 -0
  43. data/lib/fontisan/cldr/unicode_set_parser.rb +189 -0
  44. data/lib/fontisan/cldr/unknown_version_error.rb +9 -0
  45. data/lib/fontisan/cldr/version_resolver.rb +91 -0
  46. data/lib/fontisan/cldr.rb +23 -0
  47. data/lib/fontisan/cli/cldr_cli.rb +85 -0
  48. data/lib/fontisan/cli.rb +31 -2
  49. data/lib/fontisan/collection/builder.rb +0 -4
  50. data/lib/fontisan/collection/dfont_builder.rb +0 -4
  51. data/lib/fontisan/collection/shared_logic.rb +0 -2
  52. data/lib/fontisan/collection/writer.rb +0 -3
  53. data/lib/fontisan/collection.rb +15 -0
  54. data/lib/fontisan/commands/base_command.rb +0 -3
  55. data/lib/fontisan/commands/convert_command.rb +25 -20
  56. data/lib/fontisan/commands/dump_table_command.rb +0 -3
  57. data/lib/fontisan/commands/export_command.rb +0 -4
  58. data/lib/fontisan/commands/features_command.rb +0 -3
  59. data/lib/fontisan/commands/instance_command.rb +0 -5
  60. data/lib/fontisan/commands/ls_command.rb +0 -6
  61. data/lib/fontisan/commands/optical_size_command.rb +0 -3
  62. data/lib/fontisan/commands/pack_command.rb +0 -5
  63. data/lib/fontisan/commands/scripts_command.rb +0 -2
  64. data/lib/fontisan/commands/subset_command.rb +0 -3
  65. data/lib/fontisan/commands/unicode_command.rb +0 -3
  66. data/lib/fontisan/commands/unpack_command.rb +0 -7
  67. data/lib/fontisan/commands/validate_command.rb +0 -8
  68. data/lib/fontisan/commands/variable_command.rb +0 -3
  69. data/lib/fontisan/commands.rb +26 -0
  70. data/lib/fontisan/config/cldr.yml +22 -0
  71. data/lib/fontisan/config/conversion_matrix.yml +38 -0
  72. data/lib/fontisan/constants.rb +19 -0
  73. data/lib/fontisan/conversion_options.rb +30 -19
  74. data/lib/fontisan/converters/cff_table_builder.rb +0 -3
  75. data/lib/fontisan/converters/collection_converter.rb +0 -8
  76. data/lib/fontisan/converters/conversion_strategy.rb +161 -46
  77. data/lib/fontisan/converters/format_converter.rb +143 -32
  78. data/lib/fontisan/converters/glyf_table_builder.rb +0 -2
  79. data/lib/fontisan/converters/outline_converter.rb +0 -19
  80. data/lib/fontisan/converters/outline_extraction.rb +0 -5
  81. data/lib/fontisan/converters/outline_optimizer.rb +0 -5
  82. data/lib/fontisan/converters/svg_generator.rb +0 -4
  83. data/lib/fontisan/converters/table_copier.rb +0 -2
  84. data/lib/fontisan/converters/type1_converter.rb +0 -11
  85. data/lib/fontisan/converters/woff2_encoder.rb +49 -20
  86. data/lib/fontisan/converters/woff_writer.rb +211 -282
  87. data/lib/fontisan/converters.rb +21 -0
  88. data/lib/fontisan/dfont_collection.rb +29 -10
  89. data/lib/fontisan/export/exporter.rb +0 -6
  90. data/lib/fontisan/export/transformers/font_to_ttx.rb +0 -9
  91. data/lib/fontisan/export/transformers/head_transformer.rb +0 -2
  92. data/lib/fontisan/export/transformers/hhea_transformer.rb +0 -2
  93. data/lib/fontisan/export/transformers/maxp_transformer.rb +0 -2
  94. data/lib/fontisan/export/transformers/name_transformer.rb +0 -2
  95. data/lib/fontisan/export/transformers/os2_transformer.rb +0 -2
  96. data/lib/fontisan/export/transformers/post_transformer.rb +0 -2
  97. data/lib/fontisan/export/transformers.rb +17 -0
  98. data/lib/fontisan/export.rb +13 -0
  99. data/lib/fontisan/font_loader.rb +14 -19
  100. data/lib/fontisan/font_writer.rb +0 -1
  101. data/lib/fontisan/formatters.rb +9 -0
  102. data/lib/fontisan/hints/hint_converter.rb +0 -1
  103. data/lib/fontisan/hints/postscript_hint_applier.rb +0 -9
  104. data/lib/fontisan/hints/postscript_hint_extractor.rb +0 -2
  105. data/lib/fontisan/hints/truetype_hint_extractor.rb +0 -2
  106. data/lib/fontisan/hints.rb +19 -0
  107. data/lib/fontisan/metrics_calculator.rb +0 -2
  108. data/lib/fontisan/models/all_scripts_features_info.rb +0 -1
  109. data/lib/fontisan/models/cldr/language_coverage.rb +31 -0
  110. data/lib/fontisan/models/cldr.rb +12 -0
  111. data/lib/fontisan/models/collection_brief_info.rb +0 -1
  112. data/lib/fontisan/models/collection_info.rb +0 -2
  113. data/lib/fontisan/models/collection_list_info.rb +0 -1
  114. data/lib/fontisan/models/collection_validation_report.rb +0 -2
  115. data/lib/fontisan/models/color_glyph.rb +0 -1
  116. data/lib/fontisan/models/font_report.rb +0 -1
  117. data/lib/fontisan/models/ttx/tables.rb +21 -0
  118. data/lib/fontisan/models/ttx/ttfont.rb +0 -8
  119. data/lib/fontisan/models/ttx.rb +14 -0
  120. data/lib/fontisan/models.rb +47 -0
  121. data/lib/fontisan/open_type_collection.rb +6 -5
  122. data/lib/fontisan/open_type_font.rb +8 -2
  123. data/lib/fontisan/open_type_font_extensions.rb +9 -9
  124. data/lib/fontisan/optimizers/pattern_analyzer.rb +0 -1
  125. data/lib/fontisan/optimizers.rb +14 -0
  126. data/lib/fontisan/outline_extractor.rb +0 -2
  127. data/lib/fontisan/parsers/dfont_parser.rb +0 -1
  128. data/lib/fontisan/parsers.rb +10 -0
  129. data/lib/fontisan/pipeline/format_detector.rb +29 -102
  130. data/lib/fontisan/pipeline/output_writer.rb +11 -9
  131. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +0 -4
  132. data/lib/fontisan/pipeline/strategies/named_strategy.rb +0 -4
  133. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +0 -2
  134. data/lib/fontisan/pipeline/strategies.rb +16 -0
  135. data/lib/fontisan/pipeline/transformation_pipeline.rb +0 -7
  136. data/lib/fontisan/pipeline/variation_resolver.rb +0 -7
  137. data/lib/fontisan/pipeline.rb +14 -0
  138. data/lib/fontisan/sfnt_font.rb +29 -14
  139. data/lib/fontisan/sfnt_table.rb +0 -4
  140. data/lib/fontisan/subset/builder.rb +0 -6
  141. data/lib/fontisan/subset.rb +13 -0
  142. data/lib/fontisan/svg/font_generator.rb +0 -4
  143. data/lib/fontisan/svg/glyph_generator.rb +0 -2
  144. data/lib/fontisan/svg.rb +12 -0
  145. data/lib/fontisan/tables/cbdt.rb +0 -1
  146. data/lib/fontisan/tables/cblc.rb +0 -1
  147. data/lib/fontisan/tables/cff/charset.rb +0 -1
  148. data/lib/fontisan/tables/cff/charstring.rb +0 -1
  149. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +0 -4
  150. data/lib/fontisan/tables/cff/charstrings_index.rb +0 -3
  151. data/lib/fontisan/tables/cff/dict.rb +0 -1
  152. data/lib/fontisan/tables/cff/encoding.rb +0 -1
  153. data/lib/fontisan/tables/cff/header.rb +0 -2
  154. data/lib/fontisan/tables/cff/hint_operation_injector.rb +0 -2
  155. data/lib/fontisan/tables/cff/index.rb +0 -1
  156. data/lib/fontisan/tables/cff/private_dict.rb +0 -2
  157. data/lib/fontisan/tables/cff/private_dict_writer.rb +0 -2
  158. data/lib/fontisan/tables/cff/table_builder.rb +0 -6
  159. data/lib/fontisan/tables/cff/top_dict.rb +0 -2
  160. data/lib/fontisan/tables/cff.rb +23 -15
  161. data/lib/fontisan/tables/cff2/charstring_parser.rb +0 -2
  162. data/lib/fontisan/tables/cff2/table_builder.rb +0 -11
  163. data/lib/fontisan/tables/cff2/table_reader.rb +0 -2
  164. data/lib/fontisan/tables/cff2.rb +13 -14
  165. data/lib/fontisan/tables/cmap.rb +24 -2
  166. data/lib/fontisan/tables/cmap_table.rb +0 -3
  167. data/lib/fontisan/tables/colr.rb +0 -1
  168. data/lib/fontisan/tables/cpal.rb +0 -1
  169. data/lib/fontisan/tables/cvar.rb +0 -2
  170. data/lib/fontisan/tables/fvar.rb +0 -1
  171. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +0 -2
  172. data/lib/fontisan/tables/glyf/glyph_builder.rb +0 -3
  173. data/lib/fontisan/tables/glyf.rb +0 -6
  174. data/lib/fontisan/tables/glyf_table.rb +0 -3
  175. data/lib/fontisan/tables/gpos.rb +0 -2
  176. data/lib/fontisan/tables/gsub.rb +0 -2
  177. data/lib/fontisan/tables/gvar.rb +0 -2
  178. data/lib/fontisan/tables/head.rb +0 -2
  179. data/lib/fontisan/tables/head_table.rb +0 -3
  180. data/lib/fontisan/tables/hhea.rb +0 -2
  181. data/lib/fontisan/tables/hhea_table.rb +0 -3
  182. data/lib/fontisan/tables/hmtx.rb +0 -2
  183. data/lib/fontisan/tables/hmtx_table.rb +0 -3
  184. data/lib/fontisan/tables/hvar.rb +0 -3
  185. data/lib/fontisan/tables/loca.rb +0 -2
  186. data/lib/fontisan/tables/loca_table.rb +0 -3
  187. data/lib/fontisan/tables/maxp.rb +0 -2
  188. data/lib/fontisan/tables/maxp_table.rb +0 -3
  189. data/lib/fontisan/tables/mvar.rb +0 -3
  190. data/lib/fontisan/tables/name.rb +0 -2
  191. data/lib/fontisan/tables/name_table.rb +0 -3
  192. data/lib/fontisan/tables/os2_table.rb +0 -3
  193. data/lib/fontisan/tables/post_table.rb +0 -3
  194. data/lib/fontisan/tables/sbix.rb +0 -1
  195. data/lib/fontisan/tables/svg.rb +0 -1
  196. data/lib/fontisan/tables/variation_common.rb +0 -1
  197. data/lib/fontisan/tables/vvar.rb +0 -3
  198. data/lib/fontisan/tables.rb +55 -0
  199. data/lib/fontisan/true_type_collection.rb +6 -14
  200. data/lib/fontisan/true_type_font.rb +8 -2
  201. data/lib/fontisan/true_type_font_extensions.rb +9 -9
  202. data/lib/fontisan/type1/afm_generator.rb +0 -4
  203. data/lib/fontisan/type1/conversion_options.rb +0 -2
  204. data/lib/fontisan/type1/encodings.rb +0 -2
  205. data/lib/fontisan/type1/generator.rb +0 -8
  206. data/lib/fontisan/type1/pfa_generator.rb +0 -3
  207. data/lib/fontisan/type1/pfb_generator.rb +0 -5
  208. data/lib/fontisan/type1/pfm_generator.rb +0 -4
  209. data/lib/fontisan/type1.rb +42 -69
  210. data/lib/fontisan/type1_font.rb +40 -11
  211. data/lib/fontisan/utilities/checksum_calculator.rb +0 -1
  212. data/lib/fontisan/utilities.rb +10 -0
  213. data/lib/fontisan/utils.rb +10 -0
  214. data/lib/fontisan/validation/collection_validator.rb +0 -2
  215. data/lib/fontisan/validation.rb +9 -0
  216. data/lib/fontisan/validators/basic_validator.rb +0 -2
  217. data/lib/fontisan/validators/font_book_validator.rb +0 -2
  218. data/lib/fontisan/validators/opentype_validator.rb +0 -2
  219. data/lib/fontisan/validators/profile_loader.rb +0 -5
  220. data/lib/fontisan/validators/validator.rb +0 -2
  221. data/lib/fontisan/validators/web_font_validator.rb +0 -2
  222. data/lib/fontisan/validators.rb +14 -0
  223. data/lib/fontisan/variable/delta_applicator.rb +0 -4
  224. data/lib/fontisan/variable/instancer.rb +0 -3
  225. data/lib/fontisan/variable/static_font_builder.rb +0 -3
  226. data/lib/fontisan/variable.rb +16 -0
  227. data/lib/fontisan/variation/blend_applier.rb +0 -2
  228. data/lib/fontisan/variation/cache.rb +0 -2
  229. data/lib/fontisan/variation/converter.rb +0 -3
  230. data/lib/fontisan/variation/data_extractor.rb +0 -2
  231. data/lib/fontisan/variation/delta_applier.rb +0 -5
  232. data/lib/fontisan/variation/inspector.rb +0 -1
  233. data/lib/fontisan/variation/instance_generator.rb +0 -6
  234. data/lib/fontisan/variation/instance_writer.rb +0 -5
  235. data/lib/fontisan/variation/metrics_adjuster.rb +0 -4
  236. data/lib/fontisan/variation/optimizer.rb +0 -3
  237. data/lib/fontisan/variation/parallel_generator.rb +0 -3
  238. data/lib/fontisan/variation/subsetter.rb +0 -4
  239. data/lib/fontisan/variation/tuple_variation_header.rb +0 -2
  240. data/lib/fontisan/variation/variable_svg_generator.rb +0 -3
  241. data/lib/fontisan/variation/variation_context.rb +0 -3
  242. data/lib/fontisan/variation/variation_preserver.rb +0 -3
  243. data/lib/fontisan/variation.rb +31 -0
  244. data/lib/fontisan/version.rb +1 -1
  245. data/lib/fontisan/woff2.rb +13 -0
  246. data/lib/fontisan/woff2_font.rb +31 -9
  247. data/lib/fontisan/woff_font.rb +31 -2
  248. data/lib/fontisan.rb +121 -196
  249. 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Cldr
5
+ # Raised by Cldr::VersionResolver when a user-supplied version string
6
+ # is not in Cldr::Config.known_versions.
7
+ class UnknownVersionError < Cldr::Error; end
8
+ end
9
+ 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 (ttf, otf, type1, t1, woff, woff2)",
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 "table_analyzer"
4
- require_relative "table_deduplicator"
5
- require_relative "offset_calculator"
6
- require_relative "writer"
7
3
  require "yaml"
8
4
 
9
5
  module Fontisan
@@ -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
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../constants"
4
- require_relative "../utilities/checksum_calculator"
5
-
6
3
  module Fontisan
7
4
  module Collection
8
5
  # CollectionWriter writes binary TTC/OTC files
@@ -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,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../font_loader"
4
- require_relative "../error"
5
-
6
3
  module Fontisan
7
4
  module Commands
8
5
  # Abstract base class for all CLI commands.
@@ -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
- opts = options.transform_keys(&:to_sym)
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 = opts[:output]
70
+ @output_path = @options[:output]
74
71
 
75
72
  # Parse target format
76
- @target_format = parse_target_format(opts[:to])
73
+ @target_format = parse_target_format(@options[:to])
77
74
 
78
75
  # Extract ConversionOptions if provided
79
- @conv_options = extract_conversion_options(opts)
76
+ @conv_options = extract_conversion_options(@options)
80
77
 
81
78
  # Parse coordinates if string provided
82
- @coordinates = if opts[:coordinates]
83
- parse_coordinates(opts[:coordinates])
84
- elsif opts[:instance_coordinates]
85
- opts[:instance_coordinates]
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 = opts[:instance_index]
89
- @preserve_variation = opts[:preserve_variation]
90
- @preserve_hints = opts.fetch(:preserve_hints, false)
91
- @collection_target_format = opts.fetch(:target_format, "preserve").to_s
92
- @validate = !opts[:no_validate]
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,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../font_loader"
4
- require_relative "../error"
5
-
6
3
  module Fontisan
7
4
  module Commands
8
5
  # Command to dump raw table data from fonts
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base_command"
4
- require_relative "../export/exporter"
5
- require_relative "../font_loader"
6
-
7
3
  module Fontisan
8
4
  module Commands
9
5
  # ExportCommand provides CLI interface for font export to YAML/JSON
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
- require_relative "base_command"
5
- require_relative "../models/features_info"
6
- require_relative "../models/all_scripts_features_info"
7
4
 
8
5
  module Fontisan
9
6
  module Commands
@@ -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).