fontisan 0.2.1 → 0.2.3
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 +58 -392
- data/README.adoc +1509 -1430
- 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/base_collection.rb +296 -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 +129 -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 +120 -30
- data/lib/fontisan/font_writer.rb +2 -0
- data/lib/fontisan/formatters/text_formatter.rb +116 -19
- 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 +37 -0
- data/lib/fontisan/models/collection_info.rb +6 -1
- 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_collection.rb +17 -220
- 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_collection.rb +29 -113
- 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 +9 -2
|
@@ -29,7 +29,7 @@ module Fontisan
|
|
|
29
29
|
MDRP_MIN_RND_BLACK = 0xC0
|
|
30
30
|
IUP_Y = 0x30
|
|
31
31
|
IUP_X = 0x31
|
|
32
|
-
SHP = [0x32, 0x33]
|
|
32
|
+
SHP = [0x32, 0x33].freeze
|
|
33
33
|
ALIGNRP = 0x3C
|
|
34
34
|
DELTAP1 = 0x5D
|
|
35
35
|
DELTAP2 = 0x71
|
|
@@ -104,7 +104,7 @@ module Fontisan
|
|
|
104
104
|
hints << Models::Hint.new(
|
|
105
105
|
type: :interpolate,
|
|
106
106
|
data: { axis: opcode == IUP_Y ? :y : :x },
|
|
107
|
-
source_format: :truetype
|
|
107
|
+
source_format: :truetype,
|
|
108
108
|
)
|
|
109
109
|
i += 1
|
|
110
110
|
|
|
@@ -113,7 +113,7 @@ module Fontisan
|
|
|
113
113
|
hints << Models::Hint.new(
|
|
114
114
|
type: :shift,
|
|
115
115
|
data: { instructions: [opcode] },
|
|
116
|
-
source_format: :truetype
|
|
116
|
+
source_format: :truetype,
|
|
117
117
|
)
|
|
118
118
|
i += 1
|
|
119
119
|
|
|
@@ -122,7 +122,7 @@ module Fontisan
|
|
|
122
122
|
hints << Models::Hint.new(
|
|
123
123
|
type: :align,
|
|
124
124
|
data: {},
|
|
125
|
-
source_format: :truetype
|
|
125
|
+
source_format: :truetype,
|
|
126
126
|
)
|
|
127
127
|
i += 1
|
|
128
128
|
|
|
@@ -137,9 +137,9 @@ module Fontisan
|
|
|
137
137
|
type: :delta,
|
|
138
138
|
data: {
|
|
139
139
|
instructions: [opcode] + [count] + delta_data,
|
|
140
|
-
count: count
|
|
140
|
+
count: count,
|
|
141
141
|
},
|
|
142
|
-
source_format: :truetype
|
|
142
|
+
source_format: :truetype,
|
|
143
143
|
)
|
|
144
144
|
i += count * 2 + 1
|
|
145
145
|
end
|
|
@@ -165,7 +165,7 @@ module Fontisan
|
|
|
165
165
|
|
|
166
166
|
# Check if next instruction is MDRP (stem width)
|
|
167
167
|
has_width = index + 1 < bytes.length &&
|
|
168
|
-
|
|
168
|
+
bytes[index + 1] == MDRP_MIN_RND_BLACK
|
|
169
169
|
|
|
170
170
|
if has_width
|
|
171
171
|
Models::Hint.new(
|
|
@@ -173,12 +173,10 @@ module Fontisan
|
|
|
173
173
|
data: {
|
|
174
174
|
position: 0, # Would be extracted from graphics state
|
|
175
175
|
width: 0, # Would be calculated from MDRP
|
|
176
|
-
orientation: :vertical # Inferred from instruction context
|
|
176
|
+
orientation: :vertical, # Inferred from instruction context
|
|
177
177
|
},
|
|
178
|
-
source_format: :truetype
|
|
178
|
+
source_format: :truetype,
|
|
179
179
|
)
|
|
180
|
-
else
|
|
181
|
-
nil
|
|
182
180
|
end
|
|
183
181
|
end
|
|
184
182
|
|
|
@@ -265,21 +263,19 @@ module Fontisan
|
|
|
265
263
|
|
|
266
264
|
# Iterate through all glyphs
|
|
267
265
|
(0...num_glyphs).each do |glyph_id|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
next
|
|
282
|
-
end
|
|
266
|
+
glyph = glyf_table.glyph_for(glyph_id)
|
|
267
|
+
next unless glyph
|
|
268
|
+
next if glyph.number_of_contours <= 0 # Skip compound glyphs and empty glyphs
|
|
269
|
+
|
|
270
|
+
# Extract hints from simple glyph instructions
|
|
271
|
+
hints = extract(glyph)
|
|
272
|
+
next if hints.empty?
|
|
273
|
+
|
|
274
|
+
# Store glyph hints
|
|
275
|
+
hint_set.add_glyph_hints(glyph_id, hints)
|
|
276
|
+
rescue StandardError
|
|
277
|
+
# Skip glyphs that fail to parse
|
|
278
|
+
next
|
|
283
279
|
end
|
|
284
280
|
rescue StandardError => e
|
|
285
281
|
warn "Failed to extract glyph hints: #{e.message}"
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Hints
|
|
5
|
+
# Analyzes TrueType bytecode instructions to extract hint parameters
|
|
6
|
+
#
|
|
7
|
+
# This analyzer parses fpgm (Font Program) and prep (Control Value Program)
|
|
8
|
+
# bytecode to extract semantic hint information that can be converted to
|
|
9
|
+
# PostScript Private dict parameters.
|
|
10
|
+
#
|
|
11
|
+
# **Key Extracted Parameters:**
|
|
12
|
+
#
|
|
13
|
+
# - Blue zones (alignment zones for baseline, x-height, cap-height)
|
|
14
|
+
# - Stem widths (from CVT setup in prep)
|
|
15
|
+
# - Delta base and shift values
|
|
16
|
+
# - Twilight zone setup
|
|
17
|
+
#
|
|
18
|
+
# @example Analyze prep program
|
|
19
|
+
# analyzer = TrueTypeInstructionAnalyzer.new
|
|
20
|
+
# params = analyzer.analyze_prep(prep_bytecode, cvt_values)
|
|
21
|
+
class TrueTypeInstructionAnalyzer
|
|
22
|
+
# TrueType instruction opcodes relevant for hint extraction
|
|
23
|
+
NPUSHB = 0x40 # Push N bytes
|
|
24
|
+
NPUSHW = 0x41 # Push N words
|
|
25
|
+
PUSHB = (0xB0..0xB7).to_a # Push 1-8 bytes
|
|
26
|
+
PUSHW = (0xB8..0xBF).to_a # Push 1-8 words
|
|
27
|
+
SVTCA_Y = 0x00 # Set freedom and projection vectors to Y-axis
|
|
28
|
+
SVTCA_X = 0x01 # Set freedom and projection vectors to X-axis
|
|
29
|
+
RCVT = 0x45 # Read CVT
|
|
30
|
+
WCVTP = 0x44 # Write CVT (in Pixels)
|
|
31
|
+
WCVTF = 0x70 # Write CVT (in FUnits)
|
|
32
|
+
MDAP = [0x2E, 0x2F].freeze # Move Direct Absolute Point
|
|
33
|
+
SCVTCI = 0x1D # Set Control Value Table Cut In
|
|
34
|
+
SSWCI = 0x1E # Set Single Width Cut In
|
|
35
|
+
SSW = 0x1F # Set Single Width
|
|
36
|
+
|
|
37
|
+
# Analyze prep program to extract hint parameters
|
|
38
|
+
#
|
|
39
|
+
# @param prep [String] Control value program bytecode
|
|
40
|
+
# @param cvt [Array<Integer>] Control values
|
|
41
|
+
# @return [Hash] Extracted hint parameters
|
|
42
|
+
def analyze_prep(prep, cvt = [])
|
|
43
|
+
return {} if prep.nil? && (cvt.nil? || cvt.empty?)
|
|
44
|
+
|
|
45
|
+
params = {}
|
|
46
|
+
|
|
47
|
+
# Parse prep bytecode if present
|
|
48
|
+
if prep && !prep.empty?
|
|
49
|
+
bytes = prep.bytes
|
|
50
|
+
stack = []
|
|
51
|
+
i = 0
|
|
52
|
+
|
|
53
|
+
while i < bytes.length
|
|
54
|
+
opcode = bytes[i]
|
|
55
|
+
|
|
56
|
+
case opcode
|
|
57
|
+
when NPUSHB
|
|
58
|
+
# Push N bytes
|
|
59
|
+
i += 1
|
|
60
|
+
count = bytes[i]
|
|
61
|
+
i += 1
|
|
62
|
+
count.times do
|
|
63
|
+
stack.push(bytes[i])
|
|
64
|
+
i += 1
|
|
65
|
+
end
|
|
66
|
+
next
|
|
67
|
+
|
|
68
|
+
when NPUSHW
|
|
69
|
+
# Push N words (16-bit values)
|
|
70
|
+
i += 1
|
|
71
|
+
count = bytes[i]
|
|
72
|
+
i += 1
|
|
73
|
+
count.times do
|
|
74
|
+
value = (bytes[i] << 8) | bytes[i + 1]
|
|
75
|
+
# Convert to signed
|
|
76
|
+
value = value - 65536 if value > 32767
|
|
77
|
+
stack.push(value)
|
|
78
|
+
i += 2
|
|
79
|
+
end
|
|
80
|
+
next
|
|
81
|
+
|
|
82
|
+
when *PUSHB
|
|
83
|
+
# Push 1-8 bytes
|
|
84
|
+
count = opcode - 0xB0 + 1
|
|
85
|
+
i += 1
|
|
86
|
+
count.times do
|
|
87
|
+
stack.push(bytes[i])
|
|
88
|
+
i += 1
|
|
89
|
+
end
|
|
90
|
+
next
|
|
91
|
+
|
|
92
|
+
when *PUSHW
|
|
93
|
+
# Push 1-8 words
|
|
94
|
+
count = opcode - 0xB8 + 1
|
|
95
|
+
i += 1
|
|
96
|
+
count.times do
|
|
97
|
+
value = (bytes[i] << 8) | bytes[i + 1]
|
|
98
|
+
value = value - 65536 if value > 32767
|
|
99
|
+
stack.push(value)
|
|
100
|
+
i += 2
|
|
101
|
+
end
|
|
102
|
+
next
|
|
103
|
+
|
|
104
|
+
when WCVTP, WCVTF
|
|
105
|
+
# Write to CVT - this shows which CVT indices are being set up
|
|
106
|
+
# Pattern: value cvt_index WCVTP (stack top to bottom)
|
|
107
|
+
if stack.length >= 2
|
|
108
|
+
value = stack.pop
|
|
109
|
+
cvt_index = stack.pop
|
|
110
|
+
# Track CVT modifications (useful for understanding setup)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
when SSW
|
|
114
|
+
# Set Single Width - used for stem width control
|
|
115
|
+
if stack.length >= 1
|
|
116
|
+
width = stack.pop
|
|
117
|
+
params[:single_width] = width unless params[:single_width]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
when SSWCI
|
|
121
|
+
# Set Single Width Cut In
|
|
122
|
+
if stack.length >= 1
|
|
123
|
+
params[:single_width_cut_in] = stack.pop
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
when SCVTCI
|
|
127
|
+
# Set CVT Cut In
|
|
128
|
+
if stack.length >= 1
|
|
129
|
+
params[:cvt_cut_in] = stack.pop
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
i += 1
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Extract blue zones from CVT analysis (always do this if CVT is present)
|
|
138
|
+
if cvt && !cvt.empty?
|
|
139
|
+
params.merge!(extract_blue_zones_from_cvt(cvt))
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
params
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
warn "Error analyzing prep program: #{e.message}"
|
|
145
|
+
{}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Analyze Font Program (fpgm) for complexity indicators
|
|
149
|
+
#
|
|
150
|
+
# The fpgm contains font-level function definitions. While we don't
|
|
151
|
+
# fully decompile it, we can extract useful metadata about hint complexity.
|
|
152
|
+
#
|
|
153
|
+
# @param fpgm [String] Binary fpgm data
|
|
154
|
+
# @return [Hash] Analysis results with complexity indicators
|
|
155
|
+
def analyze_fpgm(fpgm)
|
|
156
|
+
return {} if fpgm.nil? || fpgm.empty?
|
|
157
|
+
|
|
158
|
+
size = fpgm.bytesize
|
|
159
|
+
|
|
160
|
+
# Estimate complexity based on size
|
|
161
|
+
complexity = if size < 100
|
|
162
|
+
:simple
|
|
163
|
+
elsif size < 200
|
|
164
|
+
:moderate
|
|
165
|
+
else
|
|
166
|
+
:complex
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
{
|
|
170
|
+
fpgm_size: size,
|
|
171
|
+
has_functions: size > 0,
|
|
172
|
+
complexity: complexity,
|
|
173
|
+
}
|
|
174
|
+
rescue StandardError
|
|
175
|
+
# Return empty hash on any error
|
|
176
|
+
{}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Extract blue zones from CVT values using heuristics
|
|
180
|
+
#
|
|
181
|
+
# Blue zones in PostScript define alignment constraints for
|
|
182
|
+
# baseline, x-height, cap-height, ascender, and descender.
|
|
183
|
+
# TrueType doesn't have explicit blue zones, but we can derive
|
|
184
|
+
# them from CVT values using common patterns.
|
|
185
|
+
#
|
|
186
|
+
# Heuristics:
|
|
187
|
+
# - Negative values near -250 to -200: Descender zones
|
|
188
|
+
# - Values near 0: Baseline zones
|
|
189
|
+
# - Values near 500-550: X-height zones
|
|
190
|
+
# - Values near 700-750: Cap-height zones
|
|
191
|
+
# - For large UPM (>2000): Scale thresholds proportionally
|
|
192
|
+
#
|
|
193
|
+
# @param cvt [Array<Integer>] Control Value Table entries
|
|
194
|
+
# @return [Hash] Extracted blue zone parameters
|
|
195
|
+
def extract_blue_zones_from_cvt(cvt)
|
|
196
|
+
return {} if cvt.nil? || cvt.empty?
|
|
197
|
+
|
|
198
|
+
zones = {}
|
|
199
|
+
|
|
200
|
+
# Detect scale from maximum absolute value
|
|
201
|
+
max_value = cvt.map(&:abs).max
|
|
202
|
+
scale_factor = max_value > 1000 ? (max_value / 1000.0) : 1.0
|
|
203
|
+
|
|
204
|
+
# Scaled thresholds
|
|
205
|
+
descender_min = (-300 * scale_factor).to_i
|
|
206
|
+
descender_max = (-150 * scale_factor).to_i
|
|
207
|
+
baseline_range = (50 * scale_factor).to_i
|
|
208
|
+
xheight_min = (450 * scale_factor).to_i
|
|
209
|
+
xheight_max = (600 * scale_factor).to_i
|
|
210
|
+
capheight_min = (650 * scale_factor).to_i
|
|
211
|
+
capheight_max = (1500 * scale_factor).to_i # Wider range for large UPM
|
|
212
|
+
|
|
213
|
+
# Group CVT values by typical alignment zones
|
|
214
|
+
descender_values = cvt.select { |v| v < descender_max && v > descender_min }
|
|
215
|
+
baseline_values = cvt.select { |v| v >= -baseline_range && v <= baseline_range }
|
|
216
|
+
xheight_values = cvt.select { |v| v >= xheight_min && v <= xheight_max }
|
|
217
|
+
capheight_values = cvt.select { |v| v >= capheight_min && v <= capheight_max }
|
|
218
|
+
|
|
219
|
+
# Build blue_values (baseline and top zones)
|
|
220
|
+
blue_values = []
|
|
221
|
+
|
|
222
|
+
# Add baseline zone if detected
|
|
223
|
+
if baseline_values.any?
|
|
224
|
+
min_baseline = baseline_values.min
|
|
225
|
+
max_baseline = baseline_values.max
|
|
226
|
+
blue_values << min_baseline << max_baseline
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Add cap-height zone if detected (or any top zone for large UPM)
|
|
230
|
+
if capheight_values.any?
|
|
231
|
+
min_cap = capheight_values.min
|
|
232
|
+
max_cap = capheight_values.max
|
|
233
|
+
blue_values << min_cap << max_cap
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
zones[:blue_values] = blue_values unless blue_values.empty?
|
|
237
|
+
|
|
238
|
+
# Build other_blues (descender zones)
|
|
239
|
+
if descender_values.any?
|
|
240
|
+
min_desc = descender_values.min
|
|
241
|
+
max_desc = descender_values.max
|
|
242
|
+
zones[:other_blues] = [min_desc, max_desc]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
zones
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
private
|
|
249
|
+
|
|
250
|
+
# Estimate complexity of bytecode program
|
|
251
|
+
#
|
|
252
|
+
# @param bytes [Array<Integer>] Bytecode
|
|
253
|
+
# @return [Symbol] Complexity level (:simple, :moderate, :complex)
|
|
254
|
+
def estimate_complexity(bytes)
|
|
255
|
+
return :simple if bytes.length < 50
|
|
256
|
+
return :moderate if bytes.length < 200
|
|
257
|
+
:complex
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Hints
|
|
5
|
+
# Generates TrueType instruction bytecode from PostScript hint parameters
|
|
6
|
+
#
|
|
7
|
+
# This class is the inverse of TrueTypeInstructionAnalyzer - it takes
|
|
8
|
+
# PostScript hint parameters and generates equivalent TrueType prep/fpgm
|
|
9
|
+
# programs and CVT values.
|
|
10
|
+
#
|
|
11
|
+
# TrueType Instruction Opcodes:
|
|
12
|
+
# - NPUSHB (0x40): Push n bytes
|
|
13
|
+
# - NPUSHW (0x41): Push n words (16-bit)
|
|
14
|
+
# - PUSHB[n] (0xB0-0xB7): Push 1-8 bytes
|
|
15
|
+
# - PUSHW[n] (0xB8-0xBF): Push 1-8 words
|
|
16
|
+
# - SSW (0x1F): Set Single Width
|
|
17
|
+
# - SSWCI (0x1E): Set Single Width Cut-In
|
|
18
|
+
# - SCVTCI (0x1D): Set CVT Cut-In
|
|
19
|
+
# - WCVTP (0x44): Write CVT in Pixels
|
|
20
|
+
# - WCVTF (0x70): Write CVT in FUnits
|
|
21
|
+
#
|
|
22
|
+
# @example Generate TrueType programs
|
|
23
|
+
# generator = TrueTypeInstructionGenerator.new
|
|
24
|
+
# programs = generator.generate({
|
|
25
|
+
# blue_scale: 0.039625,
|
|
26
|
+
# std_hw: 80,
|
|
27
|
+
# std_vw: 90
|
|
28
|
+
# })
|
|
29
|
+
# programs[:prep] # => Binary prep program
|
|
30
|
+
# programs[:fpgm] # => Binary fpgm program (usually empty)
|
|
31
|
+
# programs[:cvt] # => Array of CVT values
|
|
32
|
+
class TrueTypeInstructionGenerator
|
|
33
|
+
# TrueType instruction opcodes
|
|
34
|
+
NPUSHB = 0x40 # Push n bytes
|
|
35
|
+
NPUSHW = 0x41 # Push n words (16-bit)
|
|
36
|
+
PUSHB_BASE = 0xB0 # PUSHB[0] through PUSHB[7]
|
|
37
|
+
PUSHW_BASE = 0xB8 # PUSHW[0] through PUSHW[7]
|
|
38
|
+
SSW = 0x1F # Set Single Width
|
|
39
|
+
SSWCI = 0x1E # Set Single Width Cut-In
|
|
40
|
+
SCVTCI = 0x1D # Set CVT Cut-In
|
|
41
|
+
WCVTP = 0x44 # Write CVT in Pixels
|
|
42
|
+
WCVTF = 0x70 # Write CVT in FUnits
|
|
43
|
+
|
|
44
|
+
# Size thresholds for instruction selection
|
|
45
|
+
MAX_PUSHB_INLINE = 8 # Maximum bytes for PUSHB[n]
|
|
46
|
+
MAX_PUSHW_INLINE = 8 # Maximum words for PUSHW[n]
|
|
47
|
+
BYTE_MAX = 255 # Maximum value for byte
|
|
48
|
+
WORD_MAX = 65535 # Maximum value for word
|
|
49
|
+
|
|
50
|
+
# Generate TrueType programs and CVT from PostScript parameters
|
|
51
|
+
#
|
|
52
|
+
# @param ps_params [Hash] PostScript hint parameters
|
|
53
|
+
# @option ps_params [Float] :blue_scale Blue scale value (0.0-1.0)
|
|
54
|
+
# @option ps_params [Integer] :std_hw Standard horizontal width
|
|
55
|
+
# @option ps_params [Integer] :std_vw Standard vertical width
|
|
56
|
+
# @option ps_params [Array<Integer>] :stem_snap_h Horizontal stem snap values
|
|
57
|
+
# @option ps_params [Array<Integer>] :stem_snap_v Vertical stem snap values
|
|
58
|
+
# @option ps_params [Array<Integer>] :blue_values Blue zone values
|
|
59
|
+
# @option ps_params [Array<Integer>] :other_blues Other blue zone values
|
|
60
|
+
# @return [Hash] Hash with :prep, :fpgm, and :cvt keys
|
|
61
|
+
def generate(ps_params)
|
|
62
|
+
# Normalize keys to symbols
|
|
63
|
+
ps_params = normalize_keys(ps_params)
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
fpgm: generate_fpgm(ps_params),
|
|
67
|
+
prep: generate_prep(ps_params),
|
|
68
|
+
cvt: generate_cvt(ps_params)
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Generate prep (Control Value Program) from PostScript parameters
|
|
73
|
+
#
|
|
74
|
+
# The prep program sets up global hint parameters:
|
|
75
|
+
# - CVT Cut-In (from blue_scale)
|
|
76
|
+
# - Single Width Cut-In (from std_hw/std_vw)
|
|
77
|
+
# - Single Width (from std_hw or std_vw)
|
|
78
|
+
#
|
|
79
|
+
# @param ps_params [Hash] PostScript parameters
|
|
80
|
+
# @return [String] Binary instruction bytes
|
|
81
|
+
def generate_prep(ps_params)
|
|
82
|
+
instructions = []
|
|
83
|
+
|
|
84
|
+
# Set CVT Cut-In from blue_scale if present
|
|
85
|
+
if ps_params[:blue_scale]
|
|
86
|
+
cvt_cut_in = calculate_cvt_cut_in(ps_params[:blue_scale])
|
|
87
|
+
instructions.concat(push_value(cvt_cut_in))
|
|
88
|
+
instructions << SCVTCI
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Set Single Width Cut-In if we have stem widths
|
|
92
|
+
if ps_params[:std_hw] || ps_params[:std_vw]
|
|
93
|
+
sw_cut_in = calculate_sw_cut_in(ps_params)
|
|
94
|
+
instructions.concat(push_value(sw_cut_in))
|
|
95
|
+
instructions << SSWCI
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Set Single Width (prefer horizontal, fall back to vertical)
|
|
99
|
+
single_width = ps_params[:std_hw] || ps_params[:std_vw]
|
|
100
|
+
if single_width
|
|
101
|
+
instructions.concat(push_value(single_width))
|
|
102
|
+
instructions << SSW
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
instructions.pack("C*")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Generate fpgm (Font Program) from PostScript parameters
|
|
109
|
+
#
|
|
110
|
+
# For converted fonts, fpgm is typically empty as font-level
|
|
111
|
+
# functions are not needed for basic hint conversion.
|
|
112
|
+
#
|
|
113
|
+
# @param _ps_params [Hash] PostScript parameters (unused)
|
|
114
|
+
# @return [String] Binary instruction bytes (empty for converted fonts)
|
|
115
|
+
def generate_fpgm(_ps_params)
|
|
116
|
+
# For converted fonts, fpgm is typically empty
|
|
117
|
+
# Advanced implementations might generate function definitions here
|
|
118
|
+
"".b
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Generate CVT (Control Value Table) from PostScript parameters
|
|
122
|
+
#
|
|
123
|
+
# CVT entries are derived from:
|
|
124
|
+
# - stem_snap_h/stem_snap_v: Stem widths
|
|
125
|
+
# - blue_values/other_blues: Alignment zones
|
|
126
|
+
# - std_hw/std_vw: Standard widths
|
|
127
|
+
#
|
|
128
|
+
# Duplicates are removed and values sorted for optimal CVT organization.
|
|
129
|
+
#
|
|
130
|
+
# @param ps_params [Hash] PostScript parameters
|
|
131
|
+
# @return [Array<Integer>] Array of 16-bit signed integers
|
|
132
|
+
def generate_cvt(ps_params)
|
|
133
|
+
cvt = []
|
|
134
|
+
|
|
135
|
+
# Add standard widths to CVT
|
|
136
|
+
cvt << ps_params[:std_hw] if ps_params[:std_hw]
|
|
137
|
+
cvt << ps_params[:std_vw] if ps_params[:std_vw]
|
|
138
|
+
|
|
139
|
+
# Add stem snap values
|
|
140
|
+
if ps_params[:stem_snap_h]
|
|
141
|
+
cvt.concat(ps_params[:stem_snap_h])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if ps_params[:stem_snap_v]
|
|
145
|
+
cvt.concat(ps_params[:stem_snap_v])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Add blue zone values (as pairs: bottom, top)
|
|
149
|
+
if ps_params[:blue_values]
|
|
150
|
+
cvt.concat(ps_params[:blue_values])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if ps_params[:other_blues]
|
|
154
|
+
cvt.concat(ps_params[:other_blues])
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Remove duplicates and sort for optimal CVT organization
|
|
158
|
+
cvt.uniq.sort
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
# Normalize hash keys to symbols
|
|
164
|
+
#
|
|
165
|
+
# @param hash [Hash] Input hash with string or symbol keys
|
|
166
|
+
# @return [Hash] Hash with symbol keys
|
|
167
|
+
def normalize_keys(hash)
|
|
168
|
+
return hash unless hash.is_a?(Hash)
|
|
169
|
+
return hash if hash.empty? || hash.keys.first.is_a?(Symbol)
|
|
170
|
+
|
|
171
|
+
hash.transform_keys(&:to_sym)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Calculate CVT Cut-In from PostScript blue_scale
|
|
175
|
+
#
|
|
176
|
+
# Blue scale controls the threshold at which alignment zones apply.
|
|
177
|
+
# We convert this to TrueType's CVT Cut-In value.
|
|
178
|
+
#
|
|
179
|
+
# @param blue_scale [Float] PostScript blue scale (0.0-1.0)
|
|
180
|
+
# @return [Integer] CVT Cut-In value in pixels
|
|
181
|
+
def calculate_cvt_cut_in(blue_scale)
|
|
182
|
+
# blue_scale of 0.039625 (common default) maps to ~17px cut-in
|
|
183
|
+
# Linear scaling: 0.039625 -> 17, 0.0 -> 0, 1.0 -> 428
|
|
184
|
+
(blue_scale * 428).round.clamp(0, 255)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Calculate Single Width Cut-In from stem widths
|
|
188
|
+
#
|
|
189
|
+
# The cut-in determines when to apply single-width rounding.
|
|
190
|
+
# We use 9 pixels as a sensible default.
|
|
191
|
+
#
|
|
192
|
+
# @param _ps_params [Hash] PostScript parameters (for future use)
|
|
193
|
+
# @return [Integer] Single Width Cut-In in pixels
|
|
194
|
+
def calculate_sw_cut_in(_ps_params)
|
|
195
|
+
9 # Standard value: 9 pixels
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Push a single value onto the TrueType stack
|
|
199
|
+
#
|
|
200
|
+
# Selects the most efficient instruction based on value size.
|
|
201
|
+
#
|
|
202
|
+
# @param value [Integer] Value to push
|
|
203
|
+
# @return [Array<Integer>] Instruction bytes
|
|
204
|
+
def push_value(value)
|
|
205
|
+
if value <= BYTE_MAX
|
|
206
|
+
push_bytes([value])
|
|
207
|
+
else
|
|
208
|
+
push_words([value])
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Push byte values using most efficient instruction
|
|
213
|
+
#
|
|
214
|
+
# Uses PUSHB[n] for 1-8 values, NPUSHB for more.
|
|
215
|
+
#
|
|
216
|
+
# @param values [Array<Integer>] Byte values (0-255)
|
|
217
|
+
# @return [Array<Integer>] Instruction bytes
|
|
218
|
+
def push_bytes(values)
|
|
219
|
+
return [] if values.empty?
|
|
220
|
+
|
|
221
|
+
# Validate all values fit in bytes
|
|
222
|
+
unless values.all? { |v| v >= 0 && v <= BYTE_MAX }
|
|
223
|
+
raise ArgumentError, "Values must be in range 0-255 for PUSHB"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
count = values.size
|
|
227
|
+
|
|
228
|
+
if count <= MAX_PUSHB_INLINE
|
|
229
|
+
# Use PUSHB[n-1] for 1-8 values
|
|
230
|
+
[PUSHB_BASE + count - 1] + values
|
|
231
|
+
else
|
|
232
|
+
# Use NPUSHB for more than 8 values
|
|
233
|
+
[NPUSHB, count] + values
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Push word values using most efficient instruction
|
|
238
|
+
#
|
|
239
|
+
# Uses PUSHW[n] for 1-8 values, NPUSHW for more.
|
|
240
|
+
# Words are encoded big-endian (high byte first).
|
|
241
|
+
#
|
|
242
|
+
# @param values [Array<Integer>] Word values (0-65535)
|
|
243
|
+
# @return [Array<Integer>] Instruction bytes
|
|
244
|
+
def push_words(values)
|
|
245
|
+
return [] if values.empty?
|
|
246
|
+
|
|
247
|
+
# Validate all values fit in words
|
|
248
|
+
unless values.all? { |v| v >= 0 && v <= WORD_MAX }
|
|
249
|
+
raise ArgumentError, "Values must be in range 0-65535 for PUSHW"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
count = values.size
|
|
253
|
+
# Convert words to big-endian byte pairs
|
|
254
|
+
word_bytes = values.flat_map { |v| [(v >> 8) & 0xFF, v & 0xFF] }
|
|
255
|
+
|
|
256
|
+
if count <= MAX_PUSHW_INLINE
|
|
257
|
+
# Use PUSHW[n-1] for 1-8 values
|
|
258
|
+
[PUSHW_BASE + count - 1] + word_bytes
|
|
259
|
+
else
|
|
260
|
+
# Use NPUSHW for more than 8 values
|
|
261
|
+
[NPUSHW, count] + word_bytes
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -28,12 +28,12 @@ module Fontisan
|
|
|
28
28
|
MODES = {
|
|
29
29
|
METADATA => {
|
|
30
30
|
tables: %w[name head hhea maxp OS/2 post].freeze,
|
|
31
|
-
description: "Metadata mode - loads only identification and metrics tables (otfinfo-equivalent)"
|
|
31
|
+
description: "Metadata mode - loads only identification and metrics tables (otfinfo-equivalent)",
|
|
32
32
|
}.freeze,
|
|
33
33
|
FULL => {
|
|
34
34
|
tables: :all,
|
|
35
|
-
description: "Full mode - loads all tables in the font"
|
|
36
|
-
}.freeze
|
|
35
|
+
description: "Full mode - loads all tables in the font",
|
|
36
|
+
}.freeze,
|
|
37
37
|
}.freeze
|
|
38
38
|
|
|
39
39
|
# Pre-computed Set for O(1) lookup of metadata tables
|
|
@@ -80,7 +80,7 @@ module Fontisan
|
|
|
80
80
|
# @raise [ArgumentError] if mode is invalid
|
|
81
81
|
def self.default_lazy?(mode)
|
|
82
82
|
validate_mode!(mode)
|
|
83
|
-
true
|
|
83
|
+
true # Lazy loading is recommended for all modes
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# Get mode description
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
require_relative "font_info"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Models
|
|
8
|
+
# Model for collection brief information
|
|
9
|
+
#
|
|
10
|
+
# Represents collection metadata plus brief info for each font.
|
|
11
|
+
# Used by InfoCommand in brief mode for collections.
|
|
12
|
+
#
|
|
13
|
+
# @example Creating collection brief info
|
|
14
|
+
# info = CollectionBriefInfo.new(
|
|
15
|
+
# collection_path: "fonts.ttc",
|
|
16
|
+
# collection_type: "TTC",
|
|
17
|
+
# collection_version: "1.0",
|
|
18
|
+
# num_fonts: 3,
|
|
19
|
+
# fonts: [font_info1, font_info2, font_info3]
|
|
20
|
+
# )
|
|
21
|
+
class CollectionBriefInfo < Lutaml::Model::Serializable
|
|
22
|
+
attribute :collection_path, :string
|
|
23
|
+
attribute :collection_type, :string
|
|
24
|
+
attribute :collection_version, :string
|
|
25
|
+
attribute :num_fonts, :integer
|
|
26
|
+
attribute :fonts, FontInfo, collection: true
|
|
27
|
+
|
|
28
|
+
key_value do
|
|
29
|
+
map "collection_path", to: :collection_path
|
|
30
|
+
map "collection_type", to: :collection_type
|
|
31
|
+
map "collection_version", to: :collection_version
|
|
32
|
+
map "num_fonts", to: :num_fonts
|
|
33
|
+
map "fonts", to: :fonts
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|