fontisan 0.2.16 → 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 (318) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +14 -90
  3. data/Gemfile +6 -3
  4. data/README.adoc +257 -1
  5. data/docs/.vitepress/config.ts +68 -8
  6. data/docs/.vitepress/theme/style.css +570 -272
  7. data/docs/CONVERSION_GUIDE.adoc +31 -8
  8. data/docs/EXTRACT_TTC_MIGRATION.md +1 -1
  9. data/docs/WOFF_WOFF2_FORMATS.adoc +53 -0
  10. data/docs/api/conversion-options.md +37 -14
  11. data/docs/api/font-loader.md +21 -15
  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/audit/codepoint_range_coalescer.rb +41 -0
  33. data/lib/fontisan/audit/context.rb +122 -0
  34. data/lib/fontisan/audit/differ.rb +124 -0
  35. data/lib/fontisan/audit/extractors/aggregations.rb +54 -0
  36. data/lib/fontisan/audit/extractors/base.rb +26 -0
  37. data/lib/fontisan/audit/extractors/color_capabilities.rb +141 -0
  38. data/lib/fontisan/audit/extractors/coverage.rb +48 -0
  39. data/lib/fontisan/audit/extractors/hinting.rb +197 -0
  40. data/lib/fontisan/audit/extractors/identity.rb +52 -0
  41. data/lib/fontisan/audit/extractors/language_coverage.rb +37 -0
  42. data/lib/fontisan/audit/extractors/licensing.rb +79 -0
  43. data/lib/fontisan/audit/extractors/metrics.rb +103 -0
  44. data/lib/fontisan/audit/extractors/opentype_layout.rb +69 -0
  45. data/lib/fontisan/audit/extractors/provenance.rb +29 -0
  46. data/lib/fontisan/audit/extractors/style.rb +32 -0
  47. data/lib/fontisan/audit/extractors/variation_detail.rb +99 -0
  48. data/lib/fontisan/audit/extractors.rb +27 -0
  49. data/lib/fontisan/audit/library_aggregator.rb +83 -0
  50. data/lib/fontisan/audit/library_auditor.rb +90 -0
  51. data/lib/fontisan/audit/registry.rb +60 -0
  52. data/lib/fontisan/audit/style_extractor.rb +80 -0
  53. data/lib/fontisan/audit.rb +20 -0
  54. data/lib/fontisan/base_collection.rb +23 -9
  55. data/lib/fontisan/binary/structures.rb +0 -2
  56. data/lib/fontisan/binary.rb +11 -0
  57. data/lib/fontisan/cldr/aggregator.rb +33 -0
  58. data/lib/fontisan/cldr/cache_manager.rb +110 -0
  59. data/lib/fontisan/cldr/config.rb +59 -0
  60. data/lib/fontisan/cldr/download_error.rb +9 -0
  61. data/lib/fontisan/cldr/downloader.rb +79 -0
  62. data/lib/fontisan/cldr/error.rb +8 -0
  63. data/lib/fontisan/cldr/index.rb +64 -0
  64. data/lib/fontisan/cldr/index_builder.rb +72 -0
  65. data/lib/fontisan/cldr/unicode_set_parser.rb +172 -0
  66. data/lib/fontisan/cldr/unknown_version_error.rb +9 -0
  67. data/lib/fontisan/cldr/version_resolver.rb +91 -0
  68. data/lib/fontisan/cldr.rb +23 -0
  69. data/lib/fontisan/cli/cldr_cli.rb +85 -0
  70. data/lib/fontisan/cli/ucd_cli.rb +97 -0
  71. data/lib/fontisan/cli.rb +201 -2
  72. data/lib/fontisan/collection/builder.rb +0 -4
  73. data/lib/fontisan/collection/dfont_builder.rb +0 -4
  74. data/lib/fontisan/collection/shared_logic.rb +0 -2
  75. data/lib/fontisan/collection/writer.rb +0 -3
  76. data/lib/fontisan/collection.rb +15 -0
  77. data/lib/fontisan/commands/audit_command.rb +123 -0
  78. data/lib/fontisan/commands/audit_compare_command.rb +66 -0
  79. data/lib/fontisan/commands/audit_library_command.rb +46 -0
  80. data/lib/fontisan/commands/base_command.rb +0 -3
  81. data/lib/fontisan/commands/convert_command.rb +25 -20
  82. data/lib/fontisan/commands/dump_table_command.rb +0 -3
  83. data/lib/fontisan/commands/export_command.rb +0 -4
  84. data/lib/fontisan/commands/features_command.rb +0 -3
  85. data/lib/fontisan/commands/instance_command.rb +0 -5
  86. data/lib/fontisan/commands/ls_command.rb +0 -6
  87. data/lib/fontisan/commands/optical_size_command.rb +0 -3
  88. data/lib/fontisan/commands/pack_command.rb +0 -5
  89. data/lib/fontisan/commands/scripts_command.rb +0 -2
  90. data/lib/fontisan/commands/subset_command.rb +0 -3
  91. data/lib/fontisan/commands/unicode_command.rb +0 -3
  92. data/lib/fontisan/commands/unpack_command.rb +0 -7
  93. data/lib/fontisan/commands/validate_command.rb +0 -8
  94. data/lib/fontisan/commands/variable_command.rb +0 -3
  95. data/lib/fontisan/commands.rb +29 -0
  96. data/lib/fontisan/config/cldr.yml +22 -0
  97. data/lib/fontisan/config/conversion_matrix.yml +38 -0
  98. data/lib/fontisan/config/ucd.yml +23 -0
  99. data/lib/fontisan/constants.rb +48 -6
  100. data/lib/fontisan/conversion_options.rb +30 -19
  101. data/lib/fontisan/converters/cff_table_builder.rb +0 -3
  102. data/lib/fontisan/converters/collection_converter.rb +0 -8
  103. data/lib/fontisan/converters/conversion_strategy.rb +161 -46
  104. data/lib/fontisan/converters/format_converter.rb +143 -32
  105. data/lib/fontisan/converters/glyf_table_builder.rb +0 -2
  106. data/lib/fontisan/converters/outline_converter.rb +0 -19
  107. data/lib/fontisan/converters/outline_extraction.rb +0 -5
  108. data/lib/fontisan/converters/outline_optimizer.rb +0 -5
  109. data/lib/fontisan/converters/svg_generator.rb +0 -4
  110. data/lib/fontisan/converters/table_copier.rb +0 -2
  111. data/lib/fontisan/converters/type1_converter.rb +0 -11
  112. data/lib/fontisan/converters/woff2_encoder.rb +49 -20
  113. data/lib/fontisan/converters/woff_writer.rb +211 -282
  114. data/lib/fontisan/converters.rb +21 -0
  115. data/lib/fontisan/dfont_collection.rb +29 -10
  116. data/lib/fontisan/export/exporter.rb +0 -6
  117. data/lib/fontisan/export/transformers/font_to_ttx.rb +0 -9
  118. data/lib/fontisan/export/transformers/head_transformer.rb +0 -2
  119. data/lib/fontisan/export/transformers/hhea_transformer.rb +0 -2
  120. data/lib/fontisan/export/transformers/maxp_transformer.rb +0 -2
  121. data/lib/fontisan/export/transformers/name_transformer.rb +0 -2
  122. data/lib/fontisan/export/transformers/os2_transformer.rb +0 -2
  123. data/lib/fontisan/export/transformers/post_transformer.rb +0 -2
  124. data/lib/fontisan/export/transformers.rb +17 -0
  125. data/lib/fontisan/export.rb +13 -0
  126. data/lib/fontisan/font_loader.rb +189 -328
  127. data/lib/fontisan/font_writer.rb +0 -1
  128. data/lib/fontisan/formatters/audit_diff_text_renderer.rb +122 -0
  129. data/lib/fontisan/formatters/audit_text_renderer.rb +324 -0
  130. data/lib/fontisan/formatters/library_summary_text_renderer.rb +99 -0
  131. data/lib/fontisan/formatters/text_formatter.rb +6 -0
  132. data/lib/fontisan/formatters.rb +12 -0
  133. data/lib/fontisan/hints/hint_converter.rb +0 -1
  134. data/lib/fontisan/hints/postscript_hint_applier.rb +0 -9
  135. data/lib/fontisan/hints/postscript_hint_extractor.rb +0 -2
  136. data/lib/fontisan/hints/truetype_hint_extractor.rb +0 -2
  137. data/lib/fontisan/hints.rb +16 -0
  138. data/lib/fontisan/metrics_calculator.rb +0 -2
  139. data/lib/fontisan/models/all_scripts_features_info.rb +0 -1
  140. data/lib/fontisan/models/audit/audit_axis.rb +30 -0
  141. data/lib/fontisan/models/audit/audit_block.rb +32 -0
  142. data/lib/fontisan/models/audit/audit_diff.rb +77 -0
  143. data/lib/fontisan/models/audit/audit_report.rb +153 -0
  144. data/lib/fontisan/models/audit/codepoint_range.rb +40 -0
  145. data/lib/fontisan/models/audit/codepoint_set_diff.rb +34 -0
  146. data/lib/fontisan/models/audit/color_capabilities.rb +93 -0
  147. data/lib/fontisan/models/audit/duplicate_group.rb +23 -0
  148. data/lib/fontisan/models/audit/embedding_type.rb +76 -0
  149. data/lib/fontisan/models/audit/field_change.rb +28 -0
  150. data/lib/fontisan/models/audit/fs_selection_flags.rb +61 -0
  151. data/lib/fontisan/models/audit/gasp_range.rb +63 -0
  152. data/lib/fontisan/models/audit/hinting.rb +93 -0
  153. data/lib/fontisan/models/audit/library_summary.rb +40 -0
  154. data/lib/fontisan/models/audit/licensing.rb +48 -0
  155. data/lib/fontisan/models/audit/metrics.rb +111 -0
  156. data/lib/fontisan/models/audit/named_instance.rb +41 -0
  157. data/lib/fontisan/models/audit/opentype_layout.rb +40 -0
  158. data/lib/fontisan/models/audit/script_coverage_row.rb +26 -0
  159. data/lib/fontisan/models/audit/script_features.rb +28 -0
  160. data/lib/fontisan/models/audit/variation_detail.rb +44 -0
  161. data/lib/fontisan/models/audit.rb +33 -0
  162. data/lib/fontisan/models/cldr/language_coverage.rb +31 -0
  163. data/lib/fontisan/models/cldr.rb +12 -0
  164. data/lib/fontisan/models/collection_brief_info.rb +0 -1
  165. data/lib/fontisan/models/collection_info.rb +0 -2
  166. data/lib/fontisan/models/collection_list_info.rb +0 -1
  167. data/lib/fontisan/models/collection_validation_report.rb +0 -2
  168. data/lib/fontisan/models/color_glyph.rb +0 -1
  169. data/lib/fontisan/models/font_report.rb +0 -1
  170. data/lib/fontisan/models/ttx/tables.rb +21 -0
  171. data/lib/fontisan/models/ttx/ttfont.rb +0 -8
  172. data/lib/fontisan/models/ttx.rb +14 -0
  173. data/lib/fontisan/models/ucd/ucd.rb +38 -0
  174. data/lib/fontisan/models/ucd/ucd_char.rb +67 -0
  175. data/lib/fontisan/models/ucd.rb +19 -0
  176. data/lib/fontisan/models.rb +47 -0
  177. data/lib/fontisan/open_type_collection.rb +6 -5
  178. data/lib/fontisan/open_type_font.rb +8 -2
  179. data/lib/fontisan/open_type_font_extensions.rb +9 -9
  180. data/lib/fontisan/optimizers/pattern_analyzer.rb +0 -1
  181. data/lib/fontisan/optimizers.rb +14 -0
  182. data/lib/fontisan/outline_extractor.rb +0 -2
  183. data/lib/fontisan/parsers/dfont_parser.rb +0 -1
  184. data/lib/fontisan/parsers.rb +10 -0
  185. data/lib/fontisan/pipeline/format_detector.rb +29 -102
  186. data/lib/fontisan/pipeline/output_writer.rb +11 -9
  187. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +0 -4
  188. data/lib/fontisan/pipeline/strategies/named_strategy.rb +0 -4
  189. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +0 -2
  190. data/lib/fontisan/pipeline/strategies.rb +14 -0
  191. data/lib/fontisan/pipeline/transformation_pipeline.rb +0 -7
  192. data/lib/fontisan/pipeline/variation_resolver.rb +0 -7
  193. data/lib/fontisan/pipeline.rb +13 -0
  194. data/lib/fontisan/sfnt_font.rb +29 -14
  195. data/lib/fontisan/sfnt_table.rb +0 -4
  196. data/lib/fontisan/subset/builder.rb +0 -6
  197. data/lib/fontisan/subset.rb +13 -0
  198. data/lib/fontisan/svg/font_generator.rb +0 -4
  199. data/lib/fontisan/svg/glyph_generator.rb +0 -2
  200. data/lib/fontisan/svg.rb +12 -0
  201. data/lib/fontisan/tables/cbdt.rb +0 -1
  202. data/lib/fontisan/tables/cblc.rb +0 -1
  203. data/lib/fontisan/tables/cff/charset.rb +0 -1
  204. data/lib/fontisan/tables/cff/charstring.rb +0 -1
  205. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +0 -4
  206. data/lib/fontisan/tables/cff/charstrings_index.rb +0 -3
  207. data/lib/fontisan/tables/cff/dict.rb +0 -1
  208. data/lib/fontisan/tables/cff/encoding.rb +0 -1
  209. data/lib/fontisan/tables/cff/header.rb +0 -2
  210. data/lib/fontisan/tables/cff/hint_operation_injector.rb +0 -2
  211. data/lib/fontisan/tables/cff/index.rb +0 -1
  212. data/lib/fontisan/tables/cff/private_dict.rb +0 -2
  213. data/lib/fontisan/tables/cff/private_dict_writer.rb +0 -2
  214. data/lib/fontisan/tables/cff/table_builder.rb +0 -6
  215. data/lib/fontisan/tables/cff/top_dict.rb +0 -2
  216. data/lib/fontisan/tables/cff.rb +22 -15
  217. data/lib/fontisan/tables/cff2/charstring_parser.rb +0 -2
  218. data/lib/fontisan/tables/cff2/table_builder.rb +0 -11
  219. data/lib/fontisan/tables/cff2/table_reader.rb +0 -2
  220. data/lib/fontisan/tables/cff2.rb +13 -14
  221. data/lib/fontisan/tables/cmap.rb +24 -2
  222. data/lib/fontisan/tables/cmap_table.rb +0 -3
  223. data/lib/fontisan/tables/colr.rb +0 -1
  224. data/lib/fontisan/tables/cpal.rb +0 -1
  225. data/lib/fontisan/tables/cvar.rb +0 -2
  226. data/lib/fontisan/tables/fvar.rb +0 -1
  227. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +0 -2
  228. data/lib/fontisan/tables/glyf/glyph_builder.rb +0 -3
  229. data/lib/fontisan/tables/glyf.rb +0 -6
  230. data/lib/fontisan/tables/glyf_table.rb +0 -3
  231. data/lib/fontisan/tables/gpos.rb +0 -2
  232. data/lib/fontisan/tables/gsub.rb +0 -2
  233. data/lib/fontisan/tables/gvar.rb +0 -2
  234. data/lib/fontisan/tables/head.rb +0 -2
  235. data/lib/fontisan/tables/head_table.rb +0 -3
  236. data/lib/fontisan/tables/hhea.rb +0 -2
  237. data/lib/fontisan/tables/hhea_table.rb +0 -3
  238. data/lib/fontisan/tables/hmtx.rb +0 -2
  239. data/lib/fontisan/tables/hmtx_table.rb +0 -3
  240. data/lib/fontisan/tables/hvar.rb +0 -3
  241. data/lib/fontisan/tables/loca.rb +0 -2
  242. data/lib/fontisan/tables/loca_table.rb +0 -3
  243. data/lib/fontisan/tables/maxp.rb +0 -2
  244. data/lib/fontisan/tables/maxp_table.rb +0 -3
  245. data/lib/fontisan/tables/mvar.rb +0 -3
  246. data/lib/fontisan/tables/name.rb +0 -2
  247. data/lib/fontisan/tables/name_table.rb +0 -3
  248. data/lib/fontisan/tables/os2_table.rb +0 -3
  249. data/lib/fontisan/tables/post_table.rb +0 -3
  250. data/lib/fontisan/tables/sbix.rb +0 -1
  251. data/lib/fontisan/tables/svg.rb +0 -1
  252. data/lib/fontisan/tables/variation_common.rb +0 -1
  253. data/lib/fontisan/tables/vvar.rb +0 -3
  254. data/lib/fontisan/tables.rb +54 -0
  255. data/lib/fontisan/true_type_collection.rb +6 -14
  256. data/lib/fontisan/true_type_font.rb +8 -2
  257. data/lib/fontisan/true_type_font_extensions.rb +9 -9
  258. data/lib/fontisan/type1/afm_generator.rb +0 -4
  259. data/lib/fontisan/type1/conversion_options.rb +0 -2
  260. data/lib/fontisan/type1/encodings.rb +0 -2
  261. data/lib/fontisan/type1/generator.rb +0 -8
  262. data/lib/fontisan/type1/pfa_generator.rb +0 -3
  263. data/lib/fontisan/type1/pfb_generator.rb +0 -5
  264. data/lib/fontisan/type1/pfm_generator.rb +0 -4
  265. data/lib/fontisan/type1.rb +42 -69
  266. data/lib/fontisan/type1_font.rb +40 -11
  267. data/lib/fontisan/ucd/aggregator.rb +73 -0
  268. data/lib/fontisan/ucd/cache_manager.rb +111 -0
  269. data/lib/fontisan/ucd/config.rb +59 -0
  270. data/lib/fontisan/ucd/download_error.rb +9 -0
  271. data/lib/fontisan/ucd/downloader.rb +88 -0
  272. data/lib/fontisan/ucd/error.rb +8 -0
  273. data/lib/fontisan/ucd/index.rb +103 -0
  274. data/lib/fontisan/ucd/index_builder.rb +107 -0
  275. data/lib/fontisan/ucd/range_entry.rb +56 -0
  276. data/lib/fontisan/ucd/unknown_version_error.rb +9 -0
  277. data/lib/fontisan/ucd/version_resolver.rb +79 -0
  278. data/lib/fontisan/ucd.rb +23 -0
  279. data/lib/fontisan/utilities/checksum_calculator.rb +0 -1
  280. data/lib/fontisan/utilities.rb +10 -0
  281. data/lib/fontisan/utils.rb +10 -0
  282. data/lib/fontisan/validation/collection_validator.rb +0 -2
  283. data/lib/fontisan/validation.rb +9 -0
  284. data/lib/fontisan/validators/basic_validator.rb +0 -2
  285. data/lib/fontisan/validators/font_book_validator.rb +0 -2
  286. data/lib/fontisan/validators/opentype_validator.rb +0 -2
  287. data/lib/fontisan/validators/profile_loader.rb +0 -5
  288. data/lib/fontisan/validators/validator.rb +0 -2
  289. data/lib/fontisan/validators/web_font_validator.rb +0 -2
  290. data/lib/fontisan/validators.rb +14 -0
  291. data/lib/fontisan/variable/delta_applicator.rb +0 -4
  292. data/lib/fontisan/variable/instancer.rb +0 -3
  293. data/lib/fontisan/variable/static_font_builder.rb +0 -3
  294. data/lib/fontisan/variable.rb +16 -0
  295. data/lib/fontisan/variation/blend_applier.rb +0 -2
  296. data/lib/fontisan/variation/cache.rb +0 -2
  297. data/lib/fontisan/variation/converter.rb +0 -3
  298. data/lib/fontisan/variation/data_extractor.rb +0 -2
  299. data/lib/fontisan/variation/delta_applier.rb +0 -5
  300. data/lib/fontisan/variation/inspector.rb +0 -1
  301. data/lib/fontisan/variation/instance_generator.rb +0 -6
  302. data/lib/fontisan/variation/instance_writer.rb +0 -5
  303. data/lib/fontisan/variation/metrics_adjuster.rb +0 -4
  304. data/lib/fontisan/variation/optimizer.rb +0 -3
  305. data/lib/fontisan/variation/parallel_generator.rb +0 -3
  306. data/lib/fontisan/variation/subsetter.rb +0 -4
  307. data/lib/fontisan/variation/tuple_variation_header.rb +0 -2
  308. data/lib/fontisan/variation/variable_svg_generator.rb +0 -3
  309. data/lib/fontisan/variation/variation_context.rb +0 -3
  310. data/lib/fontisan/variation/variation_preserver.rb +0 -3
  311. data/lib/fontisan/variation.rb +31 -0
  312. data/lib/fontisan/version.rb +1 -1
  313. data/lib/fontisan/woff2.rb +13 -0
  314. data/lib/fontisan/woff2_font.rb +31 -9
  315. data/lib/fontisan/woff_font.rb +31 -2
  316. data/lib/fontisan.rb +124 -196
  317. metadata +128 -7
  318. data/fontisan.gemspec +0 -47
