fontisan 0.1.0 → 0.2.1

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 (214) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +672 -69
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1477 -297
  6. data/Rakefile +63 -41
  7. data/benchmark/variation_quick_bench.rb +47 -0
  8. data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
  9. data/fontisan.gemspec +4 -1
  10. data/lib/fontisan/binary/base_record.rb +22 -1
  11. data/lib/fontisan/cli.rb +364 -4
  12. data/lib/fontisan/collection/builder.rb +341 -0
  13. data/lib/fontisan/collection/offset_calculator.rb +227 -0
  14. data/lib/fontisan/collection/table_analyzer.rb +204 -0
  15. data/lib/fontisan/collection/table_deduplicator.rb +317 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +24 -1
  18. data/lib/fontisan/commands/convert_command.rb +218 -0
  19. data/lib/fontisan/commands/export_command.rb +161 -0
  20. data/lib/fontisan/commands/info_command.rb +40 -6
  21. data/lib/fontisan/commands/instance_command.rb +286 -0
  22. data/lib/fontisan/commands/ls_command.rb +113 -0
  23. data/lib/fontisan/commands/pack_command.rb +241 -0
  24. data/lib/fontisan/commands/subset_command.rb +245 -0
  25. data/lib/fontisan/commands/unpack_command.rb +338 -0
  26. data/lib/fontisan/commands/validate_command.rb +203 -0
  27. data/lib/fontisan/commands/variable_command.rb +30 -1
  28. data/lib/fontisan/config/collection_settings.yml +56 -0
  29. data/lib/fontisan/config/conversion_matrix.yml +212 -0
  30. data/lib/fontisan/config/export_settings.yml +66 -0
  31. data/lib/fontisan/config/subset_profiles.yml +100 -0
  32. data/lib/fontisan/config/svg_settings.yml +60 -0
  33. data/lib/fontisan/config/validation_rules.yml +149 -0
  34. data/lib/fontisan/config/variable_settings.yml +99 -0
  35. data/lib/fontisan/config/woff2_settings.yml +77 -0
  36. data/lib/fontisan/constants.rb +79 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +408 -0
  39. data/lib/fontisan/converters/outline_converter.rb +998 -0
  40. data/lib/fontisan/converters/svg_generator.rb +244 -0
  41. data/lib/fontisan/converters/table_copier.rb +117 -0
  42. data/lib/fontisan/converters/woff2_encoder.rb +416 -0
  43. data/lib/fontisan/converters/woff_writer.rb +391 -0
  44. data/lib/fontisan/error.rb +203 -0
  45. data/lib/fontisan/export/exporter.rb +262 -0
  46. data/lib/fontisan/export/table_serializer.rb +255 -0
  47. data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
  48. data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
  49. data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
  50. data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
  51. data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
  52. data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
  53. data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
  54. data/lib/fontisan/export/ttx_generator.rb +527 -0
  55. data/lib/fontisan/export/ttx_parser.rb +300 -0
  56. data/lib/fontisan/font_loader.rb +122 -15
  57. data/lib/fontisan/font_writer.rb +302 -0
  58. data/lib/fontisan/formatters/text_formatter.rb +102 -0
  59. data/lib/fontisan/glyph_accessor.rb +503 -0
  60. data/lib/fontisan/hints/hint_converter.rb +310 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
  65. data/lib/fontisan/loading_modes.rb +115 -0
  66. data/lib/fontisan/metrics_calculator.rb +277 -0
  67. data/lib/fontisan/models/collection_font_summary.rb +52 -0
  68. data/lib/fontisan/models/collection_info.rb +76 -0
  69. data/lib/fontisan/models/collection_list_info.rb +37 -0
  70. data/lib/fontisan/models/font_export.rb +158 -0
  71. data/lib/fontisan/models/font_summary.rb +48 -0
  72. data/lib/fontisan/models/glyph_outline.rb +343 -0
  73. data/lib/fontisan/models/hint.rb +405 -0
  74. data/lib/fontisan/models/outline.rb +664 -0
  75. data/lib/fontisan/models/table_sharing_info.rb +40 -0
  76. data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
  77. data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
  78. data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
  79. data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
  80. data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
  81. data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
  82. data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
  83. data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
  84. data/lib/fontisan/models/ttx/ttfont.rb +49 -0
  85. data/lib/fontisan/models/validation_report.rb +203 -0
  86. data/lib/fontisan/open_type_collection.rb +156 -2
  87. data/lib/fontisan/open_type_font.rb +321 -19
  88. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  89. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  90. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  91. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  92. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  93. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  94. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  95. data/lib/fontisan/outline_extractor.rb +423 -0
  96. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  97. data/lib/fontisan/pipeline/output_writer.rb +154 -0
  98. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  99. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  100. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  101. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  102. data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
  103. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  104. data/lib/fontisan/subset/builder.rb +268 -0
  105. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  106. data/lib/fontisan/subset/options.rb +142 -0
  107. data/lib/fontisan/subset/profile.rb +152 -0
  108. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  109. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  110. data/lib/fontisan/svg/font_generator.rb +264 -0
  111. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  112. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  113. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  114. data/lib/fontisan/tables/cff/charset.rb +282 -0
  115. data/lib/fontisan/tables/cff/charstring.rb +934 -0
  116. data/lib/fontisan/tables/cff/charstring_builder.rb +356 -0
  117. data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
  118. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  119. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  120. data/lib/fontisan/tables/cff/dict.rb +351 -0
  121. data/lib/fontisan/tables/cff/dict_builder.rb +257 -0
  122. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  123. data/lib/fontisan/tables/cff/header.rb +102 -0
  124. data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
  125. data/lib/fontisan/tables/cff/index.rb +237 -0
  126. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  127. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  128. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  129. data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
  130. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  131. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  132. data/lib/fontisan/tables/cff.rb +489 -0
  133. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  134. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  135. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  136. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +246 -0
  137. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  138. data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
  139. data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
  140. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  141. data/lib/fontisan/tables/cff2.rb +346 -0
  142. data/lib/fontisan/tables/cvar.rb +203 -0
  143. data/lib/fontisan/tables/fvar.rb +2 -2
  144. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  145. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  146. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  147. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  148. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  149. data/lib/fontisan/tables/glyf.rb +235 -0
  150. data/lib/fontisan/tables/gvar.rb +231 -0
  151. data/lib/fontisan/tables/hhea.rb +124 -0
  152. data/lib/fontisan/tables/hmtx.rb +287 -0
  153. data/lib/fontisan/tables/hvar.rb +191 -0
  154. data/lib/fontisan/tables/loca.rb +322 -0
  155. data/lib/fontisan/tables/maxp.rb +192 -0
  156. data/lib/fontisan/tables/mvar.rb +185 -0
  157. data/lib/fontisan/tables/name.rb +99 -30
  158. data/lib/fontisan/tables/variation_common.rb +346 -0
  159. data/lib/fontisan/tables/vvar.rb +234 -0
  160. data/lib/fontisan/true_type_collection.rb +156 -2
  161. data/lib/fontisan/true_type_font.rb +321 -20
  162. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  163. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  164. data/lib/fontisan/utilities/checksum_calculator.rb +60 -0
  165. data/lib/fontisan/utils/thread_pool.rb +134 -0
  166. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  167. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  168. data/lib/fontisan/validation/structure_validator.rb +198 -0
  169. data/lib/fontisan/validation/table_validator.rb +158 -0
  170. data/lib/fontisan/validation/validator.rb +152 -0
  171. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  172. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  173. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  174. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  175. data/lib/fontisan/variable/instancer.rb +344 -0
  176. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  177. data/lib/fontisan/variable/region_matcher.rb +208 -0
  178. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  179. data/lib/fontisan/variable/table_updater.rb +219 -0
  180. data/lib/fontisan/variation/blend_applier.rb +199 -0
  181. data/lib/fontisan/variation/cache.rb +298 -0
  182. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  183. data/lib/fontisan/variation/converter.rb +375 -0
  184. data/lib/fontisan/variation/data_extractor.rb +86 -0
  185. data/lib/fontisan/variation/delta_applier.rb +266 -0
  186. data/lib/fontisan/variation/delta_parser.rb +228 -0
  187. data/lib/fontisan/variation/inspector.rb +275 -0
  188. data/lib/fontisan/variation/instance_generator.rb +273 -0
  189. data/lib/fontisan/variation/instance_writer.rb +341 -0
  190. data/lib/fontisan/variation/interpolator.rb +231 -0
  191. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  192. data/lib/fontisan/variation/optimizer.rb +418 -0
  193. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  194. data/lib/fontisan/variation/region_matcher.rb +221 -0
  195. data/lib/fontisan/variation/subsetter.rb +463 -0
  196. data/lib/fontisan/variation/table_accessor.rb +105 -0
  197. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  198. data/lib/fontisan/variation/validator.rb +345 -0
  199. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  200. data/lib/fontisan/variation/variation_context.rb +211 -0
  201. data/lib/fontisan/variation/variation_preserver.rb +288 -0
  202. data/lib/fontisan/version.rb +1 -1
  203. data/lib/fontisan/version.rb.orig +9 -0
  204. data/lib/fontisan/woff2/directory.rb +257 -0
  205. data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
  206. data/lib/fontisan/woff2/header.rb +101 -0
  207. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  208. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  209. data/lib/fontisan/woff2_font.rb +717 -0
  210. data/lib/fontisan/woff_font.rb +488 -0
  211. data/lib/fontisan.rb +132 -0
  212. data/scripts/compare_stack_aware.rb +187 -0
  213. data/scripts/measure_optimization.rb +141 -0
  214. metadata +234 -4
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module Fontisan
6
+ module Export
7
+ # TtxParser parses TTX XML format to font data
8
+ #
9
+ # Parses fonttools-compatible TTX XML files and reconstructs
10
+ # font data that can be written back to binary formats.
11
+ #
12
+ # @example Parsing TTX file
13
+ # parser = TtxParser.new
14
+ # font_data = parser.parse(File.read("font.ttx"))
15
+ # # Use font_data to rebuild binary font
16
+ class TtxParser
17
+ # Parse TTX XML content
18
+ #
19
+ # @param ttx_xml [String] TTX XML content
20
+ # @return [Hash] Parsed font data structure
21
+ def parse(ttx_xml)
22
+ doc = Nokogiri::XML(ttx_xml)
23
+ ttfont = doc.at_xpath("/ttFont")
24
+ raise "No ttFont root element found" unless ttfont
25
+
26
+ {
27
+ sfnt_version: parse_sfnt_version(ttfont["sfntVersion"]),
28
+ glyph_order: parse_glyph_order(ttfont),
29
+ tables: parse_tables(ttfont),
30
+ }
31
+ end
32
+
33
+ private
34
+
35
+ # Parse SFNT version
36
+ #
37
+ # @param version_str [String] Version string
38
+ # @return [Integer] Version integer
39
+ def parse_sfnt_version(version_str)
40
+ # Handle format like "\x00\x01\x00\x00" or "0x00010000"
41
+ if version_str.start_with?("\\x")
42
+ # Parse escaped hex bytes
43
+ bytes = version_str.scan(/\\x([0-9a-f]{2})/i).flatten
44
+ bytes.map { |b| b.to_i(16) }.pack("C*").unpack1("N")
45
+ elsif version_str.start_with?("0x")
46
+ version_str.to_i(16)
47
+ else
48
+ version_str.to_i
49
+ end
50
+ end
51
+
52
+ # Parse GlyphOrder section
53
+ #
54
+ # @param ttfont [Nokogiri::XML::Element] Root element
55
+ # @return [Array<Hash>] Array of glyph entries
56
+ def parse_glyph_order(ttfont)
57
+ glyph_order = ttfont.at_xpath("GlyphOrder")
58
+ return [] unless glyph_order
59
+
60
+ glyph_order.xpath("GlyphID").map do |glyph_id|
61
+ {
62
+ id: glyph_id["id"].to_i,
63
+ name: glyph_id["name"],
64
+ }
65
+ end
66
+ end
67
+
68
+ # Parse all tables
69
+ #
70
+ # @param ttfont [Nokogiri::XML::Element] Root element
71
+ # @return [Hash] Hash of table data by tag
72
+ def parse_tables(ttfont)
73
+ tables = {}
74
+
75
+ # Parse specific tables
76
+ parse_head_table(ttfont, tables)
77
+ parse_hhea_table(ttfont, tables)
78
+ parse_maxp_table(ttfont, tables)
79
+ parse_name_table(ttfont, tables)
80
+ parse_os2_table(ttfont, tables)
81
+ parse_post_table(ttfont, tables)
82
+
83
+ # Parse any remaining binary tables
84
+ parse_binary_tables(ttfont, tables)
85
+
86
+ tables
87
+ end
88
+
89
+ # Parse head table
90
+ #
91
+ # @param ttfont [Nokogiri::XML::Element] Root element
92
+ # @param tables [Hash] Tables hash
93
+ # @return [void]
94
+ def parse_head_table(ttfont, tables)
95
+ head = ttfont.at_xpath("head")
96
+ return unless head
97
+
98
+ tables["head"] = {
99
+ table_version: parse_fixed(head.at_xpath("tableVersion")&.[]("value")),
100
+ font_revision: parse_fixed(head.at_xpath("fontRevision")&.[]("value")),
101
+ checksum_adjustment: parse_hex(head.at_xpath("checkSumAdjustment")&.[]("value")),
102
+ magic_number: parse_hex(head.at_xpath("magicNumber")&.[]("value")),
103
+ flags: head.at_xpath("flags")&.[]("value").to_i,
104
+ units_per_em: head.at_xpath("unitsPerEm")&.[]("value").to_i,
105
+ created: parse_timestamp(head.at_xpath("created")&.[]("value")),
106
+ modified: parse_timestamp(head.at_xpath("modified")&.[]("value")),
107
+ x_min: head.at_xpath("xMin")&.[]("value").to_i,
108
+ y_min: head.at_xpath("yMin")&.[]("value").to_i,
109
+ x_max: head.at_xpath("xMax")&.[]("value").to_i,
110
+ y_max: head.at_xpath("yMax")&.[]("value").to_i,
111
+ mac_style: parse_binary_flags(head.at_xpath("macStyle")&.[]("value")),
112
+ lowest_rec_ppem: head.at_xpath("lowestRecPPEM")&.[]("value").to_i,
113
+ font_direction_hint: head.at_xpath("fontDirectionHint")&.[]("value").to_i,
114
+ index_to_loc_format: head.at_xpath("indexToLocFormat")&.[]("value").to_i,
115
+ glyph_data_format: head.at_xpath("glyphDataFormat")&.[]("value").to_i,
116
+ }
117
+ end
118
+
119
+ # Parse hhea table
120
+ #
121
+ # @param ttfont [Nokogiri::XML::Element] Root element
122
+ # @param tables [Hash] Tables hash
123
+ # @return [void]
124
+ def parse_hhea_table(ttfont, tables)
125
+ hhea = ttfont.at_xpath("hhea")
126
+ return unless hhea
127
+
128
+ tables["hhea"] = {
129
+ table_version: parse_hex(hhea.at_xpath("tableVersion")&.[]("value")),
130
+ ascent: hhea.at_xpath("ascent")&.[]("value").to_i,
131
+ descent: hhea.at_xpath("descent")&.[]("value").to_i,
132
+ line_gap: hhea.at_xpath("lineGap")&.[]("value").to_i,
133
+ advance_width_max: hhea.at_xpath("advanceWidthMax")&.[]("value").to_i,
134
+ min_left_side_bearing: hhea.at_xpath("minLeftSideBearing")&.[]("value").to_i,
135
+ min_right_side_bearing: hhea.at_xpath("minRightSideBearing")&.[]("value").to_i,
136
+ x_max_extent: hhea.at_xpath("xMaxExtent")&.[]("value").to_i,
137
+ caret_slope_rise: hhea.at_xpath("caretSlopeRise")&.[]("value").to_i,
138
+ caret_slope_run: hhea.at_xpath("caretSlopeRun")&.[]("value").to_i,
139
+ caret_offset: hhea.at_xpath("caretOffset")&.[]("value").to_i,
140
+ metric_data_format: hhea.at_xpath("metricDataFormat")&.[]("value").to_i,
141
+ number_of_h_metrics: hhea.at_xpath("numberOfHMetrics")&.[]("value").to_i,
142
+ }
143
+ end
144
+
145
+ # Parse maxp table
146
+ #
147
+ # @param ttfont [Nokogiri::XML::Element] Root element
148
+ # @param tables [Hash] Tables hash
149
+ # @return [void]
150
+ def parse_maxp_table(ttfont, tables)
151
+ maxp = ttfont.at_xpath("maxp")
152
+ return unless maxp
153
+
154
+ tables["maxp"] = {
155
+ table_version: parse_hex(maxp.at_xpath("tableVersion")&.[]("value")),
156
+ num_glyphs: maxp.at_xpath("numGlyphs")&.[]("value").to_i,
157
+ }
158
+
159
+ # Version 1.0 fields
160
+ if tables["maxp"][:table_version] >= 0x00010000
161
+ tables["maxp"].merge!({
162
+ max_points: maxp.at_xpath("maxPoints")&.[]("value")&.to_i,
163
+ max_contours: maxp.at_xpath("maxContours")&.[]("value")&.to_i,
164
+ max_composite_points: maxp.at_xpath("maxCompositePoints")&.[]("value")&.to_i,
165
+ max_composite_contours: maxp.at_xpath("maxCompositeContours")&.[]("value")&.to_i,
166
+ })
167
+ end
168
+ end
169
+
170
+ # Parse name table
171
+ #
172
+ # @param ttfont [Nokogiri::XML::Element] Root element
173
+ # @param tables [Hash] Tables hash
174
+ # @return [void]
175
+ def parse_name_table(ttfont, tables)
176
+ name_elem = ttfont.at_xpath("name")
177
+ return unless name_elem
178
+
179
+ name_records = name_elem.xpath("namerecord").map do |record|
180
+ {
181
+ name_id: record["nameID"].to_i,
182
+ platform_id: record["platformID"].to_i,
183
+ encoding_id: record["platEncID"].to_i,
184
+ language_id: parse_hex(record["langID"]),
185
+ string: record.text,
186
+ }
187
+ end
188
+
189
+ tables["name"] = { name_records: name_records }
190
+ end
191
+
192
+ # Parse OS/2 table (stub)
193
+ #
194
+ # @param ttfont [Nokogiri::XML::Element] Root element
195
+ # @param tables [Hash] Tables hash
196
+ # @return [void]
197
+ def parse_os2_table(ttfont, tables)
198
+ os2 = ttfont.at_xpath("OS/2")
199
+ return unless os2
200
+
201
+ # Basic OS/2 parsing - can be expanded
202
+ tables["OS/2"] = { parsed: false }
203
+ end
204
+
205
+ # Parse post table (stub)
206
+ #
207
+ # @param ttfont [Nokogiri::XML::Element] Root element
208
+ # @param tables [Hash] Tables hash
209
+ # @return [void]
210
+ def parse_post_table(ttfont, tables)
211
+ post = ttfont.at_xpath("post")
212
+ return unless post
213
+
214
+ tables["post"] = {
215
+ format_type: parse_fixed(post.at_xpath("formatType")&.[]("value")),
216
+ italic_angle: parse_fixed(post.at_xpath("italicAngle")&.[]("value")),
217
+ underline_position: post.at_xpath("underlinePosition")&.[]("value").to_i,
218
+ underline_thickness: post.at_xpath("underlineThickness")&.[]("value").to_i,
219
+ is_fixed_pitch: post.at_xpath("isFixedPitch")&.[]("value").to_i,
220
+ }
221
+ end
222
+
223
+ # Parse binary tables (fallback)
224
+ #
225
+ # @param ttfont [Nokogiri::XML::Element] Root element
226
+ # @param tables [Hash] Tables hash
227
+ # @return [void]
228
+ def parse_binary_tables(ttfont, tables)
229
+ # Find all table elements not already parsed
230
+ ttfont.children.each do |elem|
231
+ next unless elem.element?
232
+ next if elem.name == "GlyphOrder"
233
+ next if tables.key?(elem.name)
234
+
235
+ hexdata = elem.at_xpath("hexdata")
236
+ if hexdata
237
+ tables[elem.name] = {
238
+ binary: parse_hex_data(hexdata.text),
239
+ }
240
+ end
241
+ end
242
+ end
243
+
244
+ # Parse fixed-point number
245
+ #
246
+ # @param value_str [String] Fixed-point string
247
+ # @return [Integer] Fixed-point integer (16.16)
248
+ def parse_fixed(value_str)
249
+ return 0 unless value_str
250
+
251
+ (value_str.to_f * 65536).to_i
252
+ end
253
+
254
+ # Parse hex value
255
+ #
256
+ # @param value_str [String] Hex string
257
+ # @return [Integer] Integer value
258
+ def parse_hex(value_str)
259
+ return 0 unless value_str
260
+
261
+ value_str.to_i(16)
262
+ end
263
+
264
+ # Parse binary flags
265
+ #
266
+ # @param flags_str [String] Binary string with spaces
267
+ # @return [Integer] Integer value
268
+ def parse_binary_flags(flags_str)
269
+ return 0 unless flags_str
270
+
271
+ flags_str.gsub(/\s+/, "").to_i(2)
272
+ end
273
+
274
+ # Parse timestamp
275
+ #
276
+ # @param timestamp_str [String] Timestamp string
277
+ # @return [Integer] Mac timestamp
278
+ def parse_timestamp(timestamp_str)
279
+ return 0 unless timestamp_str
280
+
281
+ begin
282
+ time = Time.strptime(timestamp_str, "%a %b %e %H:%M:%S %Y")
283
+ mac_epoch = Time.utc(1904, 1, 1)
284
+ (time - mac_epoch).to_i
285
+ rescue StandardError
286
+ 0
287
+ end
288
+ end
289
+
290
+ # Parse hex data
291
+ #
292
+ # @param hex_str [String] Hex string with newlines
293
+ # @return [String] Binary data
294
+ def parse_hex_data(hex_str)
295
+ hex_clean = hex_str.gsub(/\s+/, "")
296
+ [hex_clean].pack("H*")
297
+ end
298
+ end
299
+ end
300
+ end
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "constants"
4
+ require_relative "loading_modes"
4
5
  require_relative "true_type_font"
