fontisan 0.2.17 → 0.2.22

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 (316) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +14 -90
  3. data/README.adoc +257 -1
  4. data/docs/.vitepress/config.ts +68 -8
  5. data/docs/.vitepress/theme/style.css +570 -272
  6. data/docs/CONVERSION_GUIDE.adoc +31 -8
  7. data/docs/EXTRACT_TTC_MIGRATION.md +1 -1
  8. data/docs/WOFF_WOFF2_FORMATS.adoc +53 -0
  9. data/docs/api/conversion-options.md +37 -14
  10. data/docs/cli/audit.md +337 -0
  11. data/docs/cli/convert.md +20 -1
  12. data/docs/cli/index.md +31 -0
  13. data/docs/guide/color.md +1 -1
  14. data/docs/guide/conversion/options.md +32 -3
  15. data/docs/guide/conversion/ttf-otf.md +1 -1
  16. data/docs/guide/conversion/type1.md +1 -1
  17. data/docs/guide/conversion/web.md +91 -32
  18. data/docs/guide/conversion.md +6 -5
  19. data/docs/guide/formats/woff.md +35 -11
  20. data/docs/guide/index.md +2 -2
  21. data/docs/guide/migrations/extract-ttc.md +1 -1
  22. data/docs/guide/quick-start.md +4 -4
  23. data/docs/guide/type1.md +4 -4
  24. data/docs/guide/woff.md +19 -17
  25. data/docs/index.md +2 -0
  26. data/docs/lychee.toml +5 -1
  27. data/docs/package.json +1 -1
  28. data/docs/public/robots.txt +4 -0
  29. data/docs/scripts/post-build.mjs +81 -0
  30. data/lib/fontisan/audit/codepoint_range_coalescer.rb +41 -0
  31. data/lib/fontisan/audit/context.rb +122 -0
  32. data/lib/fontisan/audit/differ.rb +124 -0
  33. data/lib/fontisan/audit/extractors/aggregations.rb +54 -0
  34. data/lib/fontisan/audit/extractors/base.rb +26 -0
  35. data/lib/fontisan/audit/extractors/color_capabilities.rb +141 -0
  36. data/lib/fontisan/audit/extractors/coverage.rb +48 -0
  37. data/lib/fontisan/audit/extractors/hinting.rb +197 -0
  38. data/lib/fontisan/audit/extractors/identity.rb +52 -0
  39. data/lib/fontisan/audit/extractors/language_coverage.rb +37 -0
  40. data/lib/fontisan/audit/extractors/licensing.rb +79 -0
  41. data/lib/fontisan/audit/extractors/metrics.rb +103 -0
  42. data/lib/fontisan/audit/extractors/opentype_layout.rb +69 -0
  43. data/lib/fontisan/audit/extractors/provenance.rb +29 -0
  44. data/lib/fontisan/audit/extractors/style.rb +32 -0
  45. data/lib/fontisan/audit/extractors/variation_detail.rb +99 -0
  46. data/lib/fontisan/audit/extractors.rb +27 -0
  47. data/lib/fontisan/audit/library_aggregator.rb +83 -0
  48. data/lib/fontisan/audit/library_auditor.rb +90 -0
  49. data/lib/fontisan/audit/registry.rb +60 -0
  50. data/lib/fontisan/audit/style_extractor.rb +80 -0
  51. data/lib/fontisan/audit.rb +20 -0
  52. data/lib/fontisan/base_collection.rb +23 -9
  53. data/lib/fontisan/binary/structures.rb +0 -2
  54. data/lib/fontisan/binary.rb +11 -0
  55. data/lib/fontisan/cldr/aggregator.rb +33 -0
  56. data/lib/fontisan/cldr/cache_manager.rb +110 -0
  57. data/lib/fontisan/cldr/config.rb +59 -0
  58. data/lib/fontisan/cldr/download_error.rb +9 -0
  59. data/lib/fontisan/cldr/downloader.rb +79 -0
  60. data/lib/fontisan/cldr/error.rb +8 -0
  61. data/lib/fontisan/cldr/index.rb +64 -0
  62. data/lib/fontisan/cldr/index_builder.rb +72 -0
  63. data/lib/fontisan/cldr/unicode_set_parser.rb +172 -0
  64. data/lib/fontisan/cldr/unknown_version_error.rb +9 -0
  65. data/lib/fontisan/cldr/version_resolver.rb +91 -0
  66. data/lib/fontisan/cldr.rb +23 -0
  67. data/lib/fontisan/cli/cldr_cli.rb +85 -0
  68. data/lib/fontisan/cli/ucd_cli.rb +97 -0
  69. data/lib/fontisan/cli.rb +201 -2
  70. data/lib/fontisan/collection/builder.rb +0 -4
  71. data/lib/fontisan/collection/dfont_builder.rb +0 -4
  72. data/lib/fontisan/collection/shared_logic.rb +0 -2
  73. data/lib/fontisan/collection/writer.rb +0 -3
  74. data/lib/fontisan/collection.rb +15 -0
  75. data/lib/fontisan/commands/audit_command.rb +123 -0
  76. data/lib/fontisan/commands/audit_compare_command.rb +66 -0
  77. data/lib/fontisan/commands/audit_library_command.rb +46 -0
  78. data/lib/fontisan/commands/base_command.rb +0 -3
  79. data/lib/fontisan/commands/convert_command.rb +25 -20
  80. data/lib/fontisan/commands/dump_table_command.rb +0 -3
  81. data/lib/fontisan/commands/export_command.rb +0 -4
  82. data/lib/fontisan/commands/features_command.rb +0 -3
  83. data/lib/fontisan/commands/instance_command.rb +0 -5
  84. data/lib/fontisan/commands/ls_command.rb +0 -6
  85. data/lib/fontisan/commands/optical_size_command.rb +0 -3
  86. data/lib/fontisan/commands/pack_command.rb +0 -5
  87. data/lib/fontisan/commands/scripts_command.rb +0 -2
  88. data/lib/fontisan/commands/subset_command.rb +0 -3
  89. data/lib/fontisan/commands/unicode_command.rb +0 -3
  90. data/lib/fontisan/commands/unpack_command.rb +0 -7
  91. data/lib/fontisan/commands/validate_command.rb +0 -8
  92. data/lib/fontisan/commands/variable_command.rb +0 -3
  93. data/lib/fontisan/commands.rb +29 -0
  94. data/lib/fontisan/config/cldr.yml +22 -0
  95. data/lib/fontisan/config/conversion_matrix.yml +38 -0
  96. data/lib/fontisan/config/ucd.yml +23 -0
  97. data/lib/fontisan/constants.rb +19 -0
  98. data/lib/fontisan/conversion_options.rb +30 -19
  99. data/lib/fontisan/converters/cff_table_builder.rb +0 -3
  100. data/lib/fontisan/converters/collection_converter.rb +0 -8
  101. data/lib/fontisan/converters/conversion_strategy.rb +161 -46
  102. data/lib/fontisan/converters/format_converter.rb +143 -32
  103. data/lib/fontisan/converters/glyf_table_builder.rb +0 -2
  104. data/lib/fontisan/converters/outline_converter.rb +0 -19
  105. data/lib/fontisan/converters/outline_extraction.rb +0 -5
  106. data/lib/fontisan/converters/outline_optimizer.rb +0 -5
  107. data/lib/fontisan/converters/svg_generator.rb +0 -4
  108. data/lib/fontisan/converters/table_copier.rb +0 -2
  109. data/lib/fontisan/converters/type1_converter.rb +0 -11
  110. data/lib/fontisan/converters/woff2_encoder.rb +49 -20
  111. data/lib/fontisan/converters/woff_writer.rb +211 -282
  112. data/lib/fontisan/converters.rb +21 -0
  113. data/lib/fontisan/dfont_collection.rb +29 -10
  114. data/lib/fontisan/export/exporter.rb +0 -6
  115. data/lib/fontisan/export/transformers/font_to_ttx.rb +0 -9
  116. data/lib/fontisan/export/transformers/head_transformer.rb +0 -2
  117. data/lib/fontisan/export/transformers/hhea_transformer.rb +0 -2
  118. data/lib/fontisan/export/transformers/maxp_transformer.rb +0 -2
  119. data/lib/fontisan/export/transformers/name_transformer.rb +0 -2
  120. data/lib/fontisan/export/transformers/os2_transformer.rb +0 -2
  121. data/lib/fontisan/export/transformers/post_transformer.rb +0 -2
  122. data/lib/fontisan/export/transformers.rb +17 -0
  123. data/lib/fontisan/export.rb +13 -0
  124. data/lib/fontisan/font_loader.rb +14 -19
  125. data/lib/fontisan/font_writer.rb +0 -1
  126. data/lib/fontisan/formatters/audit_diff_text_renderer.rb +122 -0
  127. data/lib/fontisan/formatters/audit_text_renderer.rb +324 -0
  128. data/lib/fontisan/formatters/library_summary_text_renderer.rb +99 -0
  129. data/lib/fontisan/formatters/text_formatter.rb +6 -0
  130. data/lib/fontisan/formatters.rb +12 -0
  131. data/lib/fontisan/hints/hint_converter.rb +0 -1
  132. data/lib/fontisan/hints/postscript_hint_applier.rb +0 -9
  133. data/lib/fontisan/hints/postscript_hint_extractor.rb +0 -2
  134. data/lib/fontisan/hints/truetype_hint_extractor.rb +0 -2
  135. data/lib/fontisan/hints.rb +16 -0
  136. data/lib/fontisan/metrics_calculator.rb +0 -2
  137. data/lib/fontisan/models/all_scripts_features_info.rb +0 -1
  138. data/lib/fontisan/models/audit/audit_axis.rb +30 -0
  139. data/lib/fontisan/models/audit/audit_block.rb +32 -0
  140. data/lib/fontisan/models/audit/audit_diff.rb +77 -0
  141. data/lib/fontisan/models/audit/audit_report.rb +153 -0
  142. data/lib/fontisan/models/audit/codepoint_range.rb +40 -0
  143. data/lib/fontisan/models/audit/codepoint_set_diff.rb +34 -0
  144. data/lib/fontisan/models/audit/color_capabilities.rb +93 -0
  145. data/lib/fontisan/models/audit/duplicate_group.rb +23 -0
  146. data/lib/fontisan/models/audit/embedding_type.rb +76 -0
  147. data/lib/fontisan/models/audit/field_change.rb +28 -0
  148. data/lib/fontisan/models/audit/fs_selection_flags.rb +61 -0
  149. data/lib/fontisan/models/audit/gasp_range.rb +63 -0
  150. data/lib/fontisan/models/audit/hinting.rb +93 -0
  151. data/lib/fontisan/models/audit/library_summary.rb +40 -0
  152. data/lib/fontisan/models/audit/licensing.rb +48 -0
  153. data/lib/fontisan/models/audit/metrics.rb +111 -0
  154. data/lib/fontisan/models/audit/named_instance.rb +41 -0
  155. data/lib/fontisan/models/audit/opentype_layout.rb +40 -0
  156. data/lib/fontisan/models/audit/script_coverage_row.rb +26 -0
  157. data/lib/fontisan/models/audit/script_features.rb +28 -0
  158. data/lib/fontisan/models/audit/variation_detail.rb +44 -0
  159. data/lib/fontisan/models/audit.rb +33 -0
  160. data/lib/fontisan/models/cldr/language_coverage.rb +31 -0
  161. data/lib/fontisan/models/cldr.rb +12 -0
  162. data/lib/fontisan/models/collection_brief_info.rb +0 -1
  163. data/lib/fontisan/models/collection_info.rb +0 -2
  164. data/lib/fontisan/models/collection_list_info.rb +0 -1
  165. data/lib/fontisan/models/collection_validation_report.rb +0 -2
  166. data/lib/fontisan/models/color_glyph.rb +0 -1
  167. data/lib/fontisan/models/font_report.rb +0 -1
  168. data/lib/fontisan/models/ttx/tables.rb +21 -0
  169. data/lib/fontisan/models/ttx/ttfont.rb +0 -8
  170. data/lib/fontisan/models/ttx.rb +14 -0
  171. data/lib/fontisan/models/ucd/ucd.rb +38 -0
  172. data/lib/fontisan/models/ucd/ucd_char.rb +67 -0
  173. data/lib/fontisan/models/ucd.rb +19 -0
  174. data/lib/fontisan/models.rb +47 -0
  175. data/lib/fontisan/open_type_collection.rb +6 -5
  176. data/lib/fontisan/open_type_font.rb +8 -2
  177. data/lib/fontisan/open_type_font_extensions.rb +9 -9
  178. data/lib/fontisan/optimizers/pattern_analyzer.rb +0 -1
  179. data/lib/fontisan/optimizers.rb +14 -0
  180. data/lib/fontisan/outline_extractor.rb +0 -2
  181. data/lib/fontisan/parsers/dfont_parser.rb +0 -1
  182. data/lib/fontisan/parsers.rb +10 -0
  183. data/lib/fontisan/pipeline/format_detector.rb +29 -102
  184. data/lib/fontisan/pipeline/output_writer.rb +11 -9
  185. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +0 -4
  186. data/lib/fontisan/pipeline/strategies/named_strategy.rb +0 -4
  187. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +0 -2
  188. data/lib/fontisan/pipeline/strategies.rb +14 -0
  189. data/lib/fontisan/pipeline/transformation_pipeline.rb +0 -7
  190. data/lib/fontisan/pipeline/variation_resolver.rb +0 -7
  191. data/lib/fontisan/pipeline.rb +13 -0
  192. data/lib/fontisan/sfnt_font.rb +29 -14
  193. data/lib/fontisan/sfnt_table.rb +0 -4
  194. data/lib/fontisan/subset/builder.rb +0 -6
  195. data/lib/fontisan/subset.rb +13 -0
  196. data/lib/fontisan/svg/font_generator.rb +0 -4
  197. data/lib/fontisan/svg/glyph_generator.rb +0 -2
  198. data/lib/fontisan/svg.rb +12 -0
  199. data/lib/fontisan/tables/cbdt.rb +0 -1
  200. data/lib/fontisan/tables/cblc.rb +0 -1
  201. data/lib/fontisan/tables/cff/charset.rb +0 -1
  202. data/lib/fontisan/tables/cff/charstring.rb +0 -1
  203. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +0 -4
  204. data/lib/fontisan/tables/cff/charstrings_index.rb +0 -3
  205. data/lib/fontisan/tables/cff/dict.rb +0 -1
  206. data/lib/fontisan/tables/cff/encoding.rb +0 -1
  207. data/lib/fontisan/tables/cff/header.rb +0 -2
  208. data/lib/fontisan/tables/cff/hint_operation_injector.rb +0 -2
  209. data/lib/fontisan/tables/cff/index.rb +0 -1
  210. data/lib/fontisan/tables/cff/private_dict.rb +0 -2
  211. data/lib/fontisan/tables/cff/private_dict_writer.rb +0 -2
  212. data/lib/fontisan/tables/cff/table_builder.rb +0 -6
  213. data/lib/fontisan/tables/cff/top_dict.rb +0 -2
  214. data/lib/fontisan/tables/cff.rb +22 -15
  215. data/lib/fontisan/tables/cff2/charstring_parser.rb +0 -2
  216. data/lib/fontisan/tables/cff2/table_builder.rb +0 -11
  217. data/lib/fontisan/tables/cff2/table_reader.rb +0 -2
  218. data/lib/fontisan/tables/cff2.rb +13 -14
  219. data/lib/fontisan/tables/cmap.rb +24 -2
  220. data/lib/fontisan/tables/cmap_table.rb +0 -3
  221. data/lib/fontisan/tables/colr.rb +0 -1
  222. data/lib/fontisan/tables/cpal.rb +0 -1
  223. data/lib/fontisan/tables/cvar.rb +0 -2
  224. data/lib/fontisan/tables/fvar.rb +0 -1
  225. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +0 -2
  226. data/lib/fontisan/tables/glyf/glyph_builder.rb +0 -3
  227. data/lib/fontisan/tables/glyf.rb +0 -6
  228. data/lib/fontisan/tables/glyf_table.rb +0 -3
  229. data/lib/fontisan/tables/gpos.rb +0 -2
  230. data/lib/fontisan/tables/gsub.rb +0 -2
  231. data/lib/fontisan/tables/gvar.rb +0 -2
  232. data/lib/fontisan/tables/head.rb +0 -2
  233. data/lib/fontisan/tables/head_table.rb +0 -3
  234. data/lib/fontisan/tables/hhea.rb +0 -2
  235. data/lib/fontisan/tables/hhea_table.rb +0 -3
  236. data/lib/fontisan/tables/hmtx.rb +0 -2
  237. data/lib/fontisan/tables/hmtx_table.rb +0 -3
  238. data/lib/fontisan/tables/hvar.rb +0 -3
  239. data/lib/fontisan/tables/loca.rb +0 -2
  240. data/lib/fontisan/tables/loca_table.rb +0 -3
  241. data/lib/fontisan/tables/maxp.rb +0 -2
  242. data/lib/fontisan/tables/maxp_table.rb +0 -3
  243. data/lib/fontisan/tables/mvar.rb +0 -3
  244. data/lib/fontisan/tables/name.rb +0 -2
  245. data/lib/fontisan/tables/name_table.rb +0 -3
  246. data/lib/fontisan/tables/os2_table.rb +0 -3
  247. data/lib/fontisan/tables/post_table.rb +0 -3
  248. data/lib/fontisan/tables/sbix.rb +0 -1
  249. data/lib/fontisan/tables/svg.rb +0 -1
  250. data/lib/fontisan/tables/variation_common.rb +0 -1
  251. data/lib/fontisan/tables/vvar.rb +0 -3
  252. data/lib/fontisan/tables.rb +54 -0
  253. data/lib/fontisan/true_type_collection.rb +6 -14
  254. data/lib/fontisan/true_type_font.rb +8 -2
  255. data/lib/fontisan/true_type_font_extensions.rb +9 -9
  256. data/lib/fontisan/type1/afm_generator.rb +0 -4
  257. data/lib/fontisan/type1/conversion_options.rb +0 -2
  258. data/lib/fontisan/type1/encodings.rb +0 -2
  259. data/lib/fontisan/type1/generator.rb +0 -8
  260. data/lib/fontisan/type1/pfa_generator.rb +0 -3
  261. data/lib/fontisan/type1/pfb_generator.rb +0 -5
  262. data/lib/fontisan/type1/pfm_generator.rb +0 -4
  263. data/lib/fontisan/type1.rb +42 -69
  264. data/lib/fontisan/type1_font.rb +40 -11
  265. data/lib/fontisan/ucd/aggregator.rb +73 -0
  266. data/lib/fontisan/ucd/cache_manager.rb +111 -0
  267. data/lib/fontisan/ucd/config.rb +59 -0
  268. data/lib/fontisan/ucd/download_error.rb +9 -0
  269. data/lib/fontisan/ucd/downloader.rb +88 -0
  270. data/lib/fontisan/ucd/error.rb +8 -0
  271. data/lib/fontisan/ucd/index.rb +103 -0
  272. data/lib/fontisan/ucd/index_builder.rb +107 -0
  273. data/lib/fontisan/ucd/range_entry.rb +56 -0
  274. data/lib/fontisan/ucd/unknown_version_error.rb +9 -0
  275. data/lib/fontisan/ucd/version_resolver.rb +79 -0
  276. data/lib/fontisan/ucd.rb +23 -0
  277. data/lib/fontisan/utilities/checksum_calculator.rb +0 -1
  278. data/lib/fontisan/utilities.rb +10 -0
  279. data/lib/fontisan/utils.rb +10 -0
  280. data/lib/fontisan/validation/collection_validator.rb +0 -2
  281. data/lib/fontisan/validation.rb +9 -0
  282. data/lib/fontisan/validators/basic_validator.rb +0 -2
  283. data/lib/fontisan/validators/font_book_validator.rb +0 -2
  284. data/lib/fontisan/validators/opentype_validator.rb +0 -2
  285. data/lib/fontisan/validators/profile_loader.rb +0 -5
  286. data/lib/fontisan/validators/validator.rb +0 -2
  287. data/lib/fontisan/validators/web_font_validator.rb +0 -2
  288. data/lib/fontisan/validators.rb +14 -0
  289. data/lib/fontisan/variable/delta_applicator.rb +0 -4
  290. data/lib/fontisan/variable/instancer.rb +0 -3
  291. data/lib/fontisan/variable/static_font_builder.rb +0 -3
  292. data/lib/fontisan/variable.rb +16 -0
  293. data/lib/fontisan/variation/blend_applier.rb +0 -2
  294. data/lib/fontisan/variation/cache.rb +0 -2
  295. data/lib/fontisan/variation/converter.rb +0 -3
  296. data/lib/fontisan/variation/data_extractor.rb +0 -2
  297. data/lib/fontisan/variation/delta_applier.rb +0 -5
  298. data/lib/fontisan/variation/inspector.rb +0 -1
  299. data/lib/fontisan/variation/instance_generator.rb +0 -6
  300. data/lib/fontisan/variation/instance_writer.rb +0 -5
  301. data/lib/fontisan/variation/metrics_adjuster.rb +0 -4
  302. data/lib/fontisan/variation/optimizer.rb +0 -3
  303. data/lib/fontisan/variation/parallel_generator.rb +0 -3
  304. data/lib/fontisan/variation/subsetter.rb +0 -4
  305. data/lib/fontisan/variation/tuple_variation_header.rb +0 -2
  306. data/lib/fontisan/variation/variable_svg_generator.rb +0 -3
  307. data/lib/fontisan/variation/variation_context.rb +0 -3
  308. data/lib/fontisan/variation/variation_preserver.rb +0 -3
  309. data/lib/fontisan/variation.rb +31 -0
  310. data/lib/fontisan/version.rb +1 -1
  311. data/lib/fontisan/woff2.rb +13 -0
  312. data/lib/fontisan/woff2_font.rb +31 -9
  313. data/lib/fontisan/woff_font.rb +31 -2
  314. data/lib/fontisan.rb +124 -196
  315. metadata +114 -7
  316. data/fontisan.gemspec +0 -48
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "parsers/dfont_parser"
4
- require_relative "error"
5
- require_relative "collection/shared_logic"
6
-
7
3
  module Fontisan
