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,354 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../models/hint"
4
+
5
+ module Fontisan
6
+ module Hints
7
+ # Extracts rendering hints from PostScript/CFF CharString data
8
+ #
9
+ # PostScript Type 1 and CFF fonts embed hints directly in the
10
+ # CharString data as operators. This extractor parses CharString
11
+ # sequences to identify and extract hint operators.
12
+ #
13
+ # **Supported PostScript Hint Operators:**
14
+ #
15
+ # - hstem/vstem - Horizontal/vertical stem hints
16
+ # - hstem3/vstem3 - Multiple stem hints
17
+ # - hintmask - Hint replacement masks
18
+ # - cntrmask - Counter control masks
19
+ #
20
+ # @example Extract hints from a CharString
21
+ # extractor = PostScriptHintExtractor.new
22
+ # hints = extractor.extract(charstring)
23
+ class PostScriptHintExtractor
24
+ # CFF CharString operators
25
+ HSTEM = 1
26
+ VSTEM = 3
27
+ HINTMASK = 19
28
+ CNTRMASK = 20
29
+ HSTEM3 = 12 << 8 | 2
30
+ VSTEM3 = 12 << 8 | 1
31
+
32
+ # Extract complete hint data from OpenType/CFF font
33
+ #
34
+ # This extracts both font-level hints (CFF Private dict) and
35
+ # per-glyph hints from CharStrings.
36
+ #
37
+ # @param font [OpenTypeFont] OpenType font with CFF table
38
+ # @return [Models::HintSet] Complete hint set
39
+ def extract_from_font(font)
40
+ hint_set = Models::HintSet.new(format: "postscript")
41
+
42
+ # Extract font-level Private dict hints
43
+ hint_set.private_dict_hints = extract_private_dict_hints(font).to_json
44
+
45
+ # Extract per-glyph CharString hints
46
+ extract_charstring_hints(font, hint_set)
47
+
48
+ # Update metadata
49
+ hint_set.has_hints = !hint_set.empty?
50
+
51
+ hint_set
52
+ end
53
+
54
+ # Extract hints from CFF CharString
55
+ #
56
+ # @param charstring [CharString, String] CFF CharString object or bytes
57
+ # @return [Array<Hint>] Extracted hints
58
+ def extract(charstring)
59
+ return [] if charstring.nil?
60
+
61
+ # Get CharString bytes
62
+ bytes = if charstring.respond_to?(:data)
63
+ charstring.data
64
+ elsif charstring.respond_to?(:bytes)
65
+ charstring.bytes
66
+ elsif charstring.is_a?(String)
67
+ charstring.bytes
68
+ else
69
+ return []
70
+ end
71
+
72
+ return [] if bytes.empty?
73
+
74
+ parse_charstring(bytes)
75
+ end
76
+
77
+ private
78
+
79
+ # Parse CharString bytes to extract hints
80
+ #
81
+ # @param bytes [Array<Integer>] CharString bytes
82
+ # @return [Array<Hint>] Extracted hints
83
+ def parse_charstring(bytes)
84
+ hints = []
85
+ stack = []
86
+ i = 0
87
+
88
+ while i < bytes.length
89
+ byte = bytes[i]
90
+
91
+ if operator?(byte)
92
+ # Process operator
93
+ operator = if byte == 12
94
+ # Two-byte operator
95
+ i += 1
96
+ (12 << 8) | bytes[i]
97
+ else
98
+ byte
99
+ end
100
+
101
+ hint = process_operator(operator, stack)
102
+ hints << hint if hint
103
+
104
+ # Clear stack after operator
105
+ stack.clear
106
+ i += 1
107
+ else
108
+ # Number - push to stack
109
+ num, consumed = decode_number(bytes, i)
110
+ stack << num if num
111
+ i += consumed
112
+ end
113
+ end
114
+
115
+ hints
116
+ end
117
+
118
+ # Check if byte is an operator
119
+ #
120
+ # @param byte [Integer] Byte value
121
+ # @return [Boolean] True if operator
122
+ def operator?(byte)
123
+ byte <= 31 || byte == 255
124
+ end
125
+
126
+ # Decode a number from CharString
127
+ #
128
+ # @param bytes [Array<Integer>] CharString bytes
129
+ # @param index [Integer] Starting position
130
+ # @return [Array<Integer, Integer>] [number, bytes_consumed]
131
+ def decode_number(bytes, index)
132
+ byte = bytes[index]
133
+ return [nil, 1] if byte.nil?
134
+
135
+ case byte
136
+ when 28
137
+ # 3-byte signed integer
138
+ if index + 2 < bytes.length
139
+ num = (bytes[index + 1] << 8) | bytes[index + 2]
140
+ num = num - 65536 if num > 32767
141
+ [num, 3]
142
+ else
143
+ [nil, 1]
144
+ end
145
+ when 32..246
146
+ # Single byte integer
147
+ [byte - 139, 1]
148
+ when 247..250
149
+ # Positive 2-byte integer
150
+ if index + 1 < bytes.length
151
+ num = (byte - 247) * 256 + bytes[index + 1] + 108
152
+ [num, 2]
153
+ else
154
+ [nil, 1]
155
+ end
156
+ when 251..254
157
+ # Negative 2-byte integer
158
+ if index + 1 < bytes.length
159
+ num = -(byte - 251) * 256 - bytes[index + 1] - 108
160
+ [num, 2]
161
+ else
162
+ [nil, 1]
163
+ end
164
+ when 255
165
+ # 5-byte signed integer
166
+ if index + 4 < bytes.length
167
+ num = (bytes[index + 1] << 24) | (bytes[index + 2] << 16) |
168
+ (bytes[index + 3] << 8) | bytes[index + 4]
169
+ num = num - 4294967296 if num > 2147483647
170
+ [num, 5]
171
+ else
172
+ [nil, 1]
173
+ end
174
+ else
175
+ [nil, 1]
176
+ end
177
+ end
178
+
179
+ # Process hint operator and create Hint object
180
+ #
181
+ # @param operator [Integer] Operator code
182
+ # @param stack [Array<Integer>] Current operand stack
183
+ # @return [Hint, nil] Hint object if operator is a hint
184
+ def process_operator(operator, stack)
185
+ case operator
186
+ when HSTEM
187
+ # Horizontal stem hint
188
+ extract_stem_hint(stack, :horizontal)
189
+
190
+ when VSTEM
191
+ # Vertical stem hint
192
+ extract_stem_hint(stack, :vertical)
193
+
194
+ when HSTEM3
195
+ # Multiple horizontal stems
196
+ extract_stem3_hint(stack, :horizontal)
197
+
198
+ when VSTEM3
199
+ # Multiple vertical stems
200
+ extract_stem3_hint(stack, :vertical)
201
+
202
+ when HINTMASK
203
+ # Hint replacement mask
204
+ Models::Hint.new(
205
+ type: :hint_replacement,
206
+ data: { mask: stack.dup },
207
+ source_format: :postscript
208
+ )
209
+
210
+ when CNTRMASK
211
+ # Counter control mask
212
+ Models::Hint.new(
213
+ type: :counter,
214
+ data: { zones: stack.dup },
215
+ source_format: :postscript
216
+ )
217
+
218
+ else
219
+ nil
220
+ end
221
+ end
222
+
223
+ # Extract stem hint from stack
224
+ #
225
+ # @param stack [Array<Integer>] Operand stack
226
+ # @param orientation [Symbol] :horizontal or :vertical
227
+ # @return [Hint] Stem hint
228
+ def extract_stem_hint(stack, orientation)
229
+ # Stack should have pairs of [position, width]
230
+ return nil if stack.empty? || stack.length < 2
231
+
232
+ # Take first pair
233
+ position = stack[0]
234
+ width = stack[1]
235
+
236
+ Models::Hint.new(
237
+ type: :stem,
238
+ data: {
239
+ position: position,
240
+ width: width,
241
+ orientation: orientation
242
+ },
243
+ source_format: :postscript
244
+ )
245
+ end
246
+
247
+ # Extract stem3 hint from stack
248
+ #
249
+ # @param stack [Array<Integer>] Operand stack
250
+ # @param orientation [Symbol] :horizontal or :vertical
251
+ # @return [Hint] Stem3 hint
252
+ def extract_stem3_hint(stack, orientation)
253
+ # Stack should have 6 values: 3 pairs of [position, width]
254
+ return nil if stack.length < 6
255
+
256
+ stems = []
257
+ (0..2).each do |i|
258
+ pos_idx = i * 2
259
+ stems << {
260
+ position: stack[pos_idx],
261
+ width: stack[pos_idx + 1]
262
+ }
263
+ end
264
+
265
+ Models::Hint.new(
266
+ type: :stem3,
267
+ data: {
268
+ stems: stems,
269
+ orientation: orientation
270
+ },
271
+ source_format: :postscript
272
+ )
273
+ end
274
+
275
+ # Extract Private dict hints from CFF table
276
+ #
277
+ # Private dict contains font-level hint parameters like BlueValues,
278
+ # StdHW, StdVW, etc.
279
+ #
280
+ # @param font [OpenTypeFont] OpenType font
281
+ # @return [Hash] Private dict hint parameters
282
+ def extract_private_dict_hints(font)
283
+ hints = {}
284
+
285
+ return hints unless font.has_table?("CFF ")
286
+
287
+ cff_table = font.table("CFF ")
288
+ return hints unless cff_table
289
+
290
+ # Get Private DICT for first font (index 0)
291
+ private_dict = cff_table.private_dict(0)
292
+ return hints unless private_dict
293
+
294
+ # Extract hint-related parameters from Private DICT
295
+ # These are the key hinting parameters in CFF
296
+ hints[:blue_values] = private_dict.blue_values if private_dict.respond_to?(:blue_values)
297
+ hints[:other_blues] = private_dict.other_blues if private_dict.respond_to?(:other_blues)
298
+ hints[:family_blues] = private_dict.family_blues if private_dict.respond_to?(:family_blues)
299
+ hints[:family_other_blues] = private_dict.family_other_blues if private_dict.respond_to?(:family_other_blues)
300
+ hints[:blue_scale] = private_dict.blue_scale if private_dict.respond_to?(:blue_scale)
301
+ hints[:blue_shift] = private_dict.blue_shift if private_dict.respond_to?(:blue_shift)
302
+ hints[:blue_fuzz] = private_dict.blue_fuzz if private_dict.respond_to?(:blue_fuzz)
303
+ hints[:std_hw] = private_dict.std_hw if private_dict.respond_to?(:std_hw)
304
+ hints[:std_vw] = private_dict.std_vw if private_dict.respond_to?(:std_vw)
305
+ hints[:stem_snap_h] = private_dict.stem_snap_h if private_dict.respond_to?(:stem_snap_h)
306
+ hints[:stem_snap_v] = private_dict.stem_snap_v if private_dict.respond_to?(:stem_snap_v)
307
+ hints[:force_bold] = private_dict.force_bold if private_dict.respond_to?(:force_bold)
308
+ hints[:language_group] = private_dict.language_group if private_dict.respond_to?(:language_group)
309
+
310
+ hints.compact
311
+ rescue StandardError => e
312
+ warn "Failed to extract Private dict hints: #{e.message}"
313
+ {}
314
+ end
315
+
316
+ # Extract per-glyph CharString hints from CFF table
317
+ #
318
+ # @param font [OpenTypeFont] OpenType font
319
+ # @param hint_set [Models::HintSet] Hint set to populate
320
+ # @return [void]
321
+ def extract_charstring_hints(font, hint_set)
322
+ return unless font.has_table?("CFF ")
323
+
324
+ cff_table = font.table("CFF ")
325
+ return unless cff_table
326
+
327
+ # Get CharStrings INDEX
328
+ charstrings_index = cff_table.charstrings_index(0)
329
+ return unless charstrings_index
330
+
331
+ # Iterate through all glyphs
332
+ glyph_count = cff_table.glyph_count(0)
333
+ (0...glyph_count).each do |glyph_id|
334
+ begin
335
+ # Get CharString for this glyph
336
+ charstring = cff_table.charstring_for_glyph(glyph_id, 0)
337
+ next unless charstring
338
+
339
+ # Extract hints from CharString
340
+ hints = extract(charstring)
341
+ next if hints.empty?
342
+
343
+ # Store glyph hints
344
+ hint_set.add_glyph_hints(glyph_id, hints)
345
+ rescue StandardError => e
346
+ warn "Failed to extract hints for glyph #{glyph_id}: #{e.message}"
347
+ end
348
+ end
349
+ rescue StandardError => e
350
+ warn "Failed to extract CharString hints: #{e.message}"
351
+ end
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Hints
5
+ # Applies rendering hints to TrueType font tables
6
+ #
7
+ # This applier writes TrueType hint data into font-level tables:
8
+ # - fpgm (Font Program) - bytecode executed once at font initialization
9
+ # - prep (Control Value Program) - bytecode for glyph preparation
10
+ # - cvt (Control Values) - array of 16-bit values for hinting metrics
11
+ #
12
+ # The applier ensures proper table structure with correct checksums
13
+ # and does not corrupt the font if hint application fails.
14
+ #
15
+ # @example Apply hints from a HintSet
16
+ # applier = TrueTypeHintApplier.new
17
+ # tables = {}
18
+ # updated_tables = applier.apply(hint_set, tables)
19
+ class TrueTypeHintApplier
20
+ # Apply TrueType hints to font tables
21
+ #
22
+ # @param hint_set [HintSet] Hint data to apply
23
+ # @param tables [Hash] Font tables to update
24
+ # @return [Hash] Updated font tables
25
+ def apply(hint_set, tables)
26
+ return tables if hint_set.nil? || hint_set.empty?
27
+ return tables unless hint_set.format == "truetype"
28
+
29
+ # Write fpgm table if present
30
+ if hint_set.font_program && !hint_set.font_program.empty?
31
+ tables["fpgm"] = build_fpgm_table(hint_set.font_program)
32
+ end
33
+
34
+ # Write prep table if present
35
+ if hint_set.control_value_program && !hint_set.control_value_program.empty?
36
+ tables["prep"] = build_prep_table(hint_set.control_value_program)
37
+ end
38
+
39
+ # Write cvt table if present
40
+ if hint_set.control_values && !hint_set.control_values.empty?
41
+ tables["cvt "] = build_cvt_table(hint_set.control_values)
42
+ end
43
+
44
+ # Future enhancement: Apply per-glyph hints to glyf table
45
+ # For now, font-level tables only
46
+
47
+ tables
48
+ end
49
+
50
+ private
51
+
52
+ # Build fpgm (Font Program) table
53
+ #
54
+ # @param program_data [String] Raw bytecode
55
+ # @return [Hash] Table structure with tag, data, and checksum
56
+ def build_fpgm_table(program_data)
57
+ {
58
+ tag: "fpgm",
59
+ data: program_data,
60
+ checksum: calculate_checksum(program_data),
61
+ }
62
+ end
63
+
64
+ # Build prep (Control Value Program) table
65
+ #
66
+ # @param program_data [String] Raw bytecode
67
+ # @return [Hash] Table structure with tag, data, and checksum
68
+ def build_prep_table(program_data)
69
+ {
70
+ tag: "prep",
71
+ data: program_data,
72
+ checksum: calculate_checksum(program_data),
73
+ }
74
+ end
75
+
76
+ # Build cvt (Control Values) table
77
+ #
78
+ # CVT values are 16-bit signed integers (FWORD) in big-endian format.
79
+ # Each value represents a design-space coordinate used for hinting.
80
+ #
81
+ # @param control_values [Array<Integer>] Array of 16-bit signed values
82
+ # @return [Hash] Table structure with tag, data, and checksum
83
+ def build_cvt_table(control_values)
84
+ # Pack as 16-bit big-endian signed integers (s> = signed big-endian)
85
+ data = control_values.pack("s>*")
86
+
87
+ {
88
+ tag: "cvt ",
89
+ data: data,
90
+ checksum: calculate_checksum(data),
91
+ }
92
+ end
93
+
94
+ # Calculate OpenType table checksum
95
+ #
96
+ # OpenType spec requires tables to be checksummed as 32-bit unsigned
97
+ # integers in big-endian format. The table is padded to a multiple of
98
+ # 4 bytes with zeros before checksum calculation.
99
+ #
100
+ # @param data [String] Table data
101
+ # @return [Integer] 32-bit checksum
102
+ def calculate_checksum(data)
103
+ # Pad to 4-byte boundary with zeros
104
+ padding_needed = (4 - data.length % 4) % 4
105
+ padded = data + ("\x00" * padding_needed)
106
+
107
+ # Sum as 32-bit unsigned integers in big-endian
108
+ checksum = 0
109
+ (0...padded.length).step(4) do |i|
110
+ checksum = (checksum + padded[i, 4].unpack1("N")) & 0xFFFFFFFF
111
+ end
112
+
113
+ checksum
114
+ end
115
+ end
116
+ end
117
+ end