5
6
  require_relative "open_type_font"
6
7
  require_relative "true_type_collection"
7
8
  require_relative "open_type_collection"
9
+ require_relative "woff_font"
10
+ require_relative "woff2_font"
8
11
  require_relative "error"
9
12
 
10
13
  module Fontisan
@@ -19,50 +22,154 @@ module Fontisan
19
22
  # font = FontLoader.load("font.otf") # => OpenTypeFont
20
23
  # font = FontLoader.load("fonts.ttc") # => TrueTypeFont (first in collection)
21
24
  # font = FontLoader.load("fonts.ttc", font_index: 2) # => TrueTypeFont (third in collection)
25
+ #
26
+ # @example Loading modes
27
+ # font = FontLoader.load("font.ttf", mode: :metadata) # Load only metadata tables
28
+ # font = FontLoader.load("font.ttf", mode: :full) # Load all tables
29
+ #
30
+ # @example Lazy loading control
31
+ # font = FontLoader.load("font.ttf", lazy: true) # Tables loaded on-demand
32
+ # font = FontLoader.load("font.ttf", lazy: false) # All tables loaded upfront
22
33
  class FontLoader
23
34
  # Load a font from file with automatic format detection
24
35
  #
25
36
  # @param path [String] Path to the font file
26
37
  # @param font_index [Integer] Index of font in collection (0-based, default: 0)