@@ -1,22 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "constants"
4
- require_relative "loading_modes"
5
- require_relative "true_type_font"
6
- require_relative "open_type_font"
7
- require_relative "true_type_collection"
8
- require_relative "open_type_collection"
9
- require_relative "woff_font"
10
- require_relative "woff2_font"
11
- require_relative "type1_font"
12
- require_relative "error"
3
+ require "stringio"
13
4
 
14
5
  module Fontisan
15
- # FontLoader provides unified font loading with automatic format detection.
6
+ # FontLoader provides unified font loading with content-based format detection.
16
7
  #
17
8
  # This class is the primary entry point for loading fonts in Fontisan.
18
- # It automatically detects the font format and returns the appropriate
19
- # domain object (TrueTypeFont, OpenTypeFont, Type1Font, TrueTypeCollection, or OpenTypeCollection).
9
+ # It inspects each file's magic bytes to determine the on-disk format and
10
+ # returns the appropriate domain object (TrueTypeFont, OpenTypeFont,
11
+ # Type1Font, TrueTypeCollection, or OpenTypeCollection).
12
+ #
13
+ # Detection is purely content-based — the file extension is ignored. This
14
+ # matters because vendors occasionally ship files with a misleading
15
+ # extension (e.g. Apple ships a single OpenType-CFF font as `.ttc` in
16
+ # macOS's private FontServices framework).
20
17
  #