8
4
  # DfontCollection represents an Apple dfont suitcase containing multiple fonts
9
5
  #
@@ -26,6 +22,35 @@ module Fontisan
26
22
  class DfontCollection
27
23
  include Collection::SharedLogic
28
24
 
25
+ # High-level pipeline format identifier. Owned by the collection class
26
+ # so the conversion pipeline can dispatch without case statements (OCP).
27
+ #
28
+ # @return [Symbol] :dfont
29
+ def format = :dfont
30
+
31
+ # Whether this object represents a font collection rather than a single
32
+ # font. Each font class is the authority on this question.
33
+ #
34
+ # @return [Boolean]
35
+ def collection? = true
36
+
37
+ # Variation profile. dfonts are containers; per-font variation is not
38
+ # exposed at the collection level.
39
+ #
40
+ # @return [Symbol] :static
41
+ def variation_type = :static
42
+
43
+ # Outline representation. dfonts may contain mixed-flavor fonts;
44
+ # per-font outline type requires loading an individual font.
45
+ #
46
+ # @return [Symbol] :unknown
47
+ def outline_type = :unknown
48
+
49
+ # Collections have no single SFNT table directory.
50
+ #
51
+ # @return [Array<String>] empty
52
+ def table_names = []
53
+
29
54
  # Path to dfont file
