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,405 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # Container for all font hint data
8
+ #
9
+ # This model holds complete hint information from a font including
10
+ # font-level programs, control values, and per-glyph hints. It provides
11
+ # a format-agnostic representation that can be converted between
12
+ # TrueType and PostScript formats.
13
+ #
14
+ # @example Creating a HintSet
15
+ # hint_set = HintSet.new(
16
+ # format: :truetype,
17
+ # font_program: fpgm_data,
18
+ # control_value_program: prep_data
19
+ # )
20
+ class HintSet
21
+ # @return [String] Hint format (:truetype or :postscript)
22
+ attr_accessor :format
23
+
24
+ # TrueType font-level hint data
25
+ # @return [String] Font program (fpgm table) - bytecode executed once
26
+ attr_accessor :font_program
27
+
28
+ # @return [String] Control value program (prep table) - initialization code
29
+ attr_accessor :control_value_program
30
+
31
+ # @return [Array<Integer>] Control values (cvt table) - metrics for hinting
32
+ attr_accessor :control_values
33
+
34
+ # PostScript font-level hint data
35
+ # @return [String] CFF Private dict hint data (BlueValues, StdHW, etc.) as JSON
36
+ attr_accessor :private_dict_hints
37
+
38
+ # @return [Integer] Number of glyphs with hints
39
+ attr_accessor :hinted_glyph_count
40
+
41
+ # @return [Boolean] Whether hints are present
42
+ attr_accessor :has_hints
43
+
44
+ # Initialize a new HintSet
45
+ #
46
+ # @param format [String, Symbol] Hint format (:truetype or :postscript)
47
+ # @param font_program [String] Font program bytecode
48
+ # @param control_value_program [String] Control value program bytecode
49
+ # @param control_values [Array<Integer>] Control values
50
+ # @param private_dict_hints [String] Private dict hints as JSON
51
+ # @param hinted_glyph_count [Integer] Number of hinted glyphs
52
+ # @param has_hints [Boolean] Whether hints are present
53
+ def initialize(format: nil, font_program: "", control_value_program: "",
54
+ control_values: [], private_dict_hints: "{}",
55
+ hinted_glyph_count: 0, has_hints: false)
56
+ @format = format.to_s if format
57
+ @font_program = font_program || ""
58
+ @control_value_program = control_value_program || ""
59
+ @control_values = control_values || []
60
+ @private_dict_hints = private_dict_hints || "{}"
61
+ @glyph_hints = "{}"
62
+ @hinted_glyph_count = hinted_glyph_count
63
+ @has_hints = has_hints
64
+ end
65
+
66
+ # Add hints for a specific glyph
67
+ #
68
+ # @param glyph_id [Integer, String] Glyph identifier
69
+ # @param hints [Array<Hint>] Hints for the glyph
70
+ def add_glyph_hints(glyph_id, hints)
71
+ return if hints.nil? || hints.empty?
72
+
73
+ glyph_hints_hash = parse_glyph_hints
74
+ # Convert Hint objects to hashes for storage
75
+ hints_data = hints.map do |h|
76
+ {
77
+ type: h.type,
78
+ data: h.data,
79
+ source_format: h.source_format
80
+ }
81
+ end
82
+ glyph_hints_hash[glyph_id.to_s] = hints_data
83
+ @glyph_hints = glyph_hints_hash.to_json
84
+ @hinted_glyph_count = glyph_hints_hash.keys.length
85
+ @has_hints = true
86
+ end
87
+
88
+ # Get hints for a specific glyph
89
+ #
90
+ # @param glyph_id [Integer, String] Glyph identifier
91
+ # @return [Array<Hint>] Hints for the glyph
92
+ def get_glyph_hints(glyph_id)
93
+ glyph_hints_hash = parse_glyph_hints
94
+ hints_data = glyph_hints_hash[glyph_id.to_s]
95
+ return [] unless hints_data
96
+
97
+ # Reconstruct Hint objects from serialized data
98
+ hints_data.map { |h| Hint.new(**h.transform_keys(&:to_sym)) }
99
+ end
100
+
101
+ # Get all glyph IDs with hints
102
+ #
103
+ # @return [Array<String>] Glyph identifiers
104
+ def hinted_glyph_ids
105
+ parse_glyph_hints.keys
106
+ end
107
+
108
+ # Check if empty (no hints)
109
+ #
110
+ # @return [Boolean] True if no hints present
111
+ def empty?
112
+ !has_hints &&
113
+ (font_program.nil? || font_program.empty?) &&
114
+ (control_value_program.nil? || control_value_program.empty?) &&
115
+ (control_values.nil? || control_values.empty?) &&
116
+ (private_dict_hints.nil? || private_dict_hints == "{}")
117
+ end
118
+
119
+ private
120
+
121
+ # @return [String] Glyph hints as JSON
122
+ attr_accessor :glyph_hints
123
+
124
+ # Parse glyph hints JSON
125
+ def parse_glyph_hints
126
+ return {} if @glyph_hints.nil? || @glyph_hints.empty? || @glyph_hints == "{}"
127
+ JSON.parse(@glyph_hints)
128
+ rescue JSON::ParserError
129
+ {}
130
+ end
131
+ end
132
+
133
+ # Universal hint representation supporting both TrueType and PostScript hints
134
+ #
135
+ # Hints are instructions that improve font rendering at small sizes by
136
+ # providing information about how to align features to the pixel grid.
137
+ # This model provides a format-agnostic representation that can be
138
+ # converted between TrueType instructions and PostScript hint operators.
139
+ #
140
+ # **Hint Types:**
141
+ #
142
+ # - `:stem` - Vertical or horizontal stem hints (PostScript hstem/vstem)
143
+ # - `:stem3` - Multiple stem hints (PostScript hstem3/vstem3)
144
+ # - `:flex` - Flex hints for smooth curves (PostScript flex)
145
+ # - `:counter` - Counter control hints (PostScript counter)
146
+ # - `:hint_replacement` - Hint replacement (PostScript hintmask)
147
+ # - `:delta` - Delta hints for pixel-level adjustments (TrueType DELTA)
148
+ # - `:interpolate` - Interpolation hints (TrueType IUP)
149
+ # - `:shift` - Shift hints (TrueType SHP)
150
+ # - `:align` - Alignment hints (TrueType ALIGNRP)
151
+ #
152
+ # @example Creating a stem hint
153
+ # hint = Fontisan::Models::Hint.new(
154
+ # type: :stem,
155
+ # data: { position: 100, width: 50, orientation: :vertical }
156
+ # )
157
+ #
158
+ # @example Converting to TrueType
159
+ # tt_instructions = hint.to_truetype
160
+ #
161
+ # @example Converting to PostScript
162
+ # ps_operators = hint.to_postscript
163
+ class Hint
164
+ # @return [Symbol] Hint type
165
+ attr_reader :type
166
+
167
+ # @return [Hash] Hint-specific data
168
+ attr_reader :data
169
+
170
+ # @return [Symbol] Source format (:truetype or :postscript)
171
+ attr_reader :source_format
172
+
173
+ # Initialize a new hint
174
+ #
175
+ # @param type [Symbol] Hint type
176
+ # @param data [Hash] Hint-specific data
177
+ # @param source_format [Symbol] Source format (optional)
178
+ def initialize(type:, data:, source_format: nil)
179
+ @type = type
180
+ @data = data
181
+ @source_format = source_format
182
+ end
183
+
184
+ # Convert hint to TrueType instruction format
185
+ #
186
+ # @return [Array<Integer>] TrueType instruction bytes
187
+ def to_truetype
188
+ case type
189
+ when :stem
190
+ convert_stem_to_truetype
191
+ when :stem3
192
+ convert_stem3_to_truetype
193
+ when :flex
194
+ convert_flex_to_truetype
195
+ when :counter
196
+ convert_counter_to_truetype
197
+ when :hint_replacement
198
+ convert_hintmask_to_truetype
199
+ when :delta
200
+ # Already in TrueType format
201
+ data[:instructions] || []
202
+ when :interpolate
203
+ # IUP instruction
204
+ axis = data[:axis] || :y
205
+ axis == :x ? [0x31] : [0x30] # IUP[x] or IUP[y]
206
+ when :shift
207
+ # SHP instruction
208
+ data[:instructions] || []
209
+ when :align
210
+ # ALIGNRP instruction
211
+ [0x3C]
212
+ else
213
+ # Unknown hint type - return empty
214
+ []
215
+ end
216
+ rescue StandardError => e
217
+ warn "Error converting hint type #{type} to TrueType: #{e.message}"
218
+ []
219
+ end
220
+
221
+ # Convert hint to PostScript hint format
222
+ #
223
+ # @return [Hash] PostScript hint operators and arguments
224
+ def to_postscript
225
+ case type
226
+ when :stem
227
+ convert_stem_to_postscript
228
+ when :stem3
229
+ convert_stem3_to_postscript
230
+ when :flex
231
+ convert_flex_to_postscript
232
+ when :counter
233
+ convert_counter_to_postscript
234
+ when :hint_replacement
235
+ # Hintmask operator
236
+ { operator: :hintmask, args: data[:mask] || [] }
237
+ when :delta, :interpolate, :shift, :align
238
+ # TrueType-specific hints don't have direct PS equivalents
239
+ # Return approximation using stem hints
240
+ approximate_as_postscript
241
+ else
242
+ # Unknown hint type
243
+ {}
244
+ end
245
+ rescue StandardError => e
246
+ warn "Error converting hint type #{type} to PostScript: #{e.message}"
247
+ {}
248
+ end
249
+
250
+ # Check if hint is compatible with target format
251
+ #
252
+ # @param format [Symbol] Target format (:truetype or :postscript)
253
+ # @return [Boolean] True if compatible
254
+ def compatible_with?(format)
255
+ case format
256
+ when :truetype
257
+ # Most PostScript hints can be converted to TrueType
258
+ %i[stem flex counter delta interpolate shift align].include?(type)
259
+ when :postscript
260
+ # Most TrueType hints can be approximated in PostScript
261
+ %i[stem stem3 flex counter hint_replacement].include?(type)
262
+ else
263
+ false
264
+ end
265
+ end
266
+
267
+ private
268
+
269
+ # Convert stem hint to TrueType instructions
270
+ def convert_stem_to_truetype
271
+ position = data[:position] || 0
272
+ width = data[:width] || 0
273
+ orientation = data[:orientation] || :vertical
274
+
275
+ # TrueType uses MDAP (Move Direct Absolute Point) and MDRP (Move Direct Relative Point)
276
+ # to control stem positioning
277
+ instructions = []
278
+
279
+ if orientation == :vertical
280
+ # Vertical stem: use Y-axis instructions
281
+ instructions << 0x2E # MDAP[rnd] - mark reference point
282
+ instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
283
+ else
284
+ # Horizontal stem: use X-axis instructions
285
+ instructions << 0x2F # MDAP[rnd]
286
+ instructions << 0xC0 # MDRP[min,rnd,black]
287
+ end
288
+
289
+ instructions
290
+ end
291
+
292
+ # Convert flex hint to TrueType instructions
293
+ def convert_flex_to_truetype
294
+ # Flex hints ensure smooth curves
295
+ # TrueType approximates with smooth curve flags on contour points
296
+ # Return empty as this is handled at the contour level
297
+ []
298
+ end
299
+
300
+ # Convert counter hint to TrueType instructions
301
+ def convert_counter_to_truetype
302
+ # Counter hints control interior space
303
+ # TrueType uses control value program (CVT) for this
304
+ # Return empty as this requires CVT table modification
305
+ []
306
+ end
307
+
308
+ # Convert stem3 hint to TrueType instructions
309
+ def convert_stem3_to_truetype
310
+ stems = data[:stems] || []
311
+ orientation = data[:orientation] || :vertical
312
+
313
+ # Generate MDAP/MDRP pairs for each stem
314
+ instructions = []
315
+
316
+ stems.each do |stem|
317
+ if orientation == :vertical
318
+ # Vertical stem: use Y-axis instructions
319
+ instructions << 0x2E # MDAP[rnd] - mark reference point
320
+ instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
321
+ else
322
+ # Horizontal stem: use X-axis instructions
323
+ instructions << 0x2F # MDAP[rnd]
324
+ instructions << 0xC0 # MDRP[min,rnd,black]
325
+ end
326
+ end
327
+
328
+ instructions
329
+ end
330
+
331
+ # Convert hintmask hint to TrueType instructions
332
+ def convert_hintmask_to_truetype
333
+ # Hintmask controls which hints are active at runtime
334
+ # TrueType doesn't have a direct equivalent
335
+ # We can use conditional instructions, but it's complex
336
+ # For now, return empty and let the main stems handle hinting
337
+ # TODO: Implement conditional instruction generation if needed
338
+ []
339
+ end
340
+
341
+ # Convert stem hint to PostScript operators
342
+ def convert_stem_to_postscript
343
+ position = data[:position] || 0
344
+ width = data[:width] || 0
345
+ orientation = data[:orientation] || :vertical
346
+
347
+ operator = orientation == :vertical ? :vstem : :hstem
348
+
349
+ {
350
+ operator: operator,
351
+ args: [position, width],
352
+ }
353
+ end
354
+
355
+ # Convert stem3 hint to PostScript operators
356
+ def convert_stem3_to_postscript
357
+ stems = data[:stems] || []
358
+ orientation = data[:orientation] || :vertical
359
+
360
+ operator = orientation == :vertical ? :vstem3 : :hstem3
361
+
362
+ # Flatten stem positions and widths
363
+ args = stems.flat_map { |s| [s[:position], s[:width]] }
364
+
365
+ {
366
+ operator: operator,
367
+ args: args,
368
+ }
369
+ end
370
+
371
+ # Convert flex hint to PostScript operators
372
+ def convert_flex_to_postscript
373
+ points = data[:points] || []
374
+
375
+ {
376
+ operator: :flex,
377
+ args: points.flat_map { |p| [p[:x], p[:y]] },
378
+ }
379
+ end
380
+
381
+ # Convert counter hint to PostScript operators
382
+ def convert_counter_to_postscript
383
+ zones = data[:zones] || []
384
+
385
+ {
386
+ operator: :counter,
387
+ args: zones,
388
+ }
389
+ end
390
+
391
+ # Approximate TrueType-specific hints as PostScript stem hints
392
+ def approximate_as_postscript
393
+ # Best effort: create a stem hint from available data
394
+ if data[:position] && data[:width]
395
+ {
396
+ operator: :vstem,
397
+ args: [data[:position], data[:width]],
398
+ }
399
+ else
400
+ {}
401
+ end
402
+ end
403
+ end
404
+ end
405
+ end