21
18
  # @example Load any font type
22
19
  # font = FontLoader.load("font.ttf") # => TrueTypeFont
@@ -34,7 +31,28 @@ module Fontisan
34
31
  # font = FontLoader.load("font.ttf", lazy: true) # Tables loaded on-demand
35
32
  # font = FontLoader.load("font.ttf", lazy: false) # All tables loaded upfront
36
33
  class FontLoader
37
- # Load a font from file with automatic format detection
34
+ # Number of bytes read from the start of a file to identify its format.
35
+ # 100 bytes is enough to comfortably contain the Adobe Type 1 PFA header
36
+ # plus its leading whitespace, and far more than the 4 bytes needed for
37
+ # any SFNT-style or dfont magic.
38
+ PFA_PROBE_LENGTH = 100
39
+ private_constant :PFA_PROBE_LENGTH
40
+
41
+ # Map of collection format symbols to the class that loads them. Single
42
+ # source of truth for "what counts as a collection"; both {.collection?}
43
+ # and {.load_collection} dispatch off this table.
44
+ COLLECTION_CLASSES = {
45
+ ttc: TrueTypeCollection,
46
+ otc: OpenTypeCollection,
47
+ dfont: DfontCollection,
48
+ }.freeze
49
+ private_constant :COLLECTION_CLASSES
50
+
51
+ # Load a font from file with content-based format detection.
52
+ #
53
+ # The file's bytes determine its format; the extension is ignored. See
54
+ # {.detect_format} for the full list of recognised formats and how they
55
+ # are detected.
38
56
  #