30
55
  # @return [String]
31
56
  attr_reader :path
@@ -138,9 +163,6 @@ module Fontisan
138
163
  # @param io [IO] Open file handle
139
164
  # @return [Models::CollectionListInfo] Collection list info
140
165
  def list_fonts(io)
141
- require_relative "models/collection_list_info"
142
- require_relative "models/collection_font_summary"
143
-
144
166
  fonts = extract_fonts(io)
145
167
 
146
168
  summaries = fonts.map.with_index do |font, index|
@@ -227,9 +249,6 @@ module Fontisan
227
249
  # puts "Format: #{info.collection_format}"
228
250
  # end
229
251
  def collection_info(io, path)
230
- require_relative "models/collection_info"
231
- require_relative "models/table_sharing_info"
232
-
233
252
  # Calculate table sharing statistics
234
253
  table_sharing = calculate_table_sharing(io)
235
254
 
@@ -1,11 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../models/font_export"
4
- require_relative "table_serializer"
5
- require_relative "ttx_generator"
6
- require_relative "transformers/font_to_ttx"
7
- require_relative "../utilities/checksum_calculator"
8
-
9
3
  module Fontisan
10
4
  module Export
11
5
  # Exporter orchestrates font export to YAML/JSON/TTX
@@ -1,14 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../models/ttx/ttfont"
4
- require_relative "../../models/ttx/glyph_order"
5
- require_relative "head_transformer"
6
- require_relative "name_transformer"
7
- require_relative "os2_transformer"
8
- require_relative "post_transformer"
9
- require_relative "hhea_transformer"
10
- require_relative "maxp_transformer"
11
-
12
3
  module Fontisan
