fontisan 0.2.0 → 0.2.2

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +119 -308
  3. data/README.adoc +1525 -1323
  4. data/Rakefile +45 -47
  5. data/benchmark/variation_quick_bench.rb +4 -4
  6. data/docs/FONT_HINTING.adoc +562 -0
  7. data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
  8. data/lib/fontisan/cli.rb +92 -34
  9. data/lib/fontisan/collection/builder.rb +82 -0
  10. data/lib/fontisan/collection/offset_calculator.rb +2 -0
  11. data/lib/fontisan/collection/table_deduplicator.rb +76 -0
  12. data/lib/fontisan/commands/base_command.rb +21 -2
  13. data/lib/fontisan/commands/convert_command.rb +96 -165
  14. data/lib/fontisan/commands/info_command.rb +111 -5
  15. data/lib/fontisan/commands/instance_command.rb +77 -85
  16. data/lib/fontisan/commands/validate_command.rb +28 -0
  17. data/lib/fontisan/config/validation_rules.yml +1 -1
  18. data/lib/fontisan/constants.rb +34 -24
  19. data/lib/fontisan/converters/format_converter.rb +154 -1
  20. data/lib/fontisan/converters/outline_converter.rb +101 -34
  21. data/lib/fontisan/converters/woff_writer.rb +9 -4
  22. data/lib/fontisan/font_loader.rb +14 -9
  23. data/lib/fontisan/font_writer.rb +9 -6
  24. data/lib/fontisan/formatters/text_formatter.rb +45 -1
  25. data/lib/fontisan/hints/hint_converter.rb +131 -2
  26. data/lib/fontisan/hints/hint_validator.rb +284 -0
  27. data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
  28. data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
  29. data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
  30. data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
  31. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
  32. data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
  33. data/lib/fontisan/loading_modes.rb +6 -4
  34. data/lib/fontisan/models/collection_brief_info.rb +31 -0
  35. data/lib/fontisan/models/font_info.rb +3 -30
  36. data/lib/fontisan/models/hint.rb +183 -12
  37. data/lib/fontisan/models/outline.rb +4 -1
  38. data/lib/fontisan/open_type_font.rb +28 -10
  39. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  40. data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
  41. data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
  42. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  43. data/lib/fontisan/pipeline/output_writer.rb +159 -0
  44. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  45. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  46. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  47. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  48. data/lib/fontisan/pipeline/transformation_pipeline.rb +416 -0
  49. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  50. data/lib/fontisan/subset/table_subsetter.rb +5 -5
  51. data/lib/fontisan/tables/cff/charstring.rb +58 -3
  52. data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
  53. data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
  54. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  55. data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
  56. data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
  57. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  58. data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
  59. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  60. data/lib/fontisan/tables/cff.rb +2 -0
  61. data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
  62. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
  63. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  64. data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
  65. data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
  66. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  67. data/lib/fontisan/tables/cff2.rb +10 -5
  68. data/lib/fontisan/tables/cvar.rb +2 -41
  69. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
  70. data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
  71. data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
  72. data/lib/fontisan/tables/gvar.rb +2 -41
  73. data/lib/fontisan/tables/name.rb +4 -4
  74. data/lib/fontisan/true_type_font.rb +27 -10
  75. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  76. data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
  77. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  78. data/lib/fontisan/validation/table_validator.rb +1 -1
  79. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  80. data/lib/fontisan/variation/cache.rb +3 -1
  81. data/lib/fontisan/variation/converter.rb +121 -13
  82. data/lib/fontisan/variation/delta_applier.rb +2 -1
  83. data/lib/fontisan/variation/inspector.rb +2 -1
  84. data/lib/fontisan/variation/instance_generator.rb +2 -1
  85. data/lib/fontisan/variation/instance_writer.rb +341 -0
  86. data/lib/fontisan/variation/optimizer.rb +6 -3
  87. data/lib/fontisan/variation/subsetter.rb +32 -10
  88. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  89. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  90. data/lib/fontisan/variation/variation_preserver.rb +291 -0
  91. data/lib/fontisan/version.rb +1 -1
  92. data/lib/fontisan/version.rb.orig +9 -0
  93. data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
  94. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  95. data/lib/fontisan/woff2_font.rb +489 -468
  96. data/lib/fontisan/woff_font.rb +16 -11
  97. data/lib/fontisan.rb +54 -2
  98. data/scripts/measure_optimization.rb +15 -7
  99. metadata +37 -2
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Hints
5
+ # Validates hint data for correctness and compatibility
6
+ #
7
+ # This validator ensures that hints are well-formed and compatible with
8
+ # their target format. It performs multiple levels of validation:
9
+ # - TrueType instruction bytecode validation
10
+ # - PostScript hint parameter validation
11
+ # - Stack neutrality verification
12
+ # - Hint-outline compatibility checking
13
+ #
14
+ # @example Validate TrueType instructions
15
+ # validator = HintValidator.new
16
+ # result = validator.validate_truetype_instructions(prep_bytes)
17
+ # if result[:valid]
18
+ # puts "Valid TrueType instructions"
19
+ # else
20
+ # puts "Errors: #{result[:errors]}"
21
+ # end
22
+ #
23
+ # @example Validate PostScript hints
24
+ # validator = HintValidator.new
25
+ # result = validator.validate_postscript_hints(ps_dict)
26
+ # puts result[:warnings] if result[:warnings].any?
27
+ class HintValidator
28
+ # Maximum allowed values for PostScript hint parameters
29
+ MAX_BLUE_VALUES = 14 # 7 pairs
30
+ MAX_OTHER_BLUES = 10 # 5 pairs
31
+ MAX_STEM_SNAP = 12 # Maximum stem snap entries
32
+
33
+ # Validate TrueType instruction bytecode
34
+ #
35
+ # Checks for:
36
+ # - Valid instruction opcodes
37
+ # - Correct parameter counts
38
+ # - Stack neutrality
39
+ #
40
+ # @param instructions [String] Binary instruction bytes
41
+ # @return [Hash] Validation result with :valid, :errors, :warnings keys
42
+ def validate_truetype_instructions(instructions)
43
+ return { valid: true, errors: [], warnings: [] } if instructions.nil? || instructions.empty?
44
+
45
+ errors = []
46
+ warnings = []
47
+
48
+ begin
49
+ bytes = instructions.bytes
50
+ stack_depth = 0
51
+ index = 0
52
+
53
+ while index < bytes.length
54
+ opcode = bytes[index]
55
+ index += 1
56
+
57
+ case opcode
58
+ when 0x40 # NPUSHB
59
+ count = bytes[index]
60
+ index += 1
61
+ if index + count > bytes.length
62
+ errors << "NPUSHB: Not enough bytes (need #{count}, have #{bytes.length - index})"
63
+ break
64
+ end
65
+ stack_depth += count
66
+ index += count
67
+
68
+ when 0x41 # NPUSHW
69
+ count = bytes[index]
70
+ index += 1
71
+ if index + (count * 2) > bytes.length
72
+ errors << "NPUSHW: Not enough bytes (need #{count * 2}, have #{bytes.length - index})"
73
+ break
74
+ end
75
+ stack_depth += count
76
+ index += count * 2
77
+
78
+ when 0xB0..0xB7 # PUSHB[0-7]
79
+ count = opcode - 0xB0 + 1
80
+ if index + count > bytes.length
81
+ errors << "PUSHB[#{count - 1}]: Not enough bytes"
82
+ break
83
+ end
84
+ stack_depth += count
85
+ index += count
86
+
87
+ when 0xB8..0xBF # PUSHW[0-7]
88
+ count = opcode - 0xB8 + 1
89
+ if index + (count * 2) > bytes.length
90
+ errors << "PUSHW[#{count - 1}]: Not enough bytes"
91
+ break
92
+ end
93
+ stack_depth += count
94
+ index += count * 2
95
+
96
+ when 0x1D, 0x1E, 0x1F # SCVTCI, SSWCI, SSW
97
+ if stack_depth < 1
98
+ errors << "#{opcode_name(opcode)}: Stack underflow"
99
+ end
100
+ stack_depth -= 1
101
+
102
+ when 0x44, 0x70 # WCVTP, WCVTF
103
+ if stack_depth < 2
104
+ errors << "#{opcode_name(opcode)}: Stack underflow (need 2 values)"
105
+ end
106
+ stack_depth -= 2
107
+
108
+ else
109
+ warnings << "Unknown opcode: 0x#{opcode.to_s(16).upcase} at offset #{index - 1}"
110
+ end
111
+ end
112
+
113
+ # Check stack neutrality
114
+ if stack_depth != 0
115
+ warnings << "Stack not neutral: #{stack_depth} value(s) remaining"
116
+ end
117
+
118
+ rescue StandardError => e
119
+ errors << "Exception during validation: #{e.message}"
120
+ end
121
+
122
+ {
123
+ valid: errors.empty?,
124
+ errors: errors,
125
+ warnings: warnings,
126
+ }
127
+ end
128
+
129
+ # Validate PostScript hint parameters
130
+ #
131
+ # Checks for:
132
+ # - Valid parameter ranges
133
+ # - Proper pair counts for blue zones
134
+ # - Sensible stem width values
135
+ #
136
+ # @param hints [Hash] PostScript hint parameters
137
+ # @return [Hash] Validation result with :valid, :errors, :warnings keys
138
+ def validate_postscript_hints(hints)
139
+ errors = []
140
+ warnings = []
141
+
142
+ # Validate blue_values
143
+ if hints[:blue_values]
144
+ blue_values = hints[:blue_values]
145
+ if blue_values.length > MAX_BLUE_VALUES
146
+ errors << "blue_values exceeds maximum (#{MAX_BLUE_VALUES}): #{blue_values.length}"
147
+ end
148
+ if blue_values.length.odd?
149
+ errors << "blue_values must be pairs (even count): #{blue_values.length}"
150
+ end
151
+ end
152
+
153
+ # Validate other_blues
154
+ if hints[:other_blues]
155
+ other_blues = hints[:other_blues]
156
+ if other_blues.length > MAX_OTHER_BLUES
157
+ errors << "other_blues exceeds maximum (#{MAX_OTHER_BLUES}): #{other_blues.length}"
158
+ end
159
+ if other_blues.length.odd?
160
+ errors << "other_blues must be pairs (even count): #{other_blues.length}"
161
+ end
162
+ end
163
+
164
+ # Validate stem widths
165
+ [:std_hw, :std_vw].each do |key|
166
+ if hints[key] && hints[key] <= 0
167
+ errors << "#{key} must be positive: #{hints[key]}"
168
+ end
169
+ end
170
+
171
+ # Validate stem snaps
172
+ [:stem_snap_h, :stem_snap_v].each do |key|
173
+ if hints[key]
174
+ if hints[key].length > MAX_STEM_SNAP
175
+ errors << "#{key} exceeds maximum (#{MAX_STEM_SNAP}): #{hints[key].length}"
176
+ end
177
+ if hints[key].any? { |v| v <= 0 }
178
+ warnings << "#{key} contains non-positive values"
179
+ end
180
+ end
181
+ end
182
+
183
+ # Validate blue_scale
184
+ if hints[:blue_scale]
185
+ if hints[:blue_scale] <= 0
186
+ errors << "blue_scale must be positive: #{hints[:blue_scale]}"
187
+ end
188
+ if hints[:blue_scale] > 1.0
189
+ warnings << "blue_scale unusually large (>1.0): #{hints[:blue_scale]}"
190
+ end
191
+ end
192
+
193
+ # Validate language_group
194
+ if hints[:language_group]
195
+ unless [0, 1].include?(hints[:language_group])
196
+ errors << "language_group must be 0 (Latin) or 1 (CJK): #{hints[:language_group]}"
197
+ end
198
+ end
199
+
200
+ {
201
+ valid: errors.empty?,
202
+ errors: errors,
203
+ warnings: warnings,
204
+ }
205
+ end
206
+
207
+ # Validate stack neutrality of instruction sequence
208
+ #
209
+ # Ensures the instruction sequence leaves the stack in the same state
210
+ # as it started (net stack change of zero).
211
+ #
212
+ # @param instructions [String] Binary instruction bytes
213
+ # @return [Hash] Result with :neutral, :stack_depth, :errors keys
214
+ def validate_stack_neutrality(instructions)
215
+ return { neutral: true, stack_depth: 0, errors: [] } if instructions.nil? || instructions.empty?
216
+
217
+ errors = []
218
+ stack_depth = 0
219
+ bytes = instructions.bytes
220
+ index = 0
221
+
222
+ begin
223
+ while index < bytes.length
224
+ opcode = bytes[index]
225
+ index += 1
226
+
227
+ case opcode
228
+ when 0x40 # NPUSHB
229
+ count = bytes[index]
230
+ index += 1 + count
231
+ stack_depth += count
232
+
233
+ when 0x41 # NPUSHW
234
+ count = bytes[index]
235
+ index += 1 + (count * 2)
236
+ stack_depth += count
237
+
238
+ when 0xB0..0xB7 # PUSHB[0-7]
239
+ count = opcode - 0xB0 + 1
240
+ index += count
241
+ stack_depth += count
242
+
243
+ when 0xB8..0xBF # PUSHW[0-7]
244
+ count = opcode - 0xB8 + 1
245
+ index += count * 2
246
+ stack_depth += count
247
+
248
+ when 0x1D, 0x1E, 0x1F # SCVTCI, SSWCI, SSW
249
+ stack_depth -= 1
250
+
251
+ when 0x44, 0x70 # WCVTP, WCVTF
252
+ stack_depth -= 2
253
+ end
254
+ end
255
+ rescue StandardError => e
256
+ errors << "Error analyzing stack: #{e.message}"
257
+ end
258
+
259
+ {
260
+ neutral: stack_depth == 0,
261
+ stack_depth: stack_depth,
262
+ errors: errors,
263
+ }
264
+ end
265
+
266
+ private
267
+
268
+ # Get human-readable name for opcode
269
+ #
270
+ # @param opcode [Integer] Instruction opcode
271
+ # @return [String] Opcode name
272
+ def opcode_name(opcode)
273
+ case opcode
274
+ when 0x1D then "SCVTCI"
275
+ when 0x1E then "SSWCI"
276
+ when 0x1F then "SSW"
277
+ when 0x44 then "WCVTP"
278
+ when 0x70 then "WCVTF"
279
+ else "0x#{opcode.to_s(16).upcase}"
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end