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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +119 -308
- data/README.adoc +1525 -1323
- data/Rakefile +45 -47
- data/benchmark/variation_quick_bench.rb +4 -4
- data/docs/FONT_HINTING.adoc +562 -0
- data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
- data/lib/fontisan/cli.rb +92 -34
- data/lib/fontisan/collection/builder.rb +82 -0
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +21 -2
- data/lib/fontisan/commands/convert_command.rb +96 -165
- data/lib/fontisan/commands/info_command.rb +111 -5
- data/lib/fontisan/commands/instance_command.rb +77 -85
- data/lib/fontisan/commands/validate_command.rb +28 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +34 -24
- data/lib/fontisan/converters/format_converter.rb +154 -1
- data/lib/fontisan/converters/outline_converter.rb +101 -34
- data/lib/fontisan/converters/woff_writer.rb +9 -4
- data/lib/fontisan/font_loader.rb +14 -9
- data/lib/fontisan/font_writer.rb +9 -6
- data/lib/fontisan/formatters/text_formatter.rb +45 -1
- data/lib/fontisan/hints/hint_converter.rb +131 -2
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
- data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
- data/lib/fontisan/loading_modes.rb +6 -4
- data/lib/fontisan/models/collection_brief_info.rb +31 -0
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +183 -12
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/open_type_font.rb +28 -10
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +159 -0
- data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +416 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +58 -3
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff.rb +2 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +10 -5
- data/lib/fontisan/tables/cvar.rb +2 -41
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
- data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
- data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
- data/lib/fontisan/tables/gvar.rb +2 -41
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_font.rb +27 -10
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/validation/table_validator.rb +1 -1
- data/lib/fontisan/validation/variable_font_validator.rb +218 -0
- data/lib/fontisan/variation/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +121 -13
- data/lib/fontisan/variation/delta_applier.rb +2 -1
- data/lib/fontisan/variation/inspector.rb +2 -1
- data/lib/fontisan/variation/instance_generator.rb +2 -1
- data/lib/fontisan/variation/instance_writer.rb +341 -0
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_preserver.rb +291 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +489 -468
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +54 -2
- data/scripts/measure_optimization.rb +15 -7
- 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
|