13
4
  module Export
14
5
  module Transformers
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../models/ttx/tables/head_table"
4
-
5
3
  module Fontisan
6
4
  module Export
7
5
  module Transformers
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../models/ttx/tables/hhea_table"
4
-
5
3
  module Fontisan
6
4
  module Export
7
5
  module Transformers
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../models/ttx/tables/maxp_table"
4
-
5
3
  module Fontisan
6
4
  module Export
7
5
  module Transformers
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../models/ttx/tables/name_table"
4
-
5
3
  module Fontisan
6
4
  module Export
7
5
  module Transformers
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../models/ttx/tables/os2_table"
4
-
5
3
  module Fontisan
6
4
  module Export
7
5
  module Transformers
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../models/ttx/tables/post_table"
4
-
5
3
  module Fontisan
6
4
  module Export
7
5
  module Transformers
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Export::Transformers namespace.
4
+
5
+ module Fontisan
6
+ module Export
7
+ module Transformers
8
+ autoload :FontToTtx, "fontisan/export/transformers/font_to_ttx"
9
+ autoload :HeadTransformer, "fontisan/export/transformers/head_transformer"
10
+ autoload :HheaTransformer, "fontisan/export/transformers/hhea_transformer"
11
+ autoload :MaxpTransformer, "fontisan/export/transformers/maxp_transformer"
12
+ autoload :NameTransformer, "fontisan/export/transformers/name_transformer"
13
+ autoload :Os2Transformer, "fontisan/export/transformers/os2_transformer"
14
+ autoload :PostTransformer, "fontisan/export/transformers/post_transformer"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Autoload hub for the Fontisan::Export namespace.
4
+
5
+ module Fontisan
6
+ module Export
7
+ autoload :Exporter, "fontisan/export/exporter"
8
+ autoload :TableSerializer, "fontisan/export/table_serializer"
9
+ autoload :Transformers, "fontisan/export/transformers/font_to_ttx"
10
+ autoload :TtxGenerator, "fontisan/export/ttx_generator"
11
+ autoload :TtxParser, "fontisan/export/ttx_parser"
12
+ end
13
+ end
@@ -1,18 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "stringio"
4
- require_relative "constants"
5
- require_relative "loading_modes"
6
- require_relative "true_type_font"
7
- require_relative "open_type_font"
8
- require_relative "true_type_collection"
9
- require_relative "open_type_collection"
10
- require_relative "dfont_collection"
11
- require_relative "woff_font"
12
- require_relative "woff2_font"
13
- require_relative "type1_font"
14
- require_relative "parsers/dfont_parser"
15
- require_relative "error"
16
4
 