39
57
  # @param path [String] Path to the font file
40
58
  # @param font_index [Integer] Index of font in collection (0-based, default: 0)
@@ -45,125 +63,93 @@ module Fontisan
45
63
  # @raise [UnsupportedFormatError] for unsupported formats
46
64
  # @raise [InvalidFontError] for corrupted or unknown formats
47
65
  def self.load(path, font_index: 0, mode: nil, lazy: nil)
48
- raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
49
-
50
- # Resolve mode and lazy parameters with environment variables
51
66
  resolved_mode = mode || env_mode || LoadingModes::FULL
52
67
  resolved_lazy = if lazy.nil?
53
68
  env_lazy.nil? ? false : env_lazy
54
69
  else
55
70
  lazy
56
71
  end
57
-
58
- # Validate mode
59
72
  LoadingModes.validate_mode!(resolved_mode)
60
73
 
61
- # Check for Type 1 format first (PFB/PFA have different signatures)
62
- if type1_font?(path)
63
- return Type1Font.from_file(path, mode: resolved_mode)
64
- end
65
-
66
- File.open(path, "rb") do |io|
67
- signature = io.read(4)
68
- io.rewind
69
-
70
- case signature
71
- when Constants::TTC_TAG
72
- load_from_collection(io, path, font_index, mode: resolved_mode,
73
- lazy: resolved_lazy)
74
- when pack_uint32(Constants::SFNT_VERSION_TRUETYPE), "true"
75
- TrueTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
76
- when "OTTO"
77
- OpenTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
78
- when "wOFF"
79
- WoffFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
80
- when "wOF2"
81
- Woff2Font.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
82
- when Constants::DFONT_RESOURCE_HEADER
83
- extract_and_load_dfont(io, path, font_index, resolved_mode,
84
- resolved_lazy)
85
- else
86
- raise InvalidFontError,
87
- "Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, WOFF2, PFB, or PFA file."
88
- end
74
+ format = detect(path)
75
+ case format
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)
88
+ when :pfa, :pfb then Type1Font.from_file(path, mode: resolved_mode)
89
+ else
90
+ raise InvalidFontError,
91
+ "Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, WOFF2, PFB, or PFA file."
89
92
  end
