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,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../models/hint"
4
+
5
+ module Fontisan
6
+ module Hints
7
+ # Extracts rendering hints from TrueType glyph data
8
+ #
9
+ # TrueType uses bytecode instructions for hinting. This extractor
10
+ # analyzes glyph instruction sequences and converts them into
11
+ # universal Hint objects for format-agnostic representation.
12
+ #
13
+ # **Supported TrueType Instructions:**
14
+ #
15
+ # - MDAP - Move Direct Absolute Point (stem positioning)
16
+ # - MDRP - Move Direct Relative Point (stem width)
17
+ # - IUP - Interpolate Untouched Points (smooth interpolation)
18
+ # - SHP - Shift Point (point adjustments)
19
+ # - ALIGNRP - Align to Reference Point (alignment)
20
+ # - DELTA - Delta instructions (pixel-level adjustments)
21
+ #
22
+ # @example Extract hints from a glyph
23
+ # extractor = TrueTypeHintExtractor.new
24
+ # hints = extractor.extract(glyph)
25
+ class TrueTypeHintExtractor
26
+ # TrueType instruction opcodes
27
+ MDAP_RND = 0x2E
28
+ MDAP_NORND = 0x2F
29
+ MDRP_MIN_RND_BLACK = 0xC0
30
+ IUP_Y = 0x30
31
+ IUP_X = 0x31
32
+ SHP = [0x32, 0x33]
33
+ ALIGNRP = 0x3C
34
+ DELTAP1 = 0x5D
35
+ DELTAP2 = 0x71
36
+ DELTAP3 = 0x72
37
+
38
+ # Extract complete hint data from TrueType font
39
+ #
40
+ # This extracts both font-level hints (fpgm, prep, cvt tables) and
41
+ # per-glyph hints from glyph instructions.
42
+ #
43
+ # @param font [TrueTypeFont] TrueType font object
44
+ # @return [Models::HintSet] Complete hint set
45
+ def extract_from_font(font)
46
+ hint_set = Models::HintSet.new(format: "truetype")
47
+
48
+ # Extract font-level programs
49
+ hint_set.font_program = extract_font_program(font)
50
+ hint_set.control_value_program = extract_control_value_program(font)
51
+ hint_set.control_values = extract_control_values(font)
52
+
53
+ # Extract per-glyph hints
54
+ extract_glyph_hints(font, hint_set)
55
+
56
+ # Update metadata
57
+ hint_set.has_hints = !hint_set.empty?
58
+
59
+ hint_set
60
+ end
61
+
62
+ # Extract hints from TrueType glyph
63
+ #
64
+ # @param glyph [Glyph] TrueType glyph with instructions
65
+ # @return [Array<Hint>] Extracted hints
66
+ def extract(glyph)
67
+ return [] if glyph.nil? || glyph.empty?
68
+ return [] unless glyph.respond_to?(:instructions)
69
+
70
+ instructions = glyph.instructions || []
71
+ return [] if instructions.empty?
72
+
73
+ parse_instructions(instructions)
74
+ end
75
+
76
+ private
77
+
78
+ # Parse TrueType instruction bytes into Hint objects
79
+ #
80
+ # @param instructions [String, Array<Integer>] Instruction bytes
81
+ # @return [Array<Hint>] Parsed hints
82
+ def parse_instructions(instructions)
83
+ hints = []
84
+ bytes = instructions.is_a?(String) ? instructions.bytes : instructions
85
+ i = 0
86
+
87
+ while i < bytes.length
88
+ opcode = bytes[i]
89
+
90
+ case opcode
91
+ when MDAP_RND, MDAP_NORND
92
+ # Stem positioning hint
93
+ hint = extract_stem_hint(bytes, i)
94
+ hints << hint if hint
95
+ i += 1
96
+
97
+ when MDRP_MIN_RND_BLACK
98
+ # Stem width hint (usually follows MDAP)
99
+ # This is typically part of a stem hint pair
100
+ i += 1
101
+
102
+ when IUP_Y, IUP_X
103
+ # Interpolation hint
104
+ hints << Models::Hint.new(
105
+ type: :interpolate,
106
+ data: { axis: opcode == IUP_Y ? :y : :x },
107
+ source_format: :truetype
108
+ )
109
+ i += 1
110
+
111
+ when *SHP
112
+ # Shift point hint
113
+ hints << Models::Hint.new(
114
+ type: :shift,
115
+ data: { instructions: [opcode] },
116
+ source_format: :truetype
117
+ )
118
+ i += 1
119
+
120
+ when ALIGNRP
121
+ # Alignment hint
122
+ hints << Models::Hint.new(
123
+ type: :align,
124
+ data: {},
125
+ source_format: :truetype
126
+ )
127
+ i += 1
128
+
129
+ when DELTAP1, DELTAP2, DELTAP3
130
+ # Delta hint - pixel-level adjustments
131
+ # Next byte is the count
132
+ i += 1
133
+ if i < bytes.length
134
+ count = bytes[i]
135
+ delta_data = bytes[i + 1, count * 2] || []
136
+ hints << Models::Hint.new(
137
+ type: :delta,
138
+ data: {
139
+ instructions: [opcode] + [count] + delta_data,
140
+ count: count
141
+ },
142
+ source_format: :truetype
143
+ )
144
+ i += count * 2 + 1
145
+ end
146
+
147
+ else
148
+ # Unknown or data bytes - skip
149
+ i += 1
150
+ end
151
+ end
152
+
153
+ hints
154
+ end
155
+
156
+ # Extract stem hint from MDAP instruction
157
+ #
158
+ # @param bytes [Array<Integer>] Instruction bytes
159
+ # @param index [Integer] Current position
160
+ # @return [Hint, nil] Stem hint if found
161
+ def extract_stem_hint(bytes, index)
162
+ # In TrueType, stem hints are inferred from MDAP + MDRP pairs
163
+ # This is a simplified extraction - real implementation would
164
+ # need to track the graphics state and point references
165
+
166
+ # Check if next instruction is MDRP (stem width)
167
+ has_width = index + 1 < bytes.length &&
168
+ bytes[index + 1] == MDRP_MIN_RND_BLACK
169
+
170
+ if has_width
171
+ Models::Hint.new(
172
+ type: :stem,
173
+ data: {
174
+ position: 0, # Would be extracted from graphics state
175
+ width: 0, # Would be calculated from MDRP
176
+ orientation: :vertical # Inferred from instruction context
177
+ },
178
+ source_format: :truetype
179
+ )
180
+ else
181
+ nil
182
+ end
183
+ end
184
+
185
+ # Extract font program (fpgm table)
186
+ #
187
+ # @param font [TrueTypeFont] TrueType font
188
+ # @return [String] Font program bytecode (binary string)
189
+ def extract_font_program(font)
190
+ return "" unless font.has_table?("fpgm")
191
+
192
+ font_program_data = font.instance_variable_get(:@table_data)["fpgm"]
193
+ return "" unless font_program_data
194
+
195
+ # Return as binary string
196
+ font_program_data.force_encoding("ASCII-8BIT")
197
+ rescue StandardError => e
198
+ warn "Failed to extract font program: #{e.message}"
199
+ ""
200
+ end
201
+
202
+ # Extract control value program (prep table)
203
+ #
204
+ # @param font [TrueTypeFont] TrueType font
205
+ # @return [String] Control value program bytecode (binary string)
206
+ def extract_control_value_program(font)
207
+ return "" unless font.has_table?("prep")
208
+
209
+ prep_data = font.instance_variable_get(:@table_data)["prep"]
210
+ return "" unless prep_data
211
+
212
+ # Return as binary string
213
+ prep_data.force_encoding("ASCII-8BIT")
214
+ rescue StandardError => e
215
+ warn "Failed to extract control value program: #{e.message}"
216
+ ""
217
+ end
218
+
219
+ # Extract control values (cvt table)
220
+ #
221
+ # @param font [TrueTypeFont] TrueType font
222
+ # @return [Array<Integer>] Control values
223
+ def extract_control_values(font)
224
+ return [] unless font.has_table?("cvt ")
225
+
226
+ cvt_data = font.instance_variable_get(:@table_data)["cvt "]
227
+ return [] unless cvt_data
228
+
229
+ # CVT table is an array of 16-bit signed integers (FWord values)
230
+ values = []
231
+ io = StringIO.new(cvt_data)
232
+ while !io.eof?
233
+ # Read 16-bit big-endian signed integer
234
+ bytes = io.read(2)
235
+ break unless bytes&.length == 2
236
+
237
+ value = bytes.unpack1("n") # Unsigned short
238
+ # Convert to signed
239
+ value = value - 65536 if value > 32767
240
+ values << value
241
+ end
242
+
243
+ values
244
+ rescue StandardError => e
245
+ warn "Failed to extract control values: #{e.message}"
246
+ []
247
+ end
248
+
249
+ # Extract per-glyph hints from glyf table
250
+ #
251
+ # @param font [TrueTypeFont] TrueType font
252
+ # @param hint_set [Models::HintSet] Hint set to populate
253
+ # @return [void]
254
+ def extract_glyph_hints(font, hint_set)
255
+ return unless font.has_table?("glyf")
256
+
257
+ glyf_table = font.table("glyf")
258
+ return unless glyf_table
259
+
260
+ # Get number of glyphs from maxp table
261
+ maxp_table = font.table("maxp")
262
+ return unless maxp_table
263
+
264
+ num_glyphs = maxp_table.num_glyphs
265
+
266
+ # Iterate through all glyphs
267
+ (0...num_glyphs).each do |glyph_id|
268
+ begin
269
+ glyph = glyf_table.glyph_for(glyph_id)
270
+ next unless glyph
271
+ next if glyph.number_of_contours <= 0 # Skip compound glyphs and empty glyphs
272
+
273
+ # Extract hints from simple glyph instructions
274
+ hints = extract(glyph)
275
+ next if hints.empty?
276
+
277
+ # Store glyph hints
278
+ hint_set.add_glyph_hints(glyph_id, hints)
279
+ rescue StandardError => e
280
+ # Skip glyphs that fail to parse
281
+ next
282
+ end
283
+ end
284
+ rescue StandardError => e
285
+ warn "Failed to extract glyph hints: #{e.message}"
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Fontisan
6
+ # Loading modes module that defines which tables are loaded in each mode.
7
+ #
8
+ # This module provides a MECE (Mutually Exclusive, Collectively Exhaustive)
9
+ # architecture for font loading modes. Each mode defines a specific set of
10
+ # tables to load, enabling efficient parsing for different use cases.
11
+ #
12
+ # @example Using metadata mode
13
+ # mode = LoadingModes::METADATA
14
+ # tables = LoadingModes.tables_for(mode) # => ["name", "head", "hhea", "maxp", "OS/2", "post"]
15
+ #
16
+ # @example Checking table availability
17
+ # LoadingModes.table_allowed?(:metadata, "GSUB") # => false
18
+ # LoadingModes.table_allowed?(:full, "GSUB") # => true
19
+ module LoadingModes
20
+ # Metadata mode: loads only tables needed for font identification and metrics
21
+ # Equivalent to otfinfo functionality
22
+ METADATA = :metadata
23
+
24
+ # Full mode: loads all tables in the font
25
+ FULL = :full
26
+
27
+ # Mode definitions with their respective table lists
28
+ MODES = {
29
+ METADATA => {
30
+ tables: %w[name head hhea maxp OS/2 post].freeze,
31
+ description: "Metadata mode - loads only identification and metrics tables (otfinfo-equivalent)"
32
+ }.freeze,
33
+ FULL => {
34
+ tables: :all,
35
+ description: "Full mode - loads all tables in the font"
36
+ }.freeze
37
+ }.freeze
38
+
39
+ # Pre-computed Set for O(1) lookup of metadata tables
40
+ # This constant avoids recreating the Set on every font load
41
+ METADATA_TABLES_SET = MODES[METADATA][:tables].to_set.freeze
42
+
43
+ # Get the list of tables allowed for a given mode
44
+ #
45
+ # @param mode [Symbol] The loading mode (:metadata or :full)
46
+ # @return [Array<String>, Symbol] Array of table tags or :all for full mode
47
+ # @raise [ArgumentError] if mode is invalid
48
+ def self.tables_for(mode)
49
+ validate_mode!(mode)
50
+ MODES[mode][:tables]
51
+ end
52
+
53
+ # Check if a table is allowed in a given mode
54
+ #
55
+ # @param mode [Symbol] The loading mode (:metadata or :full)
56
+ # @param tag [String] The table tag to check
57
+ # @return [Boolean] true if table is allowed in the mode
58
+ # @raise [ArgumentError] if mode is invalid
59
+ def self.table_allowed?(mode, tag)
60
+ validate_mode!(mode)
61
+
62
+ tables = MODES[mode][:tables]
63
+ return true if tables == :all
64
+
65
+ tables.include?(tag)
66
+ end
67
+
68
+ # Validate that a mode is valid
69
+ #
70
+ # @param mode [Symbol] The mode to validate
71
+ # @return [Boolean] true if mode is valid
72
+ def self.valid_mode?(mode)
73
+ MODES.key?(mode)
74
+ end
75
+
76
+ # Get the default lazy loading setting for a mode
77
+ #
78
+ # @param mode [Symbol] The loading mode
79
+ # @return [Boolean] true if lazy loading is recommended for this mode
80
+ # @raise [ArgumentError] if mode is invalid
81
+ def self.default_lazy?(mode)
82
+ validate_mode!(mode)
83
+ true # Lazy loading is recommended for all modes
84
+ end
85
+
86
+ # Get mode description
87
+ #
88
+ # @param mode [Symbol] The loading mode
89
+ # @return [String] Description of the mode
90
+ # @raise [ArgumentError] if mode is invalid
91
+ def self.description(mode)
92
+ validate_mode!(mode)
93
+ MODES[mode][:description]
94
+ end
95
+
96
+ # Get all available modes
97
+ #
98
+ # @return [Array<Symbol>] List of all mode symbols
99
+ def self.all_modes
100
+ MODES.keys
101
+ end
102
+
103
+ # Validate mode and raise error if invalid
104
+ #
105
+ # @param mode [Symbol] The mode to validate
106
+ # @return [void]
107
+ # @raise [ArgumentError] if mode is invalid
108
+ def self.validate_mode!(mode)
109
+ return if valid_mode?(mode)
110
+
111
+ raise ArgumentError,
112
+ "Invalid mode: #{mode.inspect}. Valid modes are: #{all_modes.map(&:inspect).join(', ')}"
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+
5
+ module Fontisan
6
+ # High-level utility class for accessing font metrics
7
+ #
8
+ # MetricsCalculator provides a convenient API for querying font metrics from
9
+ # multiple OpenType tables without needing to work with the low-level table
10
+ # structures directly. It wraps access to hhea, hmtx, head, maxp, and cmap
11
+ # tables.
12
+ #
13
+ # The calculator handles missing tables gracefully and provides both
14
+ # individual glyph metrics and string-level calculations.
15
+ #
16
+ # @example Basic usage
17
+ # font = FontLoader.from_file("path/to/font.ttf")
18
+ # calc = MetricsCalculator.new(font)
19
+ #
20
+ # puts calc.ascent # => 2048
21
+ # puts calc.descent # => -512
22
+ # puts calc.line_height # => 2650
23
+ # puts calc.units_per_em # => 2048
24
+ #
25
+ # @example Glyph metrics
26
+ # width = calc.glyph_width(42)
27
+ # lsb = calc.glyph_left_side_bearing(42)
28
+ #
29
+ # @example String width calculation
30
+ # width = calc.string_width("Hello")
31
+ #
32
+ # @example Checking for metrics support
33
+ # if calc.has_metrics?
34
+ # puts "Font has complete horizontal metrics"
35
+ # end
36
+ class MetricsCalculator
37
+ # The font object this calculator operates on
38
+ #
39
+ # @return [OpenTypeFont, TrueTypeFont] The font instance
40
+ attr_reader :font
41
+
42
+ # Initialize a new MetricsCalculator
43
+ #
44
+ # @param font [OpenTypeFont, TrueTypeFont] Font instance to calculate metrics for
45
+ # @raise [ArgumentError] if font is nil
46
+ def initialize(font)
47
+ raise ArgumentError, "Font cannot be nil" if font.nil?
48
+
49
+ @font = font
50
+ @hhea_table = nil
51
+ @hmtx_table = nil
52
+ @head_table = nil
53
+ @maxp_table = nil
54
+ @cmap_table = nil
55
+ @hmtx_parsed = false
56
+ end
57
+
58
+ # Get typographic ascent from hhea table
59
+ #
60
+ # The ascent is the distance from the baseline to the highest ascender.
61
+ # It is a positive value in font units (FUnits).
62
+ #
63
+ # @return [Integer, nil] Ascent value in FUnits, or nil if hhea table is missing
64
+ #
65
+ # @example
66
+ # calc.ascent # => 2048
67
+ def ascent
68
+ hhea&.ascent
69
+ end
70
+
71
+ # Get typographic descent from hhea table
72
+ #
73
+ # The descent is the distance from the baseline to the lowest descender.
74
+ # It is typically a negative value in font units (FUnits).
75
+ #
76
+ # @return [Integer, nil] Descent value in FUnits, or nil if hhea table is missing
77
+ #
78
+ # @example
79
+ # calc.descent # => -512
80
+ def descent
81
+ hhea&.descent
82
+ end
83
+
84
+ # Get line gap from hhea table
85
+ #
86
+ # The line gap is additional vertical space between lines of text.
87
+ # It is a non-negative value in font units (FUnits).
88
+ #
89
+ # @return [Integer, nil] Line gap value in FUnits, or nil if hhea table is missing
90
+ #
91
+ # @example
92
+ # calc.line_gap # => 90
93
+ def line_gap
94
+ hhea&.line_gap
95
+ end
96
+
97
+ # Get units per em from head table
98
+ #
99
+ # This value defines the font's coordinate system scale. Common values
100
+ # are 1000 (PostScript fonts) or 2048 (TrueType fonts).
101
+ #
102
+ # @return [Integer, nil] Units per em value, or nil if head table is missing
103
+ #
104
+ # @example
105
+ # calc.units_per_em # => 2048
106
+ def units_per_em
107
+ head&.units_per_em
108
+ end
109
+
110
+ # Get advance width for a specific glyph
111
+ #
112
+ # The advance width is the horizontal distance to advance the pen position
113
+ # after rendering this glyph. It is in font units (FUnits).
114
+ #
115
+ # @param glyph_id [Integer] The glyph ID (0-based)
116
+ # @return [Integer, nil] Advance width in FUnits, or nil if not available
117
+ #
118
+ # @example
119
+ # calc.glyph_width(42) # => 1234
120
+ def glyph_width(glyph_id)
121
+ ensure_hmtx_parsed
122
+ return nil unless hmtx
123
+
124
+ metric = hmtx.metric_for(glyph_id)
125
+ metric&.dig(:advance_width)
126
+ end
127
+
128
+ # Alias for {#glyph_width}
129
+ #
130
+ # @param glyph_id [Integer] The glyph ID (0-based)
131
+ # @return [Integer, nil] Advance width in FUnits, or nil if not available
132
+ alias glyph_advance_width glyph_width
133
+
134
+ # Get left side bearing for a specific glyph
135
+ #
136
+ # The left side bearing (LSB) is the horizontal distance from the pen
137
+ # position to the leftmost point of the glyph. It can be negative if
138
+ # the glyph extends to the left of the pen position.
139
+ #
140
+ # @param glyph_id [Integer] The glyph ID (0-based)
141
+ # @return [Integer, nil] Left side bearing in FUnits, or nil if not available
142
+ #
143
+ # @example
144
+ # calc.glyph_left_side_bearing(42) # => 50
145
+ def glyph_left_side_bearing(glyph_id)
146
+ ensure_hmtx_parsed
147
+ return nil unless hmtx
148
+
149
+ metric = hmtx.metric_for(glyph_id)
150
+ metric&.dig(:lsb)
151
+ end
152
+
153
+ # Calculate total width for a string
154
+ #
155
+ # Calculates the sum of advance widths for all characters in the string.
156
+ # This is a simplified calculation that does not account for kerning,
157
+ # ligatures, or other advanced typography features.
158
+ #
159
+ # Characters not mapped in the font are skipped.
160
+ #
161
+ # @param string [String] The string to measure
162
+ # @return [Integer, nil] Total width in FUnits, or nil if metrics unavailable
163
+ #
164
+ # @example
165
+ # calc.string_width("Hello") # => 5420
166
+ def string_width(string)
167
+ return nil unless has_metrics?
168
+ return 0 if string.nil? || string.empty?
169
+
170
+ total_width = 0
171
+ string.each_codepoint do |codepoint|
172
+ glyph_id = codepoint_to_glyph_id(codepoint)
173
+ next unless glyph_id
174
+
175
+ width = glyph_width(glyph_id)
176
+ total_width += width if width
177
+ end
178
+
179
+ total_width
180
+ end
181
+
182
+ # Calculate line height
183
+ #
184
+ # Line height is calculated as: ascent - descent + line_gap
185
+ # This represents the recommended spacing between consecutive baselines.
186
+ #
187
+ # @return [Integer, nil] Line height in FUnits, or nil if hhea table is missing
188
+ #
189
+ # @example
190
+ # calc.line_height # => 2650 (when ascent=2048, descent=-512, line_gap=90)
191
+ def line_height
192
+ return nil unless hhea
193
+
194
+ ascent - descent + line_gap
195
+ end
196
+
197
+ # Alias for {#units_per_em}
198
+ #
199
+ # @return [Integer, nil] Units per em value, or nil if head table is missing
200
+ alias em_height units_per_em
201
+
202
+ # Check if font has complete horizontal metrics
203
+ #
204
+ # Returns true if the font has all required tables for horizontal metrics:
205
+ # hhea, hmtx, head, and maxp tables.
206
+ #
207
+ # @return [Boolean] True if all metrics tables are present
208
+ #
209
+ # @example
210
+ # calc.has_metrics? # => true
211
+ def has_metrics?
212
+ !hhea.nil? && !hmtx.nil? && !head.nil? && !maxp.nil?
213
+ end
214
+
215
+ private
216
+
217
+ # Get hhea table, caching the result
218
+ #
219
+ # @return [Tables::Hhea, nil] The hhea table or nil
220
+ def hhea
221
+ @hhea ||= font.table(Constants::HHEA_TAG)
222
+ end
223
+
224
+ # Get hmtx table, caching the result
225
+ #
226
+ # @return [Tables::Hmtx, nil] The hmtx table or nil
227
+ def hmtx
228
+ @hmtx ||= font.table(Constants::HMTX_TAG)
229
+ end
230
+
231
+ # Get head table, caching the result
232
+ #
233
+ # @return [Tables::Head, nil] The head table or nil
234
+ def head
235
+ @head ||= font.table(Constants::HEAD_TAG)
236
+ end
237
+
238
+ # Get maxp table, caching the result
239
+ #
240
+ # @return [Tables::Maxp, nil] The maxp table or nil
241
+ def maxp
242
+ @maxp ||= font.table(Constants::MAXP_TAG)
243
+ end
244
+
245
+ # Get cmap table, caching the result
246
+ #
247
+ # @return [Tables::Cmap, nil] The cmap table or nil
248
+ def cmap
249
+ @cmap ||= font.table(Constants::CMAP_TAG)
250
+ end
251
+
252
+ # Ensure hmtx table is parsed with context
253
+ #
254
+ # The hmtx table requires numberOfHMetrics from hhea and numGlyphs from maxp
255
+ # to be parsed correctly. This method ensures parsing happens lazily on first use.
256
+ #
257
+ # @return [void]
258
+ def ensure_hmtx_parsed
259
+ return if @hmtx_parsed
260
+ return unless hmtx && hhea && maxp
261
+
262
+ hmtx.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
263
+ @hmtx_parsed = true
264
+ end
265
+
266
+ # Map Unicode codepoint to glyph ID using cmap table
267
+ #
268
+ # @param codepoint [Integer] Unicode codepoint
269
+ # @return [Integer, nil] Glyph ID or nil if not mapped
270
+ def codepoint_to_glyph_id(codepoint)
271
+ return nil unless cmap
272
+
273
+ mappings = cmap.unicode_mappings
274
+ mappings[codepoint]
275
+ end
276
+ end
277
+ end