17
5
  module Fontisan
18
6
  # FontLoader provides unified font loading with content-based format detection.
@@ -85,12 +73,18 @@ module Fontisan
85
73
 
86
74
  format = detect(path)
87
75
  case format
88
- when :ttf then TrueTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
89
- when :otf then OpenTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
90
- when :woff then WoffFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
91
- when :woff2 then Woff2Font.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
92
- when :ttc, :otc then load_from_collection(path, format, font_index, mode: resolved_mode)
93
- when :dfont then load_dfont(path, font_index: font_index, mode: resolved_mode)
76
+ when :ttf then TrueTypeFont.from_file(path, mode: resolved_mode,
77
+ lazy: resolved_lazy)
78
+ when :otf then OpenTypeFont.from_file(path, mode: resolved_mode,
79
+ lazy: resolved_lazy)
80
+ when :woff then WoffFont.from_file(path, mode: resolved_mode,
81
+ lazy: resolved_lazy)
82
+ when :woff2 then Woff2Font.from_file(path, mode: resolved_mode,
83
+ lazy: resolved_lazy)
84
+ when :ttc, :otc then load_from_collection(path, format, font_index,
85
+ mode: resolved_mode)
86
+ when :dfont then load_dfont(path, font_index: font_index,
87
+ mode: resolved_mode)
94
88
  when :pfa, :pfb then Type1Font.from_file(path, mode: resolved_mode)
95
89
  else
96
90
  raise InvalidFontError,
@@ -279,7 +273,8 @@ module Fontisan
279
273
  # public `lazy:` flag is not threaded through this path.
280
274
  def self.load_dfont(path, font_index:, mode:)
281
275
  File.open(path, "rb") do |io|