90
93
  end
91
94
 
92
- # Check if a file is a collection (TTC or OTC)
95
+ # Check if a file is a collection (TTC, OTC, or dfont).
96
+ #
97
+ # Returns `false` for a ttcf-headed file whose inner fonts can't be
98
+ # classified (truncated header, offsets past EOF, unrecognised inner
99
+ # SFNT versions). Such a file is structurally invalid as a collection
100
+ # and would fail to load, so reporting it as "not a collection" matches
101
+ # what callers can actually do with it.
93
102
  #
94
103
  # @param path [String] Path to the font file
95
- # @return [Boolean] true if file is a TTC/OTC collection
104
+ # @return [Boolean] true if file is a loadable collection
96
105
  # @raise [Errno::ENOENT] if file does not exist
97
106
  #
98
107
  # @example Check if file is collection
99
108
  # FontLoader.collection?("fonts.ttc") # => true
100
109
  # FontLoader.collection?("font.ttf") # => false
101
- def self.collection?(path)
102
- raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
103
-
104
- File.open(path, "rb") do |io|
105
- signature = io.read(4)
106
- io.rewind
107
-
108
- # Check for TTC/OTC signature
109
- return true if signature == Constants::TTC_TAG
110
-
111
- # Check for dfont - dfont is a collection format even if it contains only one font
112
- if signature == Constants::DFONT_RESOURCE_HEADER
113
- require_relative "parsers/dfont_parser"
114
- return Parsers::DfontParser.dfont?(io)
115
- end
116
-
117
- false
118
- end
119
- end
110
+ def self.collection?(path) = COLLECTION_CLASSES.key?(detect(path))
120
111
 
121
- # Load a collection object without extracting fonts
122
- #
123
- # Returns the collection object (TrueTypeCollection, OpenTypeCollection, or DfontCollection)
124
- # without extracting individual fonts. Useful for inspecting collection
125
- # metadata and structure.
112
+ # Identify a font file by inspecting its magic bytes (content-based detection).
126
113
  #
127
- # = Collection Format Understanding
114
+ # Returns the actual on-disk format regardless of the file extension. This
115
+ # is the authoritative way to determine how a file should be parsed,
116
+ # because vendors occasionally ship files with a misleading extension
117
+ # (for example, Apple ships a single OpenType-CFF font as `.ttc` in
118
+ # macOS's private FontServices framework).
128
119
  #
129
- # Both TTC (TrueType Collection) and OTC (OpenType Collection) files use
130
- # the same "ttcf" signature. The distinction between TTC and OTC is NOT
131
- # in the collection format itself, but in the fonts contained within:
120
+ # Collections are distinguished by scanning the inner fonts: if any inner
121
+ # font is OpenType (CFF), the file is reported as `:otc`; otherwise (all
122
+ # inner fonts are TrueType) it is reported as `:ttc`. A ttcf-headed file
123
+ # whose inner fonts can't be classified (truncated header, offsets past
124
+ # EOF, unrecognised inner SFNT versions) returns `nil`. dfont detection
125
+ # uses the canonical resource-data-offset (256) magic only; non-canonical
126
+ # but structurally valid dfonts are accepted by {.load_collection} as a
127
+ # fallback but not reported here.
132
128
  #
133
- # - TTC typically contains TrueType fonts (glyf outlines)
134
- # - OTC typically contains OpenType fonts (CFF/CFF2 outlines)
135
- # - Mixed collections are possible (both TTF and OTF in same collection)
136
- #
137
- # dfont (Data Fork Font) is an Apple-specific format that contains Mac
138
- # font suitcase resources. It can contain multiple SFNT fonts (TrueType
139
- # or OpenType).
140
- #
141
- # Each collection can contain multiple SFNT-format font files, with table
142
- # deduplication to save space. Individual fonts within a collection are
143
- # stored at different offsets within the file, each with their own table
144
- # directory and data tables.
145
- #
146
- # = Detection Strategy
129
+ # @param path [String] Path to the font file
130
+ # @return [Symbol, nil] One of `:ttf`, `:otf`, `:ttc`, `:otc`, `:woff`,
131
+ # `:woff2`, `:dfont`, `:pfa`, `:pfb`, or `nil` when the format is not
132
+ # recognised.
133
+ # @raise [Errno::ENOENT] if the file does not exist
147
134
  #