27
- # @return [TrueTypeFont, OpenTypeFont] The loaded font object
38
+ # @param mode [Symbol] Loading mode (:metadata or :full, default: from ENV or :full)
39
+ # @param lazy [Boolean] If true, load tables on demand (default: false for eager loading)
40
+ # @return [TrueTypeFont, OpenTypeFont, WoffFont, Woff2Font] The loaded font object
28
41
  # @raise [Errno::ENOENT] if file does not exist
29
- # @raise [UnsupportedFormatError] for WOFF/WOFF2 or other unsupported formats
42
+ # @raise [UnsupportedFormatError] for unsupported formats
30
43
  # @raise [InvalidFontError] for corrupted or unknown formats
31
- def self.load(path, font_index: 0)
44
+ def self.load(path, font_index: 0, mode: nil, lazy: nil)
32
45
  raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
33
46
 
47
+ # Resolve mode and lazy parameters with environment variables
48
+ resolved_mode = mode || env_mode || LoadingModes::FULL
49
+ resolved_lazy = lazy.nil? ? (env_lazy.nil? ? false : env_lazy) : lazy
50
+
51
+ # Validate mode
52
+ LoadingModes.validate_mode!(resolved_mode)
53
+
34
54
  File.open(path, "rb") do |io|
35
55
  signature = io.read(4)