282
- sfnt_io = StringIO.new(Parsers::DfontParser.extract_sfnt(io, index: font_index))
276
+ sfnt_io = StringIO.new(Parsers::DfontParser.extract_sfnt(io,
277
+ index: font_index))
283
278
  klass = case Constants.sfnt_format_for(sfnt_io.read(4))
284
279
  when :ttf then TrueTypeFont
285
280
  when :otf then OpenTypeFont
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
- require_relative "constants"
5
4
 
6
5
  module Fontisan
7
6
  # FontWriter handles writing font binaries from table data
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Formatters
5
+ # Human-readable diff of two {Models::Audit::AuditReport}s.
6
+ #
7
+ # Output groups changes by kind: scalar field changes, codepoint set
8
+ # deltas (added/removed counts and a preview of the ranges), then
9
+ # structural inventory changes (scripts, features, blocks, languages).
10
+ # Empty sections are omitted so a no-op diff prints only the header.
11
+ class AuditDiffTextRenderer
12
+ SEPARATOR = "=" * 80
13
+ LIST_LIMIT = 10
14
+
15
+ # @param diff [Models::Audit::AuditDiff]
16
+ def initialize(diff)
17
+ @diff = diff
18
+ @lines = []
19
+ end
20
+
21
+ # @return [String]
22
+ def render
23
+ render_header
24
+ render_field_changes
25
+ render_codepoint_delta
26
+ render_structural_changes
27
+ render_empty_note
28
+ @lines.join("\n")
29
+ end
30
+
31
+ private
32
+
33
+ def render_header
34
+ @lines << "AUDIT DIFF"
35
+ @lines << SEPARATOR
36
+ @lines << "left: #{@diff.left_source}"
37
+ @lines << "right: #{@diff.right_source}"
38
+ end
39
+
40
+ def render_field_changes
41
+ changes = Array(@diff.field_changes)
42
+ return if changes.empty?
43
+
44
+ section("FIELD CHANGES (#{changes.size})")
45
+ changes.each do |change|
46
+ @lines << " #{change.field}: #{change.left.inspect} → #{change.right.inspect}"
47
+ end
48
+ end
49
+
50
+ def render_codepoint_delta
51
+ delta = @diff.codepoints
52
+ return unless delta && (delta.added_count.to_i.positive? || delta.removed_count.to_i.positive?)
53
+
54
+ section("CODEPOINT COVERAGE")
55
+ @lines << " added: #{delta.added_count}"
56
+ @lines << " removed: #{delta.removed_count}"
57
+ @lines << " unchanged: #{delta.unchanged_count}"
58
+ preview_added(delta)
59
+ preview_removed(delta)
60
+ end
61
+
62
+ def preview_added(delta)
63
+ ranges = Array(delta.added)
64
+ return if ranges.empty?
65
+
66
+ @lines << " + #{format_ranges(ranges)}"
67
+ end
68
+
69
+ def preview_removed(delta)
70
+ ranges = Array(delta.removed)
71
+ return if ranges.empty?
72
+
73
+ @lines << " - #{format_ranges(ranges)}"
74
+ end
75
+
76
+ def render_structural_changes
77
+ render_set("SCRIPTS", @diff.added_scripts, @diff.removed_scripts)
78
+ render_set("FEATURES", @diff.added_features, @diff.removed_features)
79
+ render_set("BLOCKS", @diff.added_blocks, @diff.removed_blocks)
80
+ render_set("LANGUAGES", @diff.added_languages, @diff.removed_languages)
81
+ end
82
+
83
+ def render_set(name, added, removed)
84
+ added = Array(added)
85
+ removed = Array(removed)
86
+ return if added.empty? && removed.empty?
87
+
88
+ section("#{name} CHANGES")
89
+ @lines << " + #{truncate(added)}" unless added.empty?
90
+ @lines << " - #{truncate(removed)}" unless removed.empty?
91
+ end
92
+
93
+ def render_empty_note
94
+ return unless @diff.empty?
95
+
96
+ @lines << ""
97
+ @lines << "(no differences)"
98
+ end
99
+
100
+ # ---- helpers --------------------------------------------------------
101
+
102
+ def section(title)
103
+ @lines << ""
104
+ @lines << title
105
+ end
106
+
107
+ def truncate(list)
108
+ shown = list.first(LIST_LIMIT).join(", ")
109
+ shown += ", ..." if list.size > LIST_LIMIT
110
+ shown
111
+ end
112
+
113
+ def format_ranges(ranges)
114
+ shown = ranges.first(LIST_LIMIT).map do |r|
115
+ "U+#{format('%04X', r.first_cp)}-U+#{format('%04X', r.last_cp)}"
116
+ end.join(", ")
117
+ shown += ", ..." if ranges.size > LIST_LIMIT
118
+ shown
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Formatters
5
+ # Human-readable, sectioned view of an {Models::Audit::AuditReport}.
6
+ #
7
+ # The text formatter is the default `--format text` output for
8
+ # `fontisan audit`. Complements YAML/JSON (machine-facing) with a
9
+ # terse, scannable terminal view. Every section is nil-safe so the
10
+ # same renderer covers full OpenType/TrueType faces, Type 1 fonts
11
+ # (no OS/2, no metrics, no layout), and partial reports.
12
+ class AuditTextRenderer
13
+ SEPARATOR = "=" * 80
14
+ LABEL_WIDTH = 18
15
+ LIST_LIMIT = 10
16
+
17
+ WIDTH_NAMES = {
18
+ 1 => "Ultra-condensed", 2 => "Extra-condensed", 3 => "Condensed",
19
+ 4 => "Semi-condensed", 5 => "Medium", 6 => "Semi-expanded",
20
+ 7 => "Expanded", 8 => "Extra-expanded", 9 => "Ultra-expanded"
21
+ }.freeze
22
+
23
+ # @param report [Models::Audit::AuditReport]
24
+ def initialize(report)
25
+ @report = report
26
+ @lines = []
27
+ end
28
+
29
+ # @return [String]
30
+ def render
31
+ render_header
32
+ render_identity
33
+ render_style
34
+ render_metrics
35
+ render_coverage
36
+ render_blocks
37
+ render_licensing
38
+ render_hinting
39
+ render_color
40
+ render_variation
41
+ render_opentype_layout
42
+ render_language_coverage
43
+ render_warnings
44
+ @lines.join("\n")
45
+ end
46
+
47
+ private
48
+
49
+ def render_header
50
+ @lines << (@report.postscript_name || @report.family_name || "(unknown)")
51
+ @lines << SEPARATOR
52
+ @lines << two_col("generated_at:", @report.generated_at,
53
+ "fontisan:", @report.fontisan_version)
54
+ @lines << "source_sha256: #{@report.source_sha256}"
55
+ @lines << "source_file: #{@report.source_file}"
56
+ @lines << two_col("source_format:", @report.source_format,
57
+ "layout:", layout_descriptor)
58
+ end
59
+
60
+ def layout_descriptor
61
+ if @report.num_fonts_in_source.nil? || @report.num_fonts_in_source <= 1
62
+ "single face (1/1)"
63
+ else
64
+ format("collection face (%<idx>d/%<total>d)",
65
+ idx: (@report.font_index || 0) + 1,
66
+ total: @report.num_fonts_in_source)
67
+ end
68
+ end
69
+
70
+ def render_identity
71
+ section("IDENTITY")
72
+ row("Family", @report.family_name)
73
+ row("Subfamily", @report.subfamily_name)
74
+ row("Full name", @report.full_name)
75
+ row("PostScript", @report.postscript_name)
76
+ row("Version", @report.version)
77
+ row("Revision", @report.font_revision)
78
+ end
79
+
80
+ def render_style
81
+ section("STYLE")
82
+ row("Weight class", weight_descriptor)
83
+ row("Width class", width_descriptor)
84
+ row("Bold", yes_no(@report.bold))
85
+ row("Italic", yes_no(@report.italic))
86
+ row("PANOSE", @report.panose)
87
+ end
88
+
89
+ def render_metrics
90
+ return unless @report.metrics
91
+
92
+ m = @report.metrics
93
+ section("METRICS")
94
+ row("unitsPerEm", m.units_per_em)
95
+ row("hhea", "ascent: #{m.hhea_ascent} / descent: #{m.hhea_descent} / line gap: #{m.hhea_line_gap}") if m.hhea_ascent
96
+ row("OS/2 typo", "ascent: #{m.typo_ascender} / descent: #{m.typo_descender} / line gap: #{m.typo_line_gap}") if m.typo_ascender
97
+ row("OS/2 win", "ascent: #{m.win_ascent} / descent: #{m.win_descent}") if m.win_ascent
98
+ row("x-height", m.x_height)
99
+ row("cap height", m.cap_height)
100
+ row("bbox", bbox_descriptor(m)) if m.bbox_x_min || m.bbox_x_max
101
+ end
102
+
103
+ def render_coverage
104
+ section("COVERAGE")
105
+ row("Codepoints", @report.total_codepoints)
106
+ row("Glyphs", @report.total_glyphs)
107
+ row("cmap subtables", format("%s", Array(@report.cmap_subtables).join(", "))) unless Array(@report.cmap_subtables).empty?
108
+ row("Ranges (top #{LIST_LIMIT})", codepoint_range_preview)
109
+ row("Unicode scripts", truncate_list(@report.unicode_scripts))
110
+ end
111
+
112
+ def render_blocks
113
+ blocks = Array(@report.blocks)
114
+ return if blocks.empty?
115
+
116
+ section("UNICODE BLOCKS (top #{LIST_LIMIT} by fill ratio)")
117
+ blocks.sort_by { |b| -(b.fill_ratio || 0) }.first(LIST_LIMIT).each do |block|
118
+ ratio = block.fill_ratio ? format("%<r>d%%", r: (block.fill_ratio * 100).round) : "?"
119
+ @lines << format(" %<name>-40s %<covered>d/%<total>d (%<ratio>s)",
120
+ name: "#{block.name}:", covered: block.covered || 0,
121
+ total: block.total || 0, ratio: ratio)
122
+ end
123
+ end
124
+
125
+ def render_licensing
126
+ return unless @report.licensing
127
+
128
+ l = @report.licensing
129
+ section("LICENSING")
130
+ row("Copyright", l.copyright)
131
+ row("Trademark", l.trademark)
132
+ row("Manufacturer", l.manufacturer)
133
+ row("Designer", l.designer)
134
+ row("License", l.license_description)
135
+ row("License URL", l.license_url)
136
+ row("Vendor URL", l.vendor_url)
137
+ row("Designer URL", l.designer_url)
138
+ row("Vendor ID", l.vendor_id)
139
+ row("Embedding", l.embedding_type)
140
+ end
141
+
142
+ def render_hinting
143
+ return unless @report.hinting
144
+
145
+ h = @report.hinting
146
+ section("HINTING")
147
+ row("Format", h.hinting_format || (h.is_unhinted ? "unhinted" : "unknown"))
148
+ row("fpgm", instruction_line(h.has_fpgm, h.fpgm_instruction_count))
149
+ row("prep", instruction_line(h.has_prep, h.prep_instruction_count))
150
+ row("cvt", cvt_line(h))
151
+ row("gasp", gasp_line(h))
152
+ row("CFF hints", h.cff_hint_count)
153
+ end
154
+
155
+ def render_color
156
+ return unless @report.color_capabilities
157
+
158
+ c = @report.color_capabilities
159
+ section("COLOR")
160
+ formats = Array(c.color_formats)
161
+ row("Color formats", formats.empty? ? "(none)" : truncate_list(formats))
162
+ row("COLR", colr_line(c)) if c.has_colr
163
+ row("CPAL", "palettes: #{c.cpal_palette_count}, colors: #{c.cpal_color_count}") if c.has_cpal
164
+ row("SVG documents", c.svg_document_count) if c.has_svg && c.svg_document_count
165
+ row("CBDT strikes", c.cbdt_strike_count) if c.has_cbdt && c.cbdt_strike_count
166
+ row("sbix strikes", c.sbix_strike_count) if c.has_sbix && c.sbix_strike_count
167
+ end
168
+
169
+ def render_variation
170
+ v = @report.variation
171
+ section("VARIABLE FONT")
172
+ if v.nil? || Array(v.axes).empty?
173
+ @lines << " (not variable)"
174
+ return
175
+ end
176
+
177
+ v.axes.each do |axis|
178
+ row(axis.tag, format("%<min>s .. %<max>s default %<default>s",
179
+ min: axis.min_value, max: axis.max_value,
180
+ default: axis.default_value))
181
+ end
182
+ return if Array(v.named_instances).empty?
183
+
184
+ @lines << " Named instances:"
185
+ v.named_instances.each do |inst|
186
+ @lines << " #{inst.postscript_name || inst.subfamily_name}: #{inst.coordinates}"
187
+ end
188
+ end
189
+
190
+ def render_opentype_layout
191
+ return unless @report.opentype_layout
192
+
193
+ l = @report.opentype_layout
194
+ section("OPENTYPE LAYOUT")
195
+ row("GSUB", yes_no(l.has_gsub))
196
+ row("GPOS", yes_no(l.has_gpos))
197
+ row("Scripts (#{Array(l.scripts).size})", truncate_list(l.scripts))
198
+ row("Features (#{Array(l.features).size})", truncate_list(l.features))
199
+ end
200
+
201
+ def render_language_coverage
202
+ langs = Array(@report.language_coverage)
203
+ return if langs.empty?
204
+
205
+ section("LANGUAGE COVERAGE (CLDR #{@report.cldr_version})")
206
+ langs.first(LIST_LIMIT).each do |lang|
207
+ pct = lang.coverage_ratio ? format("%<r>d%%", r: (lang.coverage_ratio * 100).round) : "?"
208
+ mark = lang.fully_supported ? "*" : " "
209
+ @lines << format(" %<mark>s %<lang>-8s %<covered>d/%<total>d (%<pct>s)",
210
+ mark: mark, lang: "#{lang.language}:", covered: lang.covered,
211
+ total: lang.total, pct: pct)
212
+ end
213
+ end
214
+
215
+ def render_warnings
216
+ section("WARNINGS")
217
+ @lines << if @report.warning
218
+ " #{@report.warning}"
219
+ else
220
+ " (none)"
221
+ end
222
+ end
223
+
224
+ # ---- formatting helpers --------------------------------------------
225
+
226
+ def section(title)
227
+ @lines << ""
228
+ @lines << title
229
+ end
230
+
231
+ def row(label, value)
232
+ return if value.nil?
233
+ return if value.is_a?(String) && value.empty?
234
+
235
+ @lines << " #{label}:#{' ' * [LABEL_WIDTH - label.to_s.length - 1, 1].max}#{value}"
236
+ end
237
+
238
+ def two_col(left_label, left_value, right_label, right_value)
239
+ left = "#{left_label} #{left_value}".ljust(40)
240
+ "#{left}#{right_label} #{right_value}"
241
+ end
242
+
243
+ def yes_no(bool)
244
+ bool ? "yes" : "no"
245
+ end
246
+
247
+ def truncate_list(items)
248
+ list = Array(items)
249
+ return "(none)" if list.empty?
250
+
251
+ shown = list.first(LIST_LIMIT).join(", ")
252
+ shown += ", ..." if list.size > LIST_LIMIT
253
+ shown
254
+ end
255
+
256
+ def weight_descriptor
257
+ return nil unless @report.weight_class
258
+
259
+ name = weight_name(@report.weight_class)
260
+ "#{@report.weight_class}#{" (#{name})" if name}"
261
+ end
262
+
263
+ def width_descriptor
264
+ return nil unless @report.width_class
265
+
266
+ name = WIDTH_NAMES[@report.width_class]
267
+ "#{@report.width_class}#{" (#{name})" if name}"
268
+ end
269
+
270
+ def weight_name(value)
271
+ case value
272
+ when 100 then "Thin"
273
+ when 200 then "Extra-light"
274
+ when 300 then "Light"
275
+ when 400 then "Regular"
276
+ when 500 then "Medium"
277
+ when 600 then "Semi-bold"
278
+ when 700 then "Bold"
279
+ when 800 then "Extra-bold"
280
+ when 900 then "Black"
281
+ end
282
+ end
283
+
284
+ def bbox_descriptor(metrics)
285
+ "(#{metrics.bbox_x_min}, #{metrics.bbox_y_min}) → (#{metrics.bbox_x_max}, #{metrics.bbox_y_max})"
286
+ end
287
+
288
+ def codepoint_range_preview
289
+ ranges = Array(@report.codepoint_ranges)
290
+ return "(none)" if ranges.empty?
291
+
292
+ shown = ranges.first(LIST_LIMIT).map do |r|
293
+ "U+#{format('%04X', r.first_cp)}-U+#{format('%04X', r.last_cp)}"
294
+ end.join(", ")
295
+ shown += ", ..." if ranges.size > LIST_LIMIT
296
+ shown
297
+ end
298
+
299
+ def instruction_line(has, count)
300
+ return "no" unless has
301
+
302
+ count ? "#{count} instructions" : "present"
303
+ end
304
+
305
+ def cvt_line(hinting)
306
+ return "no" unless hinting.has_cvt
307
+
308
+ hinting.cvt_entry_count ? "#{hinting.cvt_entry_count} entries" : "present"
309
+ end
310
+
311
+ def gasp_line(hinting)
312
+ ranges = Array(hinting.gasp_ranges)
313
+ return "no" if ranges.empty?
314
+
315
+ ppems = ranges.map(&:max_ppem).compact
316
+ "#{ranges.size} ranges (#{ppems.join('/')} ppem)"
317
+ end
318
+
319
+ def colr_line(color)
320
+ "v#{color.colr_version}, #{color.colr_base_glyph_count} base glyphs, #{color.colr_layer_count} layers"
321
+ end
322
+ end
323
+ end
324
+ end