fontisan 0.2.1 → 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 +57 -385
- data/README.adoc +1483 -1435
- data/Rakefile +3 -2
- 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 +10 -3
- data/lib/fontisan/collection/builder.rb +2 -1
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/commands/base_command.rb +5 -2
- data/lib/fontisan/commands/convert_command.rb +6 -2
- data/lib/fontisan/commands/info_command.rb +111 -5
- data/lib/fontisan/commands/instance_command.rb +8 -7
- data/lib/fontisan/commands/validate_command.rb +4 -1
- data/lib/fontisan/constants.rb +24 -24
- data/lib/fontisan/converters/format_converter.rb +8 -4
- data/lib/fontisan/converters/outline_converter.rb +21 -16
- data/lib/fontisan/converters/woff_writer.rb +8 -3
- data/lib/fontisan/font_loader.rb +11 -4
- data/lib/fontisan/font_writer.rb +2 -0
- data/lib/fontisan/formatters/text_formatter.rb +45 -1
- data/lib/fontisan/hints/hint_converter.rb +43 -47
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +1 -3
- data/lib/fontisan/hints/postscript_hint_extractor.rb +78 -43
- data/lib/fontisan/hints/truetype_hint_extractor.rb +22 -26
- 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 +4 -4
- data/lib/fontisan/models/collection_brief_info.rb +31 -0
- data/lib/fontisan/models/font_export.rb +2 -2
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +22 -23
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/models/validation_report.rb +1 -1
- data/lib/fontisan/open_type_font.rb +3 -1
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/output_writer.rb +8 -3
- data/lib/fontisan/pipeline/transformation_pipeline.rb +8 -3
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +38 -12
- data/lib/fontisan/tables/cff/charstring_parser.rb +23 -11
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +14 -14
- data/lib/fontisan/tables/cff/dict_builder.rb +4 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +6 -4
- data/lib/fontisan/tables/cff/offset_recalculator.rb +1 -1
- data/lib/fontisan/tables/cff/private_dict_writer.rb +10 -4
- data/lib/fontisan/tables/cff/table_builder.rb +1 -1
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +7 -6
- data/lib/fontisan/tables/cff2/region_matcher.rb +2 -2
- data/lib/fontisan/tables/cff2/table_builder.rb +26 -20
- data/lib/fontisan/tables/cff2/table_reader.rb +35 -33
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +2 -2
- data/lib/fontisan/tables/cff2.rb +1 -1
- 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/name.rb +4 -4
- data/lib/fontisan/true_type_font.rb +3 -1
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/variation/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +2 -1
- 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/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- data/lib/fontisan/variation/variation_preserver.rb +4 -1
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/glyf_transformer.rb +57 -30
- data/lib/fontisan/woff2_font.rb +31 -15
- data/lib/fontisan.rb +42 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +8 -2
|
@@ -82,11 +82,11 @@ module Fontisan
|
|
|
82
82
|
when :postscript
|
|
83
83
|
# Convert font-level TT → PS
|
|
84
84
|
if hint_set.font_program || hint_set.control_value_program ||
|
|
85
|
-
|
|
85
|
+
hint_set.control_values&.any?
|
|
86
86
|
ps_dict = convert_tt_programs_to_ps_dict(
|
|
87
87
|
hint_set.font_program,
|
|
88
88
|
hint_set.control_value_program,
|
|
89
|
-
hint_set.control_values
|
|
89
|
+
hint_set.control_values,
|
|
90
90
|
)
|
|
91
91
|
result.private_dict_hints = ps_dict.to_json
|
|
92
92
|
end
|
|
@@ -102,7 +102,7 @@ module Fontisan
|
|
|
102
102
|
# Convert font-level PS → TT
|
|
103
103
|
if hint_set.private_dict_hints && hint_set.private_dict_hints != "{}"
|
|
104
104
|
tt_programs = convert_ps_dict_to_tt_programs(
|
|
105
|
-
JSON.parse(hint_set.private_dict_hints)
|
|
105
|
+
JSON.parse(hint_set.private_dict_hints),
|
|
106
106
|
)
|
|
107
107
|
result.font_program = tt_programs[:fpgm]
|
|
108
108
|
result.control_value_program = tt_programs[:prep]
|
|
@@ -137,7 +137,7 @@ module Fontisan
|
|
|
137
137
|
Models::Hint.new(
|
|
138
138
|
type: hint.type,
|
|
139
139
|
data: ps_data,
|
|
140
|
-
source_format: :postscript
|
|
140
|
+
source_format: :postscript,
|
|
141
141
|
)
|
|
142
142
|
rescue StandardError => e
|
|
143
143
|
warn "Failed to convert hint to PostScript: #{e.message}"
|
|
@@ -158,7 +158,7 @@ module Fontisan
|
|
|
158
158
|
Models::Hint.new(
|
|
159
159
|
type: hint.type,
|
|
160
160
|
data: { instructions: tt_instructions },
|
|
161
|
-
source_format: :truetype
|
|
161
|
+
source_format: :truetype,
|
|
162
162
|
)
|
|
163
163
|
rescue StandardError => e
|
|
164
164
|
warn "Failed to convert hint to TrueType: #{e.message}"
|
|
@@ -230,7 +230,8 @@ module Fontisan
|
|
|
230
230
|
# Convert TrueType font programs to PostScript Private dict
|
|
231
231
|
#
|
|
232
232
|
# Analyzes TrueType fpgm, prep, and cvt to extract semantic intent
|
|
233
|
-
# and generate corresponding PostScript hint parameters
|
|
233
|
+
# and generate corresponding PostScript hint parameters using the
|
|
234
|
+
# TrueTypeInstructionAnalyzer.
|
|
234
235
|
#
|
|
235
236
|
# @param fpgm [String] Font program bytecode
|
|
236
237
|
# @param prep [String] Control value program bytecode
|
|
@@ -239,8 +240,8 @@ module Fontisan
|
|
|
239
240
|
def convert_tt_programs_to_ps_dict(fpgm, prep, cvt)
|
|
240
241
|
hints = {}
|
|
241
242
|
|
|
242
|
-
# Extract stem widths from
|
|
243
|
-
# CVT values typically contain standard widths
|
|
243
|
+
# Extract stem widths from CVT if present
|
|
244
|
+
# CVT values typically contain standard widths at the beginning
|
|
244
245
|
if cvt && !cvt.empty?
|
|
245
246
|
# First CVT value often represents standard horizontal stem
|
|
246
247
|
hints[:std_hw] = cvt[0].abs if cvt.length > 0
|
|
@@ -248,14 +249,36 @@ module Fontisan
|
|
|
248
249
|
hints[:std_vw] = cvt[1].abs if cvt.length > 1
|
|
249
250
|
end
|
|
250
251
|
|
|
251
|
-
#
|
|
252
|
-
|
|
252
|
+
# Use the instruction analyzer to extract additional hint parameters
|
|
253
|
+
analyzer = TrueTypeInstructionAnalyzer.new
|
|
254
|
+
|
|
255
|
+
# Analyze prep program if present
|
|
256
|
+
prep_hints = if prep && !prep.empty?
|
|
257
|
+
analyzer.analyze_prep(prep, cvt)
|
|
258
|
+
else
|
|
259
|
+
{}
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Analyze fpgm program complexity
|
|
263
|
+
fpgm_hints = if fpgm && !fpgm.empty?
|
|
264
|
+
analyzer.analyze_fpgm(fpgm)
|
|
265
|
+
else
|
|
266
|
+
{}
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Extract blue zones from CVT if present
|
|
270
|
+
blue_zones = if cvt && !cvt.empty?
|
|
271
|
+
analyzer.extract_blue_zones_from_cvt(cvt)
|
|
272
|
+
else
|
|
273
|
+
{}
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Merge all extracted hints (prep_hints and fpgm_hints override stem widths if present)
|
|
277
|
+
hints.merge!(prep_hints).merge!(fpgm_hints).merge!(blue_zones)
|
|
278
|
+
|
|
279
|
+
# Provide default blue_values if none were detected
|
|
253
280
|
# These are standard values that work for most Latin fonts
|
|
254
|
-
hints[:blue_values]
|
|
255
|
-
|
|
256
|
-
# Optional: Add other_blues for descenders if we detect them
|
|
257
|
-
# This would require analyzing prep program, which is complex
|
|
258
|
-
# For now, use conservative defaults
|
|
281
|
+
hints[:blue_values] ||= [-20, 0, 706, 726]
|
|
259
282
|
|
|
260
283
|
hints
|
|
261
284
|
rescue StandardError => e
|
|
@@ -266,44 +289,17 @@ module Fontisan
|
|
|
266
289
|
# Convert PostScript Private dict to TrueType font programs
|
|
267
290
|
#
|
|
268
291
|
# Generates TrueType control values and programs from PostScript
|
|
269
|
-
# hint parameters.
|
|
292
|
+
# hint parameters using the TrueTypeInstructionGenerator.
|
|
270
293
|
#
|
|
271
294
|
# @param ps_dict [Hash] PostScript Private dict parameters
|
|
272
295
|
# @return [Hash] TrueType programs ({ fpgm:, prep:, cvt: })
|
|
273
296
|
def convert_ps_dict_to_tt_programs(ps_dict)
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
# Generate control values from PS parameters
|
|
278
|
-
cvt = []
|
|
279
|
-
|
|
280
|
-
# Add standard stem widths as CVT values
|
|
281
|
-
cvt << ps_dict[:std_hw] if ps_dict[:std_hw]
|
|
282
|
-
cvt << ps_dict[:std_vw] if ps_dict[:std_vw]
|
|
283
|
-
|
|
284
|
-
# Add stem snap values if present
|
|
285
|
-
if ps_dict[:stem_snap_h]&.is_a?(Array)
|
|
286
|
-
cvt.concat(ps_dict[:stem_snap_h])
|
|
287
|
-
end
|
|
288
|
-
if ps_dict[:stem_snap_v]&.is_a?(Array)
|
|
289
|
-
cvt.concat(ps_dict[:stem_snap_v])
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
# Remove duplicates and sort
|
|
293
|
-
cvt = cvt.uniq.sort
|
|
294
|
-
|
|
295
|
-
# Generate basic prep program (empty for converted fonts)
|
|
296
|
-
# A real implementation would generate instructions to set up CVT
|
|
297
|
-
prep = ""
|
|
298
|
-
|
|
299
|
-
# fpgm typically empty for converted fonts
|
|
300
|
-
# Functions would need to be synthesized from scratch
|
|
301
|
-
fpgm = ""
|
|
302
|
-
|
|
303
|
-
{ fpgm: fpgm, prep: prep, cvt: cvt }
|
|
297
|
+
# Use the instruction generator to create real TrueType programs
|
|
298
|
+
generator = TrueTypeInstructionGenerator.new
|
|
299
|
+
generator.generate(ps_dict)
|
|
304
300
|
rescue StandardError => e
|
|
305
301
|
warn "Error converting PS dict to TT programs: #{e.message}"
|
|
306
|
-
{ fpgm: "", prep: "", cvt: [] }
|
|
302
|
+
{ fpgm: "".b, prep: "".b, cvt: [] }
|
|
307
303
|
end
|
|
308
304
|
end
|
|
309
305
|
end
|
|
@@ -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
|
|
@@ -147,14 +147,12 @@ module Fontisan
|
|
|
147
147
|
# Prepare per-glyph hint data if present
|
|
148
148
|
per_glyph_hints = if has_per_glyph_hints
|
|
149
149
|
extract_per_glyph_hints(hint_set)
|
|
150
|
-
else
|
|
151
|
-
nil
|
|
152
150
|
end
|
|
153
151
|
|
|
154
152
|
new_cff_data = Tables::Cff::TableBuilder.rebuild(
|
|
155
153
|
cff_table,
|
|
156
154
|
private_dict_hints: hint_params,
|
|
157
|
-
per_glyph_hints: per_glyph_hints
|
|
155
|
+
per_glyph_hints: per_glyph_hints,
|
|
158
156
|
)
|
|
159
157
|
|
|
160
158
|
tables["CFF "] = new_cff_data
|
|
@@ -91,12 +91,12 @@ module Fontisan
|
|
|
91
91
|
if operator?(byte)
|
|
92
92
|
# Process operator
|
|
93
93
|
operator = if byte == 12
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
# Two-byte operator
|
|
95
|
+
i += 1
|
|
96
|
+
(12 << 8) | bytes[i]
|
|
97
|
+
else
|
|
98
|
+
byte
|
|
99
|
+
end
|
|
100
100
|
|
|
101
101
|
hint = process_operator(operator, stack)
|
|
102
102
|
hints << hint if hint
|
|
@@ -165,7 +165,7 @@ module Fontisan
|
|
|
165
165
|
# 5-byte signed integer
|
|
166
166
|
if index + 4 < bytes.length
|
|
167
167
|
num = (bytes[index + 1] << 24) | (bytes[index + 2] << 16) |
|
|
168
|
-
|
|
168
|
+
(bytes[index + 3] << 8) | bytes[index + 4]
|
|
169
169
|
num = num - 4294967296 if num > 2147483647
|
|
170
170
|
[num, 5]
|
|
171
171
|
else
|
|
@@ -204,7 +204,7 @@ module Fontisan
|
|
|
204
204
|
Models::Hint.new(
|
|
205
205
|
type: :hint_replacement,
|
|
206
206
|
data: { mask: stack.dup },
|
|
207
|
-
source_format: :postscript
|
|
207
|
+
source_format: :postscript,
|
|
208
208
|
)
|
|
209
209
|
|
|
210
210
|
when CNTRMASK
|
|
@@ -212,11 +212,9 @@ module Fontisan
|
|
|
212
212
|
Models::Hint.new(
|
|
213
213
|
type: :counter,
|
|
214
214
|
data: { zones: stack.dup },
|
|
215
|
-
source_format: :postscript
|
|
215
|
+
source_format: :postscript,
|
|
216
216
|
)
|
|
217
217
|
|
|
218
|
-
else
|
|
219
|
-
nil
|
|
220
218
|
end
|
|
221
219
|
end
|
|
222
220
|
|
|
@@ -238,9 +236,9 @@ module Fontisan
|
|
|
238
236
|
data: {
|
|
239
237
|
position: position,
|
|
240
238
|
width: width,
|
|
241
|
-
orientation: orientation
|
|
239
|
+
orientation: orientation,
|
|
242
240
|
},
|
|
243
|
-
source_format: :postscript
|
|
241
|
+
source_format: :postscript,
|
|
244
242
|
)
|
|
245
243
|
end
|
|
246
244
|
|
|
@@ -258,7 +256,7 @@ module Fontisan
|
|
|
258
256
|
pos_idx = i * 2
|
|
259
257
|
stems << {
|
|
260
258
|
position: stack[pos_idx],
|
|
261
|
-
width: stack[pos_idx + 1]
|
|
259
|
+
width: stack[pos_idx + 1],
|
|
262
260
|
}
|
|
263
261
|
end
|
|
264
262
|
|
|
@@ -266,9 +264,9 @@ module Fontisan
|
|
|
266
264
|
type: :stem3,
|
|
267
265
|
data: {
|
|
268
266
|
stems: stems,
|
|
269
|
-
orientation: orientation
|
|
267
|
+
orientation: orientation,
|
|
270
268
|
},
|
|
271
|
-
source_format: :postscript
|
|
269
|
+
source_format: :postscript,
|
|
272
270
|
)
|
|
273
271
|
end
|
|
274
272
|
|
|
@@ -293,19 +291,58 @@ module Fontisan
|
|
|
293
291
|
|
|
294
292
|
# Extract hint-related parameters from Private DICT
|
|
295
293
|
# These are the key hinting parameters in CFF
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
294
|
+
if private_dict.respond_to?(:blue_values)
|
|
295
|
+
hints[:blue_values] =
|
|
296
|
+
private_dict.blue_values
|
|
297
|
+
end
|
|
298
|
+
if private_dict.respond_to?(:other_blues)
|
|
299
|
+
hints[:other_blues] =
|
|
300
|
+
private_dict.other_blues
|
|
301
|
+
end
|
|
302
|
+
if private_dict.respond_to?(:family_blues)
|
|
303
|
+
hints[:family_blues] =
|
|
304
|
+
private_dict.family_blues
|
|
305
|
+
end
|
|
306
|
+
if private_dict.respond_to?(:family_other_blues)
|
|
307
|
+
hints[:family_other_blues] =
|
|
308
|
+
private_dict.family_other_blues
|
|
309
|
+
end
|
|
310
|
+
if private_dict.respond_to?(:blue_scale)
|
|
311
|
+
hints[:blue_scale] =
|
|
312
|
+
private_dict.blue_scale
|
|
313
|
+
end
|
|
314
|
+
if private_dict.respond_to?(:blue_shift)
|
|
315
|
+
hints[:blue_shift] =
|
|
316
|
+
private_dict.blue_shift
|
|
317
|
+
end
|
|
318
|
+
if private_dict.respond_to?(:blue_fuzz)
|
|
319
|
+
hints[:blue_fuzz] =
|
|
320
|
+
private_dict.blue_fuzz
|
|
321
|
+
end
|
|
322
|
+
if private_dict.respond_to?(:std_hw)
|
|
323
|
+
hints[:std_hw] =
|
|
324
|
+
private_dict.std_hw
|
|
325
|
+
end
|
|
326
|
+
if private_dict.respond_to?(:std_vw)
|
|
327
|
+
hints[:std_vw] =
|
|
328
|
+
private_dict.std_vw
|
|
329
|
+
end
|
|
330
|
+
if private_dict.respond_to?(:stem_snap_h)
|
|
331
|
+
hints[:stem_snap_h] =
|
|
332
|
+
private_dict.stem_snap_h
|
|
333
|
+
end
|
|
334
|
+
if private_dict.respond_to?(:stem_snap_v)
|
|
335
|
+
hints[:stem_snap_v] =
|
|
336
|
+
private_dict.stem_snap_v
|
|
337
|
+
end
|
|
338
|
+
if private_dict.respond_to?(:force_bold)
|
|
339
|
+
hints[:force_bold] =
|
|
340
|
+
private_dict.force_bold
|
|
341
|
+
end
|
|
342
|
+
if private_dict.respond_to?(:language_group)
|
|
343
|
+
hints[:language_group] =
|
|
344
|
+
private_dict.language_group
|
|
345
|
+
end
|
|
309
346
|
|
|
310
347
|
hints.compact
|
|
311
348
|
rescue StandardError => e
|
|
@@ -331,20 +368,18 @@ module Fontisan
|
|
|
331
368
|
# Iterate through all glyphs
|
|
332
369
|
glyph_count = cff_table.glyph_count(0)
|
|
333
370
|
(0...glyph_count).each do |glyph_id|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
warn "Failed to extract hints for glyph #{glyph_id}: #{e.message}"
|
|
347
|
-
end
|
|
371
|
+
# Get CharString for this glyph
|
|
372
|
+
charstring = cff_table.charstring_for_glyph(glyph_id, 0)
|
|
373
|
+
next unless charstring
|
|
374
|
+
|
|
375
|
+
# Extract hints from CharString
|
|
376
|
+
hints = extract(charstring)
|
|
377
|
+
next if hints.empty?
|
|
378
|
+
|
|
379
|
+
# Store glyph hints
|
|
380
|
+
hint_set.add_glyph_hints(glyph_id, hints)
|
|
381
|
+
rescue StandardError => e
|
|
382
|
+
warn "Failed to extract hints for glyph #{glyph_id}: #{e.message}"
|
|
348
383
|
end
|
|
349
384
|
rescue StandardError => e
|
|
350
385
|
warn "Failed to extract CharString hints: #{e.message}"
|