36
56
  io.rewind
37
57
 
38
58
  case signature
39
59
  when Constants::TTC_TAG
40
- load_from_collection(io, path, font_index)
60
+ load_from_collection(io, path, font_index, mode: resolved_mode, lazy: resolved_lazy)
41
61
  when pack_uint32(Constants::SFNT_VERSION_TRUETYPE)
42
- TrueTypeFont.from_file(path)
62
+ TrueTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
43
63
  when "OTTO"
44
- OpenTypeFont.from_file(path)
64
+ OpenTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
45
65
  when "wOFF"
46
- raise UnsupportedFormatError,
47
- "Unsupported font format: WOFF. Fontisan currently supports TTF, OTF, TTC, and OTC files."
66
+ WoffFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
48
67
  when "wOF2"
49
- raise UnsupportedFormatError,
50
- "Unsupported font format: WOFF2. Fontisan currently supports TTF, OTF, TTC, and OTC files."
68
+ Woff2Font.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
69
+ else
70
+ raise InvalidFontError,
71
+ "Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, or WOFF2 file."
72
+ end
73
+ end
74
+ end
75
+
76
+ # Check if a file is a collection (TTC or OTC)
77
+ #
78
+ # @param path [String] Path to the font file
79
+ # @return [Boolean] true if file is a TTC/OTC collection
80
+ # @raise [Errno::ENOENT] if file does not exist
81
+ #
82
+ # @example Check if file is collection
83
+ # FontLoader.collection?("fonts.ttc") # => true
84
+ # FontLoader.collection?("font.ttf") # => false
85
+ def self.collection?(path)
86
+ raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
87
+
88
+ File.open(path, "rb") do |io|
89
+ signature = io.read(4)
90
+ signature == Constants::TTC_TAG
91
+ end
92
+ end
93
+
94
+ # Load a collection object without extracting fonts
95
+ #
96
+ # Returns the collection object (TrueTypeCollection or OpenTypeCollection)
97
+ # without extracting individual fonts. Useful for inspecting collection
98
+ # metadata and structure.
99
+ #
100
+ # @param path [String] Path to the collection file
101
+ # @return [TrueTypeCollection, OpenTypeCollection] The collection object
102
+ # @raise [Errno::ENOENT] if file does not exist
103
+ # @raise [InvalidFontError] if file is not a collection or type cannot be determined
104
+ #
105
+ # @example Load collection for inspection
106
+ # collection = FontLoader.load_collection("fonts.ttc")
107
+ # puts "Collection has #{collection.num_fonts} fonts"
108
+ def self.load_collection(path)
109
+ raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
110
+
111
+ File.open(path, "rb") do |io|
112
+ signature = io.read(4)
113
+
114
+ unless signature == Constants::TTC_TAG
115
+ raise InvalidFontError,
116
+ "File is not a collection (TTC/OTC). Use FontLoader.load instead."
117
+ end
118
+
119
+ # Read first font offset to detect collection type
120
+ io.seek(12) # Skip tag (4) + versions (4) + num_fonts (4)
121
+ first_offset = io.read(4).unpack1("N")
122
+
123
+ # Peek at first font's sfnt_version
124
+ io.seek(first_offset)
125
+ sfnt_version = io.read(4).unpack1("N")
126
+ io.rewind
127
+
128
+ case sfnt_version
129
+ when Constants::SFNT_VERSION_TRUETYPE
130
+ TrueTypeCollection.from_file(path)
131
+ when Constants::SFNT_VERSION_OTTO
132
+ OpenTypeCollection.from_file(path)
51
133
  else