148
- # This method scans ALL fonts in the collection to determine the collection
149
- # type accurately:
135
+ # @example Detect a real collection
136
+ # FontLoader.detect_format("fonts.ttc") # => :ttc
150
137
  #
151
- # 1. Reads all font offsets from the collection header
152
- # 2. Examines the sfnt_version of each font in the collection
153
- # 3. Counts TrueType fonts (0x00010000 or 0x74727565 "true") vs OpenType fonts (0x4F54544F "OTTO")
154
- # 4. If ANY font is OpenType (CFF), returns OpenTypeCollection
155
- # 5. Only returns TrueTypeCollection if ALL fonts are TrueType
138
+ # @example Detect a single OTF mislabeled as .ttc
139
+ # FontLoader.detect_format("SauberScript.ttc") # => :otf
140
+ def self.detect_format(path) = detect(path)
141
+
142
+ # Load a collection object without extracting fonts
156
143
  #
157
- # For dfont files, returns DfontCollection.
144
+ # Returns the collection object (TrueTypeCollection, OpenTypeCollection,
145
+ # or DfontCollection) without extracting individual fonts. Useful for
146
+ # inspecting collection metadata and structure.
158
147
  #
159
- # This approach correctly handles:
160
- # - Homogeneous collections (all TTF or all OTF)
161
- # - Mixed collections (both TTF and OTF fonts) - uses OpenTypeCollection
162
- # - Large collections with many fonts (like NotoSerifCJK.ttc with 35 fonts)
163
- # - dfont suitcases (Apple-specific)
148
+ # The TTC vs. OTC distinction is resolved by {.detect_format}, which
149
+ # scans the inner fonts; see that method for details.
164
150
  #
165
151
  # @param path [String] Path to the collection file
166
- # @return [TrueTypeCollection, OpenTypeCollection, DfontCollection] The collection object
152
+ # @return [TrueTypeCollection, OpenTypeCollection, DfontCollection]
167
153
  # @raise [Errno::ENOENT] if file does not exist
168
154
  # @raise [InvalidFontError] if file is not a collection or type cannot be determined
169
155
  #
@@ -171,69 +157,88 @@ module Fontisan
171
157
  # collection = FontLoader.load_collection("fonts.ttc")
172
158
  # puts "Collection has #{collection.num_fonts} fonts"
173
159
  def self.load_collection(path)
174
- raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
160
+ format = detect(path)
161
+ return COLLECTION_CLASSES.fetch(format).from_file(path) if COLLECTION_CLASSES.key?(format)
175
162
 
163
+ # Lenient fallback: a dfont whose resource-data offset isn't the
164
+ # canonical 256 fails the strict magic test in {.detect} but may still
165
+ # be structurally valid; try the structural check before giving up.
176
166
  File.open(path, "rb") do |io|
177
- signature = io.read(4)
178
- io.rewind
179
-
180
- # Check for dfont
181
- if signature == Constants::DFONT_RESOURCE_HEADER || dfont_signature?(io)
182
- require_relative "dfont_collection"
183
- return DfontCollection.from_file(path)
184
- end
185
-
186
- # Check for TTC/OTC
187
- unless signature == Constants::TTC_TAG
188
- raise InvalidFontError,
189
- "File is not a collection (TTC/OTC/dfont). Use FontLoader.load instead."
190
- end
191
-
192
- # Read version and num_fonts
193
- io.seek(8) # Skip tag (4) + version (4)
194
- num_fonts = io.read(4).unpack1("N")
195
-
196
- # Read all font offsets
197
- font_offsets = Array.new(num_fonts) { io.read(4).unpack1("N") }
167
+ return DfontCollection.from_file(path) if Parsers::DfontParser.dfont?(io)
168
+ end
169
+ raise InvalidFontError,
170
+ "File is not a collection (TTC/OTC/dfont). Use FontLoader.load instead."
171
+ end
198
172
 
199
- # Scan all fonts to determine collection type (not just first)
200
- truetype_count = 0
201
- opentype_count = 0
173
+ # Content-based detection. Reads 4 bytes first (covers every SFNT-style
174
+ # and canonical dfont magic), then tops up to {PFA_PROBE_LENGTH} for
175
+ # Type 1 only on an SFNT miss.
176
+ def self.detect(path)
177
+ raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
202
178
 
203
- font_offsets.each do |offset|
204
- io.rewind
205
- io.seek(offset)
206
- sfnt_version = io.read(4).unpack1("N")
179
+ File.open(path, "rb") do |io|
180
+ head4 = io.read(4)
181
+ return nil if head4.nil? || head4.empty?
182
+
183
+ sfnt = case head4
184
+ when Constants::TTC_TAG then scan_collection(io)
185
+ when Constants::SFNT_OTTO_MAGIC then :otf
186
+ when Constants::SFNT_TRUETYPE_MAGIC, Constants::SFNT_TRUE_MAGIC then :ttf
187
+ when Constants::WOFF_MAGIC then :woff
188
+ when Constants::WOFF2_MAGIC then :woff2
189
+ when Constants::DFONT_RESOURCE_HEADER
190
+ io.rewind
191
+ Parsers::DfontParser.dfont?(io) ? :dfont : nil
192
+ end
193
+ return sfnt if sfnt
194
+
195
+ rest = head4.bytesize < PFA_PROBE_LENGTH ? io.read(PFA_PROBE_LENGTH - head4.bytesize) : nil
196
+ type1_format_from_header(rest ? head4 + rest : head4)
197
+ end
198
+ end
207
199
 
208
- case sfnt_version
209
- when Constants::SFNT_VERSION_TRUETYPE, 0x74727565 # 0x74727565 = 'true'
210
- truetype_count += 1
211
- when Constants::SFNT_VERSION_OTTO
212
- opentype_count += 1
213
- else
214
- raise InvalidFontError,
215
- "Unknown font type in collection at offset #{offset} (sfnt version: 0x#{sfnt_version.to_s(16)})"
216
- end
200
+ # Identify the Type 1 sub-format (`:pfa` or `:pfb`) from a probe of the
201
+ # file's leading bytes. Returns nil if the bytes don't match Type 1.
202
+ def self.type1_format_from_header(header)
203
+ if header.bytesize >= 2
204
+ marker = (header.getbyte(0) << 8) | header.getbyte(1)
205
+ if [Constants::PFB_ASCII_CHUNK, Constants::PFB_BINARY_CHUNK].include?(marker)
206
+ return :pfb
217
207
  end
