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
|
@@ -114,7 +114,7 @@ module Fontisan
|
|
|
114
114
|
skip_tables = @checksum_config["skip_tables"] || []
|
|
115
115
|
|
|
116
116
|
font.tables.each do |table_entry|
|
|
117
|
-
tag = table_entry.tag.to_s
|
|
117
|
+
tag = table_entry.tag.to_s # Convert BinData field to string
|
|
118
118
|
|
|
119
119
|
# Skip tables that are exempt from checksum validation
|
|
120
120
|
next if skip_tables.include?(tag)
|
|
@@ -125,7 +125,7 @@ module Fontisan
|
|
|
125
125
|
|
|
126
126
|
# Calculate checksum for the table
|
|
127
127
|
calculated_checksum = calculate_table_checksum(table_data)
|
|
128
|
-
declared_checksum = table_entry.checksum.to_i
|
|
128
|
+
declared_checksum = table_entry.checksum.to_i # Convert BinData field to integer
|
|
129
129
|
|
|
130
130
|
# Special handling for head table (checksum adjustment field should be 0)
|
|
131
131
|
if tag == Constants::HEAD_TAG
|
|
@@ -44,6 +44,7 @@ module Fontisan
|
|
|
44
44
|
@ttl = ttl
|
|
45
45
|
@cache = {}
|
|
46
46
|
@access_times = {}
|
|
47
|
+
@access_counter = 0
|
|
47
48
|
@stats = {
|
|
48
49
|
hits: 0,
|
|
49
50
|
misses: 0,
|
|
@@ -219,7 +220,8 @@ module Fontisan
|
|
|
219
220
|
#
|
|
220
221
|
# @param key [String] Cache key
|
|
221
222
|
def touch(key)
|
|
222
|
-
@
|
|
223
|
+
@access_counter += 1
|
|
224
|
+
@access_times[key] = @access_counter
|
|
223
225
|
end
|
|
224
226
|
|
|
225
227
|
# Evict entries if cache is full
|
|
@@ -162,7 +162,8 @@ module Fontisan
|
|
|
162
162
|
# Note: CFF2 blend deltas are per-coordinate, we need to map to x/y
|
|
163
163
|
# This is a simplified mapping - full implementation would track
|
|
164
164
|
# which coordinates are being varied
|
|
165
|
-
regions_map[region_key][:deltas_per_point][idx / 2] ||= { x: 0,
|
|
165
|
+
regions_map[region_key][:deltas_per_point][idx / 2] ||= { x: 0,
|
|
166
|
+
y: 0 }
|
|
166
167
|
if idx.even?
|
|
167
168
|
regions_map[region_key][:deltas_per_point][idx / 2][:x] = delta
|
|
168
169
|
else
|
|
@@ -79,7 +79,8 @@ module Fontisan
|
|
|
79
79
|
# Apply each active tuple's deltas
|
|
80
80
|
adjusted_points = base_points.dup
|
|
81
81
|
matches.each do |match|
|
|
82
|
-
apply_tuple_deltas(adjusted_points, match, tuple_data,
|
|
82
|
+
apply_tuple_deltas(adjusted_points, match, tuple_data,
|
|
83
|
+
base_points.length)
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
adjusted_points
|
|
@@ -107,7 +107,8 @@ module Fontisan
|
|
|
107
107
|
index: index,
|
|
108
108
|
name: instance_name(instance[:subfamily_name_id]),
|
|
109
109
|
postscript_name: instance_name(instance[:postscript_name_id]),
|
|
110
|
-
coordinates: instance_coordinates(instance[:coordinates],
|
|
110
|
+
coordinates: instance_coordinates(instance[:coordinates],
|
|
111
|
+
@context.axes),
|
|
111
112
|
}
|
|
112
113
|
end
|
|
113
114
|
end
|
|
@@ -248,7 +248,8 @@ module Fontisan
|
|
|
248
248
|
# @param scalars [Array<Float>] Region scalars
|
|
249
249
|
# @return [Hash] Interpolated point
|
|
250
250
|
def interpolate_point(base_point, delta_points, scalars)
|
|
251
|
-
@context.interpolator.interpolate_point(base_point, delta_points,
|
|
251
|
+
@context.interpolator.interpolate_point(base_point, delta_points,
|
|
252
|
+
scalars)
|
|
252
253
|
end
|
|
253
254
|
|
|
254
255
|
private
|
|
@@ -209,9 +209,12 @@ module Fontisan
|
|
|
209
209
|
coords2 = r2.region_axes[i]
|
|
210
210
|
|
|
211
211
|
# Compare start, peak, end coordinates
|
|
212
|
-
return false unless coords_similar?(coords1.start_coord,
|
|
213
|
-
|
|
214
|
-
return false unless coords_similar?(coords1.
|
|
212
|
+
return false unless coords_similar?(coords1.start_coord,
|
|
213
|
+
coords2.start_coord)
|
|
214
|
+
return false unless coords_similar?(coords1.peak_coord,
|
|
215
|
+
coords2.peak_coord)
|
|
216
|
+
return false unless coords_similar?(coords1.end_coord,
|
|
217
|
+
coords2.end_coord)
|
|
215
218
|
end
|
|
216
219
|
|
|
217
220
|
true
|
|
@@ -112,7 +112,10 @@ module Fontisan
|
|
|
112
112
|
validate_input if @options[:validate]
|
|
113
113
|
|
|
114
114
|
fvar = variation_table("fvar")
|
|
115
|
-
|
|
115
|
+
unless fvar
|
|
116
|
+
return { tables: @font.table_data.dup,
|
|
117
|
+
report: { error: "No fvar table" } }
|
|
118
|
+
end
|
|
116
119
|
|
|
117
120
|
# Find axes to keep
|
|
118
121
|
all_axes = fvar.axes
|
|
@@ -175,7 +178,8 @@ module Fontisan
|
|
|
175
178
|
optimizer = Optimizer.new(cff2, region_threshold: threshold)
|
|
176
179
|
optimizer.optimize
|
|
177
180
|
|
|
178
|
-
@report[:regions_deduplicated] =
|
|
181
|
+
@report[:regions_deduplicated] =
|
|
182
|
+
optimizer.stats[:regions_deduplicated]
|
|
179
183
|
@report[:cff2_optimized] = true
|
|
180
184
|
end
|
|
181
185
|
|
|
@@ -316,8 +320,14 @@ module Fontisan
|
|
|
316
320
|
# @param tables [Hash] Font tables
|
|
317
321
|
# @param glyph_ids [Array<Integer>] Glyph IDs to keep
|
|
318
322
|
def subset_metrics_variations(tables, glyph_ids)
|
|
319
|
-
|
|
320
|
-
|
|
323
|
+
if has_variation_table?("HVAR")
|
|
324
|
+
subset_metrics_table(tables, "HVAR",
|
|
325
|
+
glyph_ids)
|
|
326
|
+
end
|
|
327
|
+
if has_variation_table?("VVAR")
|
|
328
|
+
subset_metrics_table(tables, "VVAR",
|
|
329
|
+
glyph_ids)
|
|
330
|
+
end
|
|
321
331
|
# MVAR is font-wide, no glyph subsetting needed
|
|
322
332
|
end
|
|
323
333
|
|
|
@@ -333,7 +343,8 @@ module Fontisan
|
|
|
333
343
|
# 3. Remove unused ItemVariationData
|
|
334
344
|
# 4. Rebuild and serialize
|
|
335
345
|
|
|
336
|
-
@report[:"#{table_tag.downcase}_note"] =
|
|
346
|
+
@report[:"#{table_tag.downcase}_note"] =
|
|
347
|
+
"#{table_tag} subsetting not yet implemented"
|
|
337
348
|
end
|
|
338
349
|
|
|
339
350
|
# Update non-variation glyph tables
|
|
@@ -396,9 +407,18 @@ module Fontisan
|
|
|
396
407
|
# @param tables [Hash] Font tables
|
|
397
408
|
# @param keep_indices [Array<Integer>] Axis indices to keep
|
|
398
409
|
def subset_metrics_axes(tables, keep_indices)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
410
|
+
if has_variation_table?("HVAR")
|
|
411
|
+
subset_metrics_table_axes(tables, "HVAR",
|
|
412
|
+
keep_indices)
|
|
413
|
+
end
|
|
414
|
+
if has_variation_table?("VVAR")
|
|
415
|
+
subset_metrics_table_axes(tables, "VVAR",
|
|
416
|
+
keep_indices)
|
|
417
|
+
end
|
|
418
|
+
if has_variation_table?("MVAR")
|
|
419
|
+
subset_metrics_table_axes(tables, "MVAR",
|
|
420
|
+
keep_indices)
|
|
421
|
+
end
|
|
402
422
|
end
|
|
403
423
|
|
|
404
424
|
# Subset a single metrics table's axes
|
|
@@ -412,7 +432,8 @@ module Fontisan
|
|
|
412
432
|
# 2. Filter ItemVariationStore regions to keep axis indices
|
|
413
433
|
# 3. Rebuild and serialize
|
|
414
434
|
|
|
415
|
-
@report[:"#{table_tag.downcase}_axes_note"] =
|
|
435
|
+
@report[:"#{table_tag.downcase}_axes_note"] =
|
|
436
|
+
"#{table_tag} axis subsetting not yet implemented"
|
|
416
437
|
end
|
|
417
438
|
|
|
418
439
|
# Simplify metrics table regions
|
|
@@ -426,7 +447,8 @@ module Fontisan
|
|
|
426
447
|
# 3. Update delta set indices
|
|
427
448
|
# 4. Serialize back to binary
|
|
428
449
|
|
|
429
|
-
@report[:metrics_simplify_note] =
|
|
450
|
+
@report[:metrics_simplify_note] =
|
|
451
|
+
"Metrics region simplification not yet implemented"
|
|
430
452
|
end
|
|
431
453
|
|
|
432
454
|
# Create temporary font wrapper for validation
|
|
@@ -157,7 +157,10 @@ module Fontisan
|
|
|
157
157
|
"Source font must respond to :has_table? and :table_data"
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
if @target_tables.nil?
|
|
161
|
+
raise ArgumentError,
|
|
162
|
+
"Target tables cannot be nil"
|
|
163
|
+
end
|
|
161
164
|
|
|
162
165
|
unless @target_tables.is_a?(Hash)
|
|
163
166
|
raise ArgumentError,
|
data/lib/fontisan/version.rb
CHANGED
|
@@ -41,7 +41,7 @@ module Fontisan
|
|
|
41
41
|
WE_HAVE_INSTRUCTIONS = 0x0100
|
|
42
42
|
USE_MY_METRICS = 0x0200
|
|
43
43
|
OVERLAP_COMPOUND = 0x0400
|
|
44
|
-
HAVE_VARIATIONS = 0x1000
|
|
44
|
+
HAVE_VARIATIONS = 0x1000 # Variable font variation data follows
|
|
45
45
|
|
|
46
46
|
# Reconstruct glyf and loca tables from transformed data
|
|
47
47
|
#
|
|
@@ -55,7 +55,8 @@ module Fontisan
|
|
|
55
55
|
|
|
56
56
|
# Check minimum size for header
|
|
57
57
|
if io.size < 8
|
|
58
|
-
raise InvalidFontError,
|
|
58
|
+
raise InvalidFontError,
|
|
59
|
+
"Transformed glyf data too small: #{io.size} bytes"
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
# Read header
|
|
@@ -69,28 +70,34 @@ module Fontisan
|
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
# Read nContour stream
|
|
72
|
-
n_contour_data = read_stream_safely(io, "nContour",
|
|
73
|
+
n_contour_data = read_stream_safely(io, "nContour",
|
|
74
|
+
variable_font: variable_font)
|
|
73
75
|
|
|
74
76
|
# Read nPoints stream
|
|
75
|
-
n_points_data = read_stream_safely(io, "nPoints",
|
|
77
|
+
n_points_data = read_stream_safely(io, "nPoints",
|
|
78
|
+
variable_font: variable_font)
|
|
76
79
|
|
|
77
80
|
# Read flag stream
|
|
78
81
|
flag_data = read_stream_safely(io, "flag", variable_font: variable_font)
|
|
79
82
|
|
|
80
83
|
# Read glyph stream (coordinates, instructions, composite data)
|
|
81
|
-
glyph_data = read_stream_safely(io, "glyph",
|
|
84
|
+
glyph_data = read_stream_safely(io, "glyph",
|
|
85
|
+
variable_font: variable_font)
|
|
82
86
|
|
|
83
87
|
# Read composite stream
|
|
84
|
-
composite_data = read_stream_safely(io, "composite",
|
|
88
|
+
composite_data = read_stream_safely(io, "composite",
|
|
89
|
+
variable_font: variable_font)
|
|
85
90
|
|
|
86
91
|
# Read bbox stream
|
|
87
92
|
bbox_data = read_stream_safely(io, "bbox", variable_font: variable_font)
|
|
88
93
|
|
|
89
94
|
# Read instruction stream
|
|
90
|
-
instruction_data = read_stream_safely(io, "instruction",
|
|
95
|
+
instruction_data = read_stream_safely(io, "instruction",
|
|
96
|
+
variable_font: variable_font)
|
|
91
97
|
|
|
92
98
|
# Parse streams
|
|
93
|
-
n_contours = parse_n_contour_stream(StringIO.new(n_contour_data),
|
|
99
|
+
n_contours = parse_n_contour_stream(StringIO.new(n_contour_data),
|
|
100
|
+
num_glyphs)
|
|
94
101
|
|
|
95
102
|
# Reconstruct glyphs
|
|
96
103
|
glyphs = reconstruct_glyphs(
|
|
@@ -101,7 +108,7 @@ module Fontisan
|
|
|
101
108
|
StringIO.new(composite_data),
|
|
102
109
|
StringIO.new(bbox_data),
|
|
103
110
|
StringIO.new(instruction_data),
|
|
104
|
-
variable_font: variable_font
|
|
111
|
+
variable_font: variable_font,
|
|
105
112
|
)
|
|
106
113
|
|
|
107
114
|
# Build glyf and loca tables
|
|
@@ -114,7 +121,7 @@ module Fontisan
|
|
|
114
121
|
# @param stream_name [String] Name of stream for error messages
|
|
115
122
|
# @param variable_font [Boolean] Whether this is a variable font (allows incomplete streams)
|
|
116
123
|
# @return [String] Stream data (empty if not available)
|
|
117
|
-
def self.read_stream_safely(io,
|
|
124
|
+
def self.read_stream_safely(io, _stream_name, variable_font: false)
|
|
118
125
|
remaining = io.size - io.pos
|
|
119
126
|
if remaining < 4
|
|
120
127
|
# Not enough data for stream size - return empty stream
|
|
@@ -131,9 +138,9 @@ module Fontisan
|
|
|
131
138
|
if remaining < stream_size
|
|
132
139
|
# Stream size extends beyond available data
|
|
133
140
|
# Read what we can
|
|
134
|
-
|
|
141
|
+
io.read(remaining) || ""
|
|
135
142
|
# For variable fonts, we may have incomplete streams - just return what we have
|
|
136
|
-
|
|
143
|
+
|
|
137
144
|
else
|
|
138
145
|
io.read(stream_size) || ""
|
|
139
146
|
end
|
|
@@ -160,18 +167,24 @@ module Fontisan
|
|
|
160
167
|
case code
|
|
161
168
|
when 255
|
|
162
169
|
return 0 if io.eof? || (io.size - io.pos) < 2
|
|
170
|
+
|
|
163
171
|
value_bytes = io.read(2)
|
|
164
172
|
return 0 unless value_bytes && value_bytes.bytesize == 2
|
|
173
|
+
|
|
165
174
|
759 + value_bytes.unpack1("n") # 253 * 3 + value
|
|
166
175
|
when 254
|
|
167
176
|
return 0 if io.eof? || (io.size - io.pos) < 2
|
|
177
|
+
|
|
168
178
|
value_bytes = io.read(2)
|
|
169
179
|
return 0 unless value_bytes && value_bytes.bytesize == 2
|
|
180
|
+
|
|
170
181
|
506 + value_bytes.unpack1("n") # 253 * 2 + value
|
|
171
182
|
when 253
|
|
172
183
|
return 0 if io.eof? || (io.size - io.pos) < 2
|
|
184
|
+
|
|
173
185
|
value_bytes = io.read(2)
|
|
174
186
|
return 0 unless value_bytes && value_bytes.bytesize == 2
|
|
187
|
+
|
|
175
188
|
253 + value_bytes.unpack1("n")
|
|
176
189
|
else
|
|
177
190
|
code
|
|
@@ -279,15 +292,15 @@ module Fontisan
|
|
|
279
292
|
x_min = y_min = x_max = y_max = 0
|
|
280
293
|
else
|
|
281
294
|
bbox_bytes = bbox_io.read(8)
|
|
282
|
-
|
|
283
|
-
x_min = y_min = x_max = y_max = 0
|
|
284
|
-
else
|
|
295
|
+
if bbox_bytes && bbox_bytes.bytesize == 8
|
|
285
296
|
x_min, y_min, x_max, y_max = bbox_bytes.unpack("n4")
|
|
286
297
|
# Convert to signed
|
|
287
298
|
x_min = x_min > 0x7FFF ? x_min - 0x10000 : x_min
|
|
288
299
|
y_min = y_min > 0x7FFF ? y_min - 0x10000 : y_min
|
|
289
300
|
x_max = x_max > 0x7FFF ? x_max - 0x10000 : x_max
|
|
290
301
|
y_max = y_max > 0x7FFF ? y_max - 0x10000 : y_max
|
|
302
|
+
else
|
|
303
|
+
x_min = y_min = x_max = y_max = 0
|
|
291
304
|
end
|
|
292
305
|
end
|
|
293
306
|
|
|
@@ -302,12 +315,12 @@ module Fontisan
|
|
|
302
315
|
instruction_length = inst_length_data
|
|
303
316
|
if instruction_length.positive?
|
|
304
317
|
inst_remaining = instruction_io.size - instruction_io.pos
|
|
305
|
-
if inst_remaining >= instruction_length
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
318
|
+
instructions = if inst_remaining >= instruction_length
|
|
319
|
+
instruction_io.read(instruction_length) || ""
|
|
320
|
+
else
|
|
321
|
+
# Read what we can
|
|
322
|
+
instruction_io.read(inst_remaining) || ""
|
|
323
|
+
end
|
|
311
324
|
end
|
|
312
325
|
end
|
|
313
326
|
end
|
|
@@ -325,7 +338,8 @@ module Fontisan
|
|
|
325
338
|
# @param instruction_io [StringIO] Instruction stream
|
|
326
339
|
# @param variable_font [Boolean] Whether this is a variable font
|
|
327
340
|
# @return [String] Glyph data in standard format
|
|
328
|
-
def self.reconstruct_composite_glyph(composite_io, bbox_io,
|
|
341
|
+
def self.reconstruct_composite_glyph(composite_io, bbox_io,
|
|
342
|
+
instruction_io, variable_font: false)
|
|
329
343
|
# Track available bytes to prevent EOF errors
|
|
330
344
|
composite_size = composite_io.size - composite_io.pos
|
|
331
345
|
|
|
@@ -364,6 +378,7 @@ module Fontisan
|
|
|
364
378
|
# Read flags and glyph_index safely
|
|
365
379
|
component_header = composite_io.read(4)
|
|
366
380
|
break unless component_header && component_header.bytesize == 4
|
|
381
|
+
|
|
367
382
|
flags, glyph_index = component_header.unpack("n2")
|
|
368
383
|
|
|
369
384
|
# Write flags and index
|
|
@@ -371,18 +386,21 @@ module Fontisan
|
|
|
371
386
|
composite_data << [glyph_index].pack("n")
|
|
372
387
|
|
|
373
388
|
# Read arguments (depend on flags)
|
|
389
|
+
remaining = composite_io.size - composite_io.pos
|
|
374
390
|
if (flags & ARG_1_AND_2_ARE_WORDS).zero?
|
|
375
|
-
remaining = composite_io.size - composite_io.pos
|
|
376
391
|
break if composite_io.eof? || remaining < 2
|
|
392
|
+
|
|
377
393
|
arg_bytes = composite_io.read(2)
|
|
378
394
|
break unless arg_bytes && arg_bytes.bytesize == 2
|
|
395
|
+
|
|
379
396
|
arg1, arg2 = arg_bytes.unpack("c2")
|
|
380
397
|
composite_data << [arg1, arg2].pack("c2")
|
|
381
398
|
else
|
|
382
|
-
remaining = composite_io.size - composite_io.pos
|
|
383
399
|
break if composite_io.eof? || remaining < 4
|
|
400
|
+
|
|
384
401
|
arg_bytes = composite_io.read(4)
|
|
385
402
|
break unless arg_bytes && arg_bytes.bytesize == 4
|
|
403
|
+
|
|
386
404
|
arg1, arg2 = arg_bytes.unpack("n2")
|
|
387
405
|
# Convert to signed
|
|
388
406
|
arg1 = arg1 > 0x7FFF ? arg1 - 0x10000 : arg1
|
|
@@ -394,22 +412,28 @@ module Fontisan
|
|
|
394
412
|
if (flags & WE_HAVE_A_SCALE) != 0
|
|
395
413
|
remaining = composite_io.size - composite_io.pos
|
|
396
414
|
break if composite_io.eof? || remaining < 2
|
|
415
|
+
|
|
397
416
|
scale_bytes = composite_io.read(2)
|
|
398
417
|
break unless scale_bytes && scale_bytes.bytesize == 2
|
|
418
|
+
|
|
399
419
|
scale = scale_bytes.unpack1("n")
|
|
400
420
|
composite_data << [scale].pack("n")
|
|
401
421
|
elsif (flags & WE_HAVE_AN_X_AND_Y_SCALE) != 0
|
|
402
422
|
remaining = composite_io.size - composite_io.pos
|
|
403
423
|
break if composite_io.eof? || remaining < 4
|
|
424
|
+
|
|
404
425
|
scale_bytes = composite_io.read(4)
|
|
405
426
|
break unless scale_bytes && scale_bytes.bytesize == 4
|
|
427
|
+
|
|
406
428
|
x_scale, y_scale = scale_bytes.unpack("n2")
|
|
407
429
|
composite_data << [x_scale, y_scale].pack("n2")
|
|
408
430
|
elsif (flags & WE_HAVE_A_TWO_BY_TWO) != 0
|
|
409
431
|
remaining = composite_io.size - composite_io.pos
|
|
410
432
|
break if composite_io.eof? || remaining < 8
|
|
433
|
+
|
|
411
434
|
matrix_bytes = composite_io.read(8)
|
|
412
435
|
break unless matrix_bytes && matrix_bytes.bytesize == 8
|
|
436
|
+
|
|
413
437
|
x_scale, scale01, scale10, y_scale = matrix_bytes.unpack("n4")
|
|
414
438
|
composite_data << [x_scale, scale01, scale10, y_scale].pack("n4")
|
|
415
439
|
end
|
|
@@ -462,12 +486,12 @@ module Fontisan
|
|
|
462
486
|
instruction_length = length_bytes.unpack1("n")
|
|
463
487
|
if instruction_length.positive?
|
|
464
488
|
remaining = instruction_io.size - instruction_io.pos
|
|
465
|
-
if remaining >= instruction_length
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
489
|
+
instructions = if remaining >= instruction_length
|
|
490
|
+
instruction_io.read(instruction_length) || ""
|
|
491
|
+
else
|
|
492
|
+
# Read what we can
|
|
493
|
+
instruction_io.read(remaining) || ""
|
|
494
|
+
end
|
|
471
495
|
end
|
|
472
496
|
end
|
|
473
497
|
end
|
|
@@ -501,6 +525,7 @@ module Fontisan
|
|
|
501
525
|
|
|
502
526
|
if (flag & REPEAT_FLAG) != 0
|
|
503
527
|
break if io.eof? || (io.size - io.pos) < 1
|
|
528
|
+
|
|
504
529
|
repeat_count = read_uint8(io)
|
|
505
530
|
repeat_count.times { flags << flag }
|
|
506
531
|
end
|
|
@@ -529,6 +554,7 @@ module Fontisan
|
|
|
529
554
|
# EOF protection
|
|
530
555
|
if (flag & short_flag) != 0
|
|
531
556
|
break if io.eof? || (io.size - io.pos) < 1
|
|
557
|
+
|
|
532
558
|
# Short vector (one byte)
|
|
533
559
|
delta = read_uint8(io)
|
|
534
560
|
delta = -delta if (flag & same_or_positive_flag).zero?
|
|
@@ -537,6 +563,7 @@ module Fontisan
|
|
|
537
563
|
delta = 0
|
|
538
564
|
else
|
|
539
565
|
break if io.eof? || (io.size - io.pos) < 2
|
|
566
|
+
|
|
540
567
|
# Long vector (two bytes, signed)
|
|
541
568
|
delta = read_int16(io)
|
|
542
569
|
end
|
data/lib/fontisan/woff2_font.rb
CHANGED
|
@@ -85,8 +85,7 @@ module Fontisan
|
|
|
85
85
|
# Simple struct for storing file path
|
|
86
86
|
IOSource = Struct.new(:path)
|
|
87
87
|
|
|
88
|
-
attr_accessor :header, :table_entries, :decompressed_tables, :parsed_tables, :io_source
|
|
89
|
-
attr_accessor :underlying_font # Allow both reading and setting for table delegation
|
|
88
|
+
attr_accessor :header, :table_entries, :decompressed_tables, :parsed_tables, :io_source, :underlying_font # Allow both reading and setting for table delegation
|
|
90
89
|
|
|
91
90
|
def initialize
|
|
92
91
|
@header = nil
|
|
@@ -94,7 +93,7 @@ module Fontisan
|
|
|
94
93
|
@decompressed_tables = {}
|
|
95
94
|
@parsed_tables = {}
|
|
96
95
|
@io_source = nil
|
|
97
|
-
@underlying_font = nil
|
|
96
|
+
@underlying_font = nil # Store the actual TrueTypeFont/OpenTypeFont
|
|
98
97
|
end
|
|
99
98
|
|
|
100
99
|
# Initialize storage hashes
|
|
@@ -159,7 +158,7 @@ module Fontisan
|
|
|
159
158
|
# Get decompressed table data
|
|
160
159
|
def table_data(tag)
|
|
161
160
|
# First try underlying font's table data if available
|
|
162
|
-
if @underlying_font
|
|
161
|
+
if @underlying_font.respond_to?(:table_data)
|
|
163
162
|
underlying_data = @underlying_font.table_data[tag]
|
|
164
163
|
return underlying_data if underlying_data
|
|
165
164
|
end
|
|
@@ -183,11 +182,13 @@ module Fontisan
|
|
|
183
182
|
# Convert to TTF
|
|
184
183
|
def to_ttf(output_path)
|
|
185
184
|
unless truetype?
|
|
186
|
-
raise InvalidFontError,
|
|
185
|
+
raise InvalidFontError,
|
|
186
|
+
"Cannot convert to TTF: font is not TrueType flavored"
|
|
187
187
|
end
|
|
188
188
|
|
|
189
189
|
# Build SFNT and create TrueTypeFont
|
|
190
|
-
sfnt_data = self.class.build_sfnt_in_memory(@header, @table_entries,
|
|
190
|
+
sfnt_data = self.class.build_sfnt_in_memory(@header, @table_entries,
|
|
191
|
+
@decompressed_tables)
|
|
191
192
|
sfnt_io = StringIO.new(sfnt_data)
|
|
192
193
|
|
|
193
194
|
# Create actual TrueTypeFont and save for table delegation
|
|
@@ -201,11 +202,13 @@ module Fontisan
|
|
|
201
202
|
# Convert to OTF
|
|
202
203
|
def to_otf(output_path)
|
|
203
204
|
unless cff?
|
|
204
|
-
raise InvalidFontError,
|
|
205
|
+
raise InvalidFontError,
|
|
206
|
+
"Cannot convert to OTF: font is not CFF flavored"
|
|
205
207
|
end
|
|
206
208
|
|
|
207
209
|
# Build SFNT and create OpenTypeFont
|
|
208
|
-
sfnt_data = self.class.build_sfnt_in_memory(@header, @table_entries,
|
|
210
|
+
sfnt_data = self.class.build_sfnt_in_memory(@header, @table_entries,
|
|
211
|
+
@decompressed_tables)
|
|
209
212
|
sfnt_io = StringIO.new(sfnt_data)
|
|
210
213
|
|
|
211
214
|
# Create actual OpenTypeFont and save for table delegation
|
|
@@ -310,13 +313,15 @@ module Fontisan
|
|
|
310
313
|
woff2.table_entries = read_table_directory_from_io(io, woff2.header)
|
|
311
314
|
|
|
312
315
|
# Decompress table data
|
|
313
|
-
woff2.decompressed_tables = decompress_tables(io, woff2.header,
|
|
316
|
+
woff2.decompressed_tables = decompress_tables(io, woff2.header,
|
|
317
|
+
woff2.table_entries)
|
|
314
318
|
|
|
315
319
|
# Apply table transformations if present
|
|
316
320
|
apply_transformations!(woff2.table_entries, woff2.decompressed_tables)
|
|
317
321
|
|
|
318
322
|
# Build SFNT structure in memory
|
|
319
|
-
sfnt_data = build_sfnt_in_memory(woff2.header, woff2.table_entries,
|
|
323
|
+
sfnt_data = build_sfnt_in_memory(woff2.header, woff2.table_entries,
|
|
324
|
+
woff2.decompressed_tables)
|
|
320
325
|
|
|
321
326
|
# Create StringIO for reading
|
|
322
327
|
sfnt_io = StringIO.new(sfnt_data)
|
|
@@ -373,7 +378,10 @@ module Fontisan
|
|
|
373
378
|
|
|
374
379
|
# Read flags byte with nil check
|
|
375
380
|
flags_data = io.read(1)
|
|
376
|
-
|
|
381
|
+
if flags_data.nil?
|
|
382
|
+
raise EOFError,
|
|
383
|
+
"Unexpected EOF while reading table directory flags"
|
|
384
|
+
end
|
|
377
385
|
|
|
378
386
|
flags = flags_data.unpack1("C")
|
|
379
387
|
entry.flags = flags
|
|
@@ -383,7 +391,11 @@ module Fontisan
|
|
|
383
391
|
if tag_index == 0x3F
|
|
384
392
|
# Custom tag (4 bytes)
|
|
385
393
|
tag_data = io.read(4)
|
|
386
|
-
|
|
394
|
+
if tag_data.nil? || tag_data.bytesize < 4
|
|
395
|
+
raise EOFError,
|
|
396
|
+
"Unexpected EOF while reading custom tag"
|
|
397
|
+
end
|
|
398
|
+
|
|
387
399
|
entry.tag = tag_data.force_encoding("UTF-8")
|
|
388
400
|
else
|
|
389
401
|
# Known tag from table
|
|
@@ -402,7 +414,8 @@ module Fontisan
|
|
|
402
414
|
# - hmtx with non-zero version: TRANSFORMED (transformLength present)
|
|
403
415
|
# - all other tables: transformation version is 0 (no transformLength)
|
|
404
416
|
transform_version = (flags >> 6) & 0x03
|
|
405
|
-
has_transform_length = if ["glyf",
|
|
417
|
+
has_transform_length = if ["glyf",
|
|
418
|
+
"loca"].include?(entry.tag) && transform_version.zero?
|
|
406
419
|
true
|
|
407
420
|
elsif entry.tag == "hmtx" && transform_version != 0
|
|
408
421
|
true
|
|
@@ -429,7 +442,10 @@ module Fontisan
|
|
|
429
442
|
result = 0
|
|
430
443
|
5.times do
|
|
431
444
|
byte_data = io.read(1)
|
|
432
|
-
|
|
445
|
+
if byte_data.nil?
|
|
446
|
+
raise EOFError,
|
|
447
|
+
"Unexpected EOF while reading UIntBase128"
|
|
448
|
+
end
|
|
433
449
|
|
|
434
450
|
byte = byte_data.unpack1("C")
|
|
435
451
|
|
|
@@ -512,7 +528,7 @@ module Fontisan
|
|
|
512
528
|
result = Woff2::GlyfTransformer.reconstruct(
|
|
513
529
|
transformed_glyf,
|
|
514
530
|
num_glyphs,
|
|
515
|
-
variable_font: variable_font
|
|
531
|
+
variable_font: variable_font,
|
|
516
532
|
)
|
|
517
533
|
decompressed_tables["glyf"] = result[:glyf]
|
|
518
534
|
decompressed_tables["loca"] = result[:loca]
|
data/lib/fontisan.rb
CHANGED
|
@@ -107,6 +107,7 @@ require_relative "fontisan/models/validation_report"
|
|
|
107
107
|
require_relative "fontisan/models/font_export"
|
|
108
108
|
require_relative "fontisan/models/collection_font_summary"
|
|
109
109
|
require_relative "fontisan/models/collection_info"
|
|
110
|
+
require_relative "fontisan/models/collection_brief_info"
|
|
110
111
|
require_relative "fontisan/models/collection_list_info"
|
|
111
112
|
require_relative "fontisan/models/font_summary"
|
|
112
113
|
require_relative "fontisan/models/table_sharing_info"
|
|
@@ -149,8 +150,6 @@ require_relative "fontisan/variation/interpolator"
|
|
|
149
150
|
require_relative "fontisan/variation/region_matcher"
|
|
150
151
|
require_relative "fontisan/variation/data_extractor"
|
|
151
152
|
require_relative "fontisan/variation/instance_generator"
|
|
152
|
-
require_relative "fontisan/variation/interpolator"
|
|
153
|
-
require_relative "fontisan/variation/region_matcher"
|
|
154
153
|
require_relative "fontisan/variation/metrics_adjuster"
|
|
155
154
|
require_relative "fontisan/variation/converter"
|
|
156
155
|
require_relative "fontisan/variation/variation_preserver"
|
|
@@ -172,6 +171,17 @@ require_relative "fontisan/optimizers/charstring_rewriter"
|
|
|
172
171
|
require_relative "fontisan/optimizers/subroutine_optimizer"
|
|
173
172
|
require_relative "fontisan/optimizers/subroutine_generator"
|
|
174
173
|
|
|
174
|
+
# Hints infrastructure
|
|
175
|
+
require_relative "fontisan/models/hint"
|
|
176
|
+
require_relative "fontisan/hints/truetype_instruction_analyzer"
|
|
177
|
+
require_relative "fontisan/hints/truetype_instruction_generator"
|
|
178
|
+
require_relative "fontisan/hints/truetype_hint_extractor"
|
|
179
|
+
require_relative "fontisan/hints/truetype_hint_applier"
|
|
180
|
+
require_relative "fontisan/hints/postscript_hint_extractor"
|
|
181
|
+
require_relative "fontisan/hints/postscript_hint_applier"
|
|
182
|
+
require_relative "fontisan/hints/hint_converter"
|
|
183
|
+
require_relative "fontisan/hints/hint_validator"
|
|
184
|
+
|
|
175
185
|
# Commands
|
|
176
186
|
require_relative "fontisan/commands/base_command"
|
|
177
187
|
require_relative "fontisan/commands/info_command"
|
|
@@ -209,4 +219,34 @@ module Fontisan
|
|
|
209
219
|
self.logger = Logger.new($stdout).tap do |log|
|
|
210
220
|
log.level = Logger::WARN
|
|
211
221
|
end
|
|
222
|
+
|
|
223
|
+
# Get font information.
|
|
224
|
+
#
|
|
225
|
+
# Supports both full and brief modes. Brief mode uses metadata loading for
|
|
226
|
+
# 5x faster parsing by loading only essential tables (name, head, hhea,
|
|
227
|
+
# maxp, OS/2, post). Returns FontInfo with 13 essential fields in brief mode
|
|
228
|
+
# or all 38 fields in full mode.
|
|
229
|
+
#
|
|
230
|
+
# @param path [String] Path to font file
|
|
231
|
+
# @param brief [Boolean] Use brief mode for fast identification (default: false)
|
|
232
|
+
# @param font_index [Integer] Index for TTC/OTC files (default: 0)
|
|
233
|
+
# @return [Models::FontInfo, Models::CollectionInfo, Models::CollectionBriefInfo] Font information
|
|
234
|
+
#
|
|
235
|
+
# @example Get full info
|
|
236
|
+
# info = Fontisan.info("font.ttf")
|
|
237
|
+
# puts info.family_name
|
|
238
|
+
# puts info.copyright # populated in full mode
|
|
239
|
+
#
|
|
240
|
+
# @example Get brief info (5x faster)
|
|
241
|
+
# info = Fontisan.info("font.ttf", brief: true)
|
|
242
|
+
# puts info.family_name # populated
|
|
243
|
+
# puts info.postscript_name # populated
|
|
244
|
+
# puts info.copyright # nil (not populated in brief mode)
|
|
245
|
+
#
|
|
246
|
+
# @example Serialize to JSON
|
|
247
|
+
# info = Fontisan.info("font.ttf", brief: true)
|
|
248
|
+
# puts info.to_json
|
|
249
|
+
def self.info(path, brief: false, font_index: 0)
|
|
250
|
+
Commands::InfoCommand.new(path, brief: brief, font_index: font_index).run
|
|
251
|
+
end
|
|
212
252
|
end
|