52
134
  raise InvalidFontError,
53
- "Unknown font format. Expected TTF, OTF, TTC, or OTC file."
135
+ "Unknown font type in collection (sfnt version: 0x#{sfnt_version.to_s(16)})"
54
136
  end
55
137
  end
56
138
  end
57
139
 
140
+ # Get mode from environment variable
141
+ #
142
+ # @return [Symbol, nil] Mode from FONTISAN_MODE or nil
143
+ # @api private
144
+ def self.env_mode
145
+ env_value = ENV["FONTISAN_MODE"]
146
+ return nil unless env_value
147
+
148
+ mode = env_value.to_sym
149
+ LoadingModes.valid_mode?(mode) ? mode : nil
150
+ end
151
+
152
+ # Get lazy setting from environment variable
153
+ #
154
+ # @return [Boolean, nil] Lazy setting from FONTISAN_LAZY or nil if not set
155
+ # @api private
156
+ def self.env_lazy
157
+ env_value = ENV["FONTISAN_LAZY"]
158
+ return nil unless env_value
159
+
160
+ env_value.downcase == "true"
161
+ end
162
+
58
163
  # Load from a collection file (TTC or OTC)
59
164
  #
60
165
  # @param io [IO] Open file handle
61
166
  # @param path [String] Path to the collection file