208
+ end
209
+
210
+ # PFA is plain text — the Adobe Type 1 header must appear at the very
211
+ # start (allowing only leading ASCII whitespace), not anywhere in the
212
+ # probe. Using start_with? avoids matching a non-Type-1 PostScript file
213
+ # that happens to mention the signature in a comment.
214
+ stripped = header.lstrip
215
+ if stripped.start_with?(Constants::PFA_SIGNATURE_ADOBE_1_0, Constants::PFA_SIGNATURE_ADOBE_3_0)
216
+ return :pfa
217
+ end
218
218
 
219
- io.rewind
219
+ nil
220
+ end
220
221
 
221
- # Determine collection type based on what fonts are inside
222
- # If ANY font is OpenType, use OpenTypeCollection (more general format)
223
- # Only use TrueTypeCollection if ALL fonts are TrueType
224
- if opentype_count.positive?
225
- OpenTypeCollection.from_file(path)
226
- else
227
- # All fonts are TrueType
228
- TrueTypeCollection.from_file(path)
222
+ # Walk a ttcf-headed file via BaseCollection. Returns `:ttc`, `:otc`, or
223
+ # nil for any truncation, unreadable offset, or unrecognised inner magic.
224
+ def self.scan_collection(io)
225
+ io.rewind
226
+ header = BaseCollection.read(io)
227
+ has_otf = false
228
+ header.font_offsets.each do |offset|
229
+ io.seek(offset)
230
+ case Constants.sfnt_format_for(io.read(4))
231
+ when :otf then has_otf = true
232
+ when :ttf then next
233
+ else return nil
229
234
  end
230
235
  end
236
+ has_otf ? :otc : :ttc
237
+ rescue BinData::ValidityError, IOError
238
+ nil
231
239
  end
232
240
 
233
- # Get mode from environment variable
234
- #
235
- # @return [Symbol, nil] Mode from FONTISAN_MODE or nil
236
- # @api private
241
+ # Mode override from FONTISAN_MODE env var, or nil.
237
242
  def self.env_mode
238
243
  env_value = ENV["FONTISAN_MODE"]
239
244
  return nil unless env_value
@@ -242,10 +247,7 @@ module Fontisan
242
247
  LoadingModes.valid_mode?(mode) ? mode : nil
243
248
  end
244
249
 
245
- # Get lazy setting from environment variable
246
- #
247
- # @return [Boolean, nil] Lazy setting from FONTISAN_LAZY or nil if not set
248
- # @api private
250
+ # Lazy override from FONTISAN_LAZY env var, or nil.
249
251
  def self.env_lazy
250
252
  env_value = ENV["FONTISAN_LAZY"]
251
253
  return nil unless env_value
@@ -253,182 +255,41 @@ module Fontisan
253
255
  env_value.downcase == "true"
254
256
  end
255
257
 
256
- # Load from a collection file (TTC or OTC)
257
- #
258
- # This is the internal method that handles loading individual fonts from
259
- # collection files. It reads the collection header to determine the type
260
- # (TTC vs OTC) and extracts the requested font.
261
- #
262
- # = Collection Header Structure
263
- #
264
- # TTC/OTC files start with:
265
- # - Bytes 0-3: "ttcf" tag (4 bytes)
266
- # - Bytes 4-7: version (2 bytes major + 2 bytes minor)
267
- # - Bytes 8-11: num_fonts (4 bytes, big-endian uint32)
268
- # - Bytes 12+: font offset array (4 bytes per font, big-endian uint32)
269
- #
270
- # CRITICAL: The method seeks to position 8 (after tag and version) to read
271
- # num_fonts, NOT position 12 which is where the offset array starts. This
272
- # was a bug that caused "Unknown font type" errors when the first offset
273
- # was misread as num_fonts.
274
- #
275
- # @param io [IO] Open file handle
276
- # @param path [String] Path to the collection file
277
- # @param font_index [Integer] Index of font to extract
278
- # @param mode [Symbol] Loading mode (:metadata or :full)
279
- # @param lazy [Boolean] If true, load tables on demand
280
- # @return [TrueTypeFont, OpenTypeFont] The loaded font object
281
- # @raise [InvalidFontError] if collection type cannot be determined
282
- def self.load_from_collection(io, path, font_index,
283
- mode: LoadingModes::FULL, lazy: true)
284
- # Read collection header to get font offsets
285
- io.seek(8) # Skip tag (4) + version (4)
286
- num_fonts = io.read(4).unpack1("N")
287
-
288
- if font_index >= num_fonts
258
+ # Load a single font from a TTC/OTC collection. `format` is the detected
259
+ # symbol routed from `.load`'s case statement, so no second magic read.
260
+ def self.load_from_collection(path, format, font_index, mode:)
261
+ collection = COLLECTION_CLASSES.fetch(format).from_file(path)
262
+ if font_index >= collection.num_fonts
289
263
  raise InvalidFontError,
290
- "Font index #{font_index} out of range (collection has #{num_fonts} fonts)"
291
- end
292
-
293
- # Read all font offsets
294
- font_offsets = Array.new(num_fonts) { io.read(4).unpack1("N") }
295
-
296
- # Scan all fonts to determine collection type (not just first)
297
- truetype_count = 0
298
- opentype_count = 0
299
-
300
- font_offsets.each do |offset|
301
- io.rewind
302
- io.seek(offset)
303
- sfnt_version = io.read(4).unpack1("N")
304
-
305
- case sfnt_version
306
- when Constants::SFNT_VERSION_TRUETYPE, 0x74727565 # 0x74727565 = 'true'
307
- truetype_count += 1
308
- when Constants::SFNT_VERSION_OTTO
309
- opentype_count += 1
310
- else
311
- raise InvalidFontError,
312
- "Unknown font type in collection at offset #{offset} (sfnt version: 0x#{sfnt_version.to_s(16)})"
313
- end
264
+ "Font index #{font_index} out of range (collection has #{collection.num_fonts} fonts)"
314
265
  end