62
167
  # @param font_index [Integer] Index of font to extract
168
+ # @param mode [Symbol] Loading mode (:metadata or :full)
169
+ # @param lazy [Boolean] If true, load tables on demand
63
170
  # @return [TrueTypeFont, OpenTypeFont] The loaded font object
64
171
  # @raise [InvalidFontError] if collection type cannot be determined
65
- def self.load_from_collection(io, path, font_index)
172
+ def self.load_from_collection(io, path, font_index, mode: LoadingModes::FULL, lazy: true)
66
173
  # Read collection header to get font offsets
67
174
  io.seek(12) # Skip tag (4) + major_version (2) + minor_version (2) + num_fonts marker (4)
68
175
  num_fonts = io.read(4).unpack1("N")
@@ -84,11 +191,11 @@ module Fontisan
84
191
  when Constants::SFNT_VERSION_TRUETYPE
85
192
  # TrueType Collection
86
193
  ttc = TrueTypeCollection.from_file(path)
87
- File.open(path, "rb") { |f| ttc.font(font_index, f) }
194
+ File.open(path, "rb") { |f| ttc.font(font_index, f, mode: mode) }
88
195
  when Constants::SFNT_VERSION_OTTO
89
196
  # OpenType Collection
90
197
  otc = OpenTypeCollection.from_file(path)
91
- File.open(path, "rb") { |f| otc.font(font_index, f) }
198
+ File.open(path, "rb") { |f| otc.font(font_index, f, mode: mode) }
92
199
  else
93
200
  raise InvalidFontError,
94
201
  "Unknown font type in collection (sfnt version: 0x#{sfnt_version.to_s(16)})"
@@ -104,6 +211,6 @@ module Fontisan
104
211
  [value].pack("N")
105
212
  end
106
213
 
107
- private_class_method :load_from_collection, :pack_uint32
214
+ private_class_method :load_from_collection, :pack_uint32, :env_mode, :env_lazy
108
215
  end
109
216
  end