315
266
 
316
- io.rewind
317
-
318
- # If ANY font is OpenType, use OpenTypeCollection (more general format)
319
- # Only use TrueTypeCollection if ALL fonts are TrueType
320
- if opentype_count.positive?
321
- # OpenType Collection
322
- otc = OpenTypeCollection.from_file(path)
323
- File.open(path, "rb") { |f| otc.font(font_index, f, mode: mode) }
324
- else
325
- # TrueType Collection (all fonts are TrueType)
326
- ttc = TrueTypeCollection.from_file(path)
327
- File.open(path, "rb") { |f| ttc.font(font_index, f, mode: mode) }
328
- end
267
+ File.open(path, "rb") { |io| collection.font(font_index, io, mode: mode) }
329
268
  end
330
269
 
331
- # Extract and load font from dfont resource fork
332
- #
333
- # @param io [IO] Open file handle
334
- # @param path [String] Path to dfont file
335
- # @param font_index [Integer] Font index in suitcase
336
- # @param mode [Symbol] Loading mode
337
- # @param lazy [Boolean] Lazy loading flag
338
- # @return [TrueTypeFont, OpenTypeFont] Loaded font
339
- # @api private
340
- def self.extract_and_load_dfont(io, _path, font_index, mode, lazy)
341
- require_relative "parsers/dfont_parser"
342
-
343
- # Extract SFNT data from resource fork
344
- sfnt_data = Parsers::DfontParser.extract_sfnt(io, index: font_index)
345
-
346
- # Create StringIO with SFNT data
347
- sfnt_io = StringIO.new(sfnt_data)
348
-
349
- # Detect SFNT signature
350
- signature = sfnt_io.read(4)
351
- sfnt_io.rewind
352
-
353
- # Read and setup font based on signature
354
- case signature
355
- when pack_uint32(Constants::SFNT_VERSION_TRUETYPE), "true"
356
- font = TrueTypeFont.read(sfnt_io)
357
- font.initialize_storage
358
- font.loading_mode = mode
359
- font.lazy_load_enabled = lazy
360
- font.read_table_data(sfnt_io) unless lazy
361
- font
362
- when "OTTO"
363
- font = OpenTypeFont.read(sfnt_io)
364
- font.initialize_storage
365
- font.loading_mode = mode
366
- font.lazy_load_enabled = lazy
367
- font.read_table_data(sfnt_io) unless lazy
368
- font
369
- else
370
- raise InvalidFontError,
371
- "Invalid SFNT data in dfont resource (signature: #{signature.inspect})"
372
- end
373
- end
374
-
375
- # Pack uint32 value to big-endian bytes
376
- #
377
- # @param value [Integer] The uint32 value
378
- # @return [String] 4-byte binary string
379
- # @api private
380
- def self.pack_uint32(value)
381
- [value].pack("N")
382
- end
383
-
384
- private_class_method :load_from_collection, :pack_uint32, :env_mode,
385
- :env_lazy, :extract_and_load_dfont
386
-
387
- # Check if file has dfont signature
388
- #
389
- # @param io [IO] Open file handle
390
- # @return [Boolean] true if dfont
391
- # @api private
392
- def self.dfont_signature?(io)
393
- require_relative "parsers/dfont_parser"
394
- Parsers::DfontParser.dfont?(io)
395
- end
396
-
397
- private_class_method :dfont_signature?
398
-
399
- # Check if file is a Type 1 font (PFB or PFA)
400
- #
401
- # Type 1 fonts come in two formats:
402
- # - PFB (Printer Font Binary): Binary format with chunk markers
403
- # - PFA (Printer Font ASCII): ASCII text format with hex encoding
404
- #
405
- # @param path [String] Path to the font file
406
- # @return [Boolean] true if Type 1 font
407
- # @api private
408
- def self.type1_font?(path)
409
- # Check file extension first (quick check)
410
- ext = File.extname(path).downcase
411
- return true if [".pfb", ".pfa", ".ps"].include?(ext)
412
-
413
- # Check PFB signature (first byte should be 0x80 or 0x81)
270
+ # Extract an SFNT from a dfont resource fork into memory and load it via
271
+ # `SfntFont.from_collection` so the loading-mode handling matches the
272
+ # TTC/OTC path. Lazy loading is a no-op for in-memory StringIO so the
273
+ # public `lazy:` flag is not threaded through this path.
274
+ def self.load_dfont(path, font_index:, mode:)
414
275
  File.open(path, "rb") do |io|
415
- first_byte = io.getbyte
416
- return true if [Constants::PFB_ASCII_CHUNK, Constants::PFB_BINARY_CHUNK].include?(first_byte)
276
+ sfnt_io = StringIO.new(Parsers::DfontParser.extract_sfnt(io,
277
+ index: font_index))
278
+ klass = case Constants.sfnt_format_for(sfnt_io.read(4))
279
+ when :ttf then TrueTypeFont
280
+ when :otf then OpenTypeFont
281
+ else raise InvalidFontError, "Invalid SFNT in dfont resource"
282
+ end
283
+ klass.from_collection(sfnt_io, 0, mode: mode)
417
284
  end
418
-
419
- # Check PFA signature (text file with Adobe header)
420
- File.open(path, "rb") do |io|
421
- # Read first 100 bytes to check for PFA signature
422
- header = io.read(100)
423
- return true if header.include?(Constants::PFA_SIGNATURE_ADOBE_1_0) ||
424
- header.include?(Constants::PFA_SIGNATURE_ADOBE_3_0)
425
- end
426
-
427
- false
428
- rescue IOError, Errno::ENOENT
429
- false
430
285
  end
431
286
 
432
- private_class_method :type1_font?
287
+ private_class_method :detect,
288
+ :type1_format_from_header,
289
+ :scan_collection,
290
+ :env_mode,
291
+ :env_lazy,
292
+ :load_from_collection,
293
+ :load_dfont
433
294
  end
434
295
  end
@@ -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