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
|
@@ -99,7 +99,11 @@ module Fontisan
|
|
|
99
99
|
# Check array limits
|
|
100
100
|
if HINT_LIMITS[key]
|
|
101
101
|
raise ArgumentError, "#{key} invalid" unless value.is_a?(Array)
|
|
102
|
-
|
|
102
|
+
|
|
103
|
+
if value.length > HINT_LIMITS[key][:max]
|
|
104
|
+
raise ArgumentError,
|
|
105
|
+
"#{key} too long"
|
|
106
|
+
end
|
|
103
107
|
if HINT_LIMITS[key][:pairs] && value.length.odd?
|
|
104
108
|
raise ArgumentError, "#{key} must be pairs"
|
|
105
109
|
end
|
|
@@ -114,12 +118,14 @@ module Fontisan
|
|
|
114
118
|
when :blue_shift, :blue_fuzz
|
|
115
119
|
raise ArgumentError, "#{key} invalid" unless value.is_a?(Numeric)
|
|
116
120
|
when :force_bold
|
|
117
|
-
raise ArgumentError, "#{key} must be 0 or 1" unless [0,
|
|
121
|
+
raise ArgumentError, "#{key} must be 0 or 1" unless [0,
|
|
122
|
+
1].include?(value)
|
|
118
123
|
when :language_group
|
|
119
|
-
raise ArgumentError, "#{key} must be 0 or 1" unless [0,
|
|
124
|
+
raise ArgumentError, "#{key} must be 0 or 1" unless [0,
|
|
125
|
+
1].include?(value)
|
|
120
126
|
end
|
|
121
127
|
end
|
|
122
128
|
end
|
|
123
129
|
end
|
|
124
130
|
end
|
|
125
|
-
end
|
|
131
|
+
end
|
|
@@ -68,7 +68,8 @@ module Fontisan
|
|
|
68
68
|
# @param global_subrs [Cff::Index, nil] Global subroutines INDEX
|
|
69
69
|
# @param local_subrs [Cff::Index, nil] Local subroutines INDEX
|
|
70
70
|
# @param vsindex [Integer] Variation store index (default 0)
|
|
71
|
-
def initialize(data, num_axes = 0, global_subrs = nil,
|
|
71
|
+
def initialize(data, num_axes = 0, global_subrs = nil,
|
|
72
|
+
local_subrs = nil, vsindex = 0)
|
|
72
73
|
@data = data
|
|
73
74
|
@num_axes = num_axes
|
|
74
75
|
@global_subrs = global_subrs
|
|
@@ -123,7 +124,8 @@ module Fontisan
|
|
|
123
124
|
when :line_to
|
|
124
125
|
[:line_to, cmd[:x], cmd[:y]]
|
|
125
126
|
when :curve_to
|
|
126
|
-
[:curve_to, cmd[:x1], cmd[:y1], cmd[:x2], cmd[:y2], cmd[:x],
|
|
127
|
+
[:curve_to, cmd[:x1], cmd[:y1], cmd[:x2], cmd[:y2], cmd[:x],
|
|
128
|
+
cmd[:y]]
|
|
127
129
|
end
|
|
128
130
|
end
|
|
129
131
|
end
|
|
@@ -146,7 +148,8 @@ module Fontisan
|
|
|
146
148
|
end
|
|
147
149
|
end
|
|
148
150
|
rescue StandardError => e
|
|
149
|
-
raise CorruptedTableError,
|
|
151
|
+
raise CorruptedTableError,
|
|
152
|
+
"Failed to parse CFF2 CharString: #{e.message}"
|
|
150
153
|
end
|
|
151
154
|
|
|
152
155
|
# Check if byte is an operator
|
|
@@ -165,7 +168,10 @@ module Fontisan
|
|
|
165
168
|
if first_byte == 12
|
|
166
169
|
# Two-byte operator
|
|
167
170
|
second_byte = @io.getbyte
|
|
168
|
-
|
|
171
|
+
if second_byte.nil?
|
|
172
|
+
raise CorruptedTableError,
|
|
173
|
+
"Unexpected end of CharString"
|
|
174
|
+
end
|
|
169
175
|
|
|
170
176
|
[12, second_byte]
|
|
171
177
|
else
|
|
@@ -305,7 +311,7 @@ module Fontisan
|
|
|
305
311
|
# @param blend_op [Hash] Blend operation data
|
|
306
312
|
# @param coordinates [Hash<String, Float>] Axis coordinates
|
|
307
313
|
# @return [Array<Float>] Blended values
|
|
308
|
-
def apply_blend(blend_op,
|
|
314
|
+
def apply_blend(blend_op, _coordinates)
|
|
309
315
|
blend_op[:blends].map do |blend|
|
|
310
316
|
base = blend[:base]
|
|
311
317
|
deltas = blend[:deltas]
|
|
@@ -313,7 +319,7 @@ module Fontisan
|
|
|
313
319
|
# Apply deltas based on coordinates
|
|
314
320
|
# This will be enhanced when we have proper coordinate interpolation
|
|
315
321
|
blended_value = base
|
|
316
|
-
deltas.each_with_index do |delta,
|
|
322
|
+
deltas.each_with_index do |delta, _axis_index|
|
|
317
323
|
# Placeholder: use normalized coordinate (will be replaced with proper interpolation)
|
|
318
324
|
scalar = 0.0 # Will be calculated by interpolator
|
|
319
325
|
blended_value += delta * scalar
|
|
@@ -573,7 +579,7 @@ module Fontisan
|
|
|
573
579
|
def callsubr
|
|
574
580
|
return if @local_subrs.nil? || @stack.empty?
|
|
575
581
|
|
|
576
|
-
|
|
582
|
+
@stack.pop
|
|
577
583
|
# Implement subroutine call (placeholder)
|
|
578
584
|
@stack.clear
|
|
579
585
|
end
|
|
@@ -581,7 +587,7 @@ module Fontisan
|
|
|
581
587
|
def callgsubr
|
|
582
588
|
return if @global_subrs.nil? || @stack.empty?
|
|
583
589
|
|
|
584
|
-
|
|
590
|
+
@stack.pop
|
|
585
591
|
# Implement global subroutine call (placeholder)
|
|
586
592
|
@stack.clear
|
|
587
593
|
end
|
|
@@ -63,14 +63,14 @@ module Fontisan
|
|
|
63
63
|
|
|
64
64
|
blends << {
|
|
65
65
|
base: base,
|
|
66
|
-
deltas: deltas
|
|
66
|
+
deltas: deltas,
|
|
67
67
|
}
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
{
|
|
71
71
|
num_values: num_values,
|
|
72
72
|
num_axes: num_axes,
|
|
73
|
-
blends: blends
|
|
73
|
+
blends: blends,
|
|
74
74
|
}
|
|
75
75
|
end
|
|
76
76
|
|
|
@@ -90,7 +90,7 @@ module Fontisan
|
|
|
90
90
|
{
|
|
91
91
|
base: value[0],
|
|
92
92
|
deltas: value[1..num_axes],
|
|
93
|
-
num_axes: num_axes
|
|
93
|
+
num_axes: num_axes,
|
|
94
94
|
}
|
|
95
95
|
end
|
|
96
96
|
|
|
@@ -149,7 +149,8 @@ module Fontisan
|
|
|
149
149
|
else
|
|
150
150
|
# Try as single blend value
|
|
151
151
|
blend_data = parse_blend_value(key, num_axes: num_axes)
|
|
152
|
-
result[key] =
|
|
152
|
+
result[key] =
|
|
153
|
+
blend_data ? apply_blend(blend_data, scalars) : value
|
|
153
154
|
end
|
|
154
155
|
else
|
|
155
156
|
# Non-blend value, copy as-is
|
|
@@ -186,7 +187,7 @@ module Fontisan
|
|
|
186
187
|
# Hint with blend data - normalize and flatten for DICT storage
|
|
187
188
|
normalized_value = {
|
|
188
189
|
base: value[:base] || value["base"],
|
|
189
|
-
deltas: value[:deltas] || value["deltas"]
|
|
190
|
+
deltas: value[:deltas] || value["deltas"],
|
|
190
191
|
}
|
|
191
192
|
result[key] = flatten_blend(normalized_value, num_axes: num_axes)
|
|
192
193
|
else
|
|
@@ -243,4 +244,4 @@ module Fontisan
|
|
|
243
244
|
end
|
|
244
245
|
end
|
|
245
246
|
end
|
|
246
|
-
end
|
|
247
|
+
end
|
|
@@ -121,7 +121,7 @@ module Fontisan
|
|
|
121
121
|
def active_regions(coordinates)
|
|
122
122
|
scalars = calculate_scalars(coordinates)
|
|
123
123
|
scalars.each_with_index.select { |scalar, _| scalar.positive? }
|
|
124
|
-
|
|
124
|
+
.map(&:last)
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
# Get scalar for specific region index
|
|
@@ -197,4 +197,4 @@ module Fontisan
|
|
|
197
197
|
end
|
|
198
198
|
end
|
|
199
199
|
end
|
|
200
|
-
end
|
|
200
|
+
end
|
|
@@ -31,6 +31,9 @@ module Fontisan
|
|
|
31
31
|
# builder = CFF2TableBuilder.new(reader, hint_set)
|
|
32
32
|
# new_cff2 = builder.build
|
|
33
33
|
class TableBuilder < Tables::Cff::TableBuilder
|
|
34
|
+
# CFF2-specific operators not supported by CFF DictBuilder
|
|
35
|
+
INVALID_CFF_KEYS = [24].freeze # operator 24 = vstore (CFF2 only)
|
|
36
|
+
|
|
34
37
|
# @return [CFF2TableReader] CFF2 table reader
|
|
35
38
|
attr_reader :reader
|
|
36
39
|
|
|
@@ -79,7 +82,7 @@ module Fontisan
|
|
|
79
82
|
top_dict: top_dict_hash,
|
|
80
83
|
charstrings: charstrings_data,
|
|
81
84
|
private_dict: private_dict_data,
|
|
82
|
-
vstore: vstore_data
|
|
85
|
+
vstore: vstore_data,
|
|
83
86
|
)
|
|
84
87
|
end
|
|
85
88
|
|
|
@@ -134,7 +137,8 @@ module Fontisan
|
|
|
134
137
|
|
|
135
138
|
# Create rebuilder with stem count
|
|
136
139
|
stem_count = calculate_stem_count
|
|
137
|
-
rebuilder = Cff::CharStringRebuilder.new(charstrings_index,
|
|
140
|
+
rebuilder = Cff::CharStringRebuilder.new(charstrings_index,
|
|
141
|
+
stem_count: stem_count)
|
|
138
142
|
|
|
139
143
|
# Modify each glyph with hints
|
|
140
144
|
hinted_glyph_ids.each do |glyph_id|
|
|
@@ -179,14 +183,14 @@ module Fontisan
|
|
|
179
183
|
# Count stems from blue zones (hstem)
|
|
180
184
|
hstem_count = 0
|
|
181
185
|
blue_values = font_hints["blue_values"] || font_hints[:blue_values]
|
|
182
|
-
if blue_values
|
|
186
|
+
if blue_values.is_a?(Array)
|
|
183
187
|
hstem_count = blue_values.size / 2
|
|
184
188
|
end
|
|
185
189
|
|
|
186
190
|
# Count stems from stem snap (vstem)
|
|
187
191
|
vstem_count = 0
|
|
188
192
|
stem_snap_h = font_hints["stem_snap_h"] || font_hints[:stem_snap_h]
|
|
189
|
-
if stem_snap_h
|
|
193
|
+
if stem_snap_h.is_a?(Array)
|
|
190
194
|
vstem_count = stem_snap_h.size
|
|
191
195
|
end
|
|
192
196
|
|
|
@@ -365,7 +369,7 @@ module Fontisan
|
|
|
365
369
|
# Extract Variable Store bytes unchanged
|
|
366
370
|
# For simplicity, extract from vstore_offset to end of table
|
|
367
371
|
# In production, we'd parse structure to get exact size
|
|
368
|
-
@reader.data[vstore_offset
|
|
372
|
+
@reader.data[vstore_offset..]
|
|
369
373
|
end
|
|
370
374
|
|
|
371
375
|
# Rebuild complete CFF2 table
|
|
@@ -376,7 +380,8 @@ module Fontisan
|
|
|
376
380
|
# @param private_dict [String, nil] Private DICT
|
|
377
381
|
# @param vstore [String, nil] Variable Store
|
|
378
382
|
# @return [String] Complete CFF2 table binary
|
|
379
|
-
def rebuild_cff2_table(header:, top_dict:, charstrings:, private_dict:,
|
|
383
|
+
def rebuild_cff2_table(header:, top_dict:, charstrings:, private_dict:,
|
|
384
|
+
vstore:)
|
|
380
385
|
output = StringIO.new("".b)
|
|
381
386
|
|
|
382
387
|
# 1. Write Header
|
|
@@ -387,7 +392,7 @@ module Fontisan
|
|
|
387
392
|
header_size: header.size,
|
|
388
393
|
charstrings: charstrings,
|
|
389
394
|
private_dict: private_dict,
|
|
390
|
-
vstore: vstore
|
|
395
|
+
vstore: vstore,
|
|
391
396
|
)
|
|
392
397
|
|
|
393
398
|
# 3. Build Top DICT with updated offsets
|
|
@@ -416,7 +421,8 @@ module Fontisan
|
|
|
416
421
|
# @param private_dict [String, nil] Private DICT data
|
|
417
422
|
# @param vstore [String, nil] Variable Store data
|
|
418
423
|
# @return [Hash] Section offsets
|
|
419
|
-
def calculate_cff2_offsets(header_size:, charstrings:, private_dict:,
|
|
424
|
+
def calculate_cff2_offsets(header_size:, charstrings:, private_dict:,
|
|
425
|
+
vstore:)
|
|
420
426
|
# Start after header
|
|
421
427
|
offset = header_size
|
|
422
428
|
|
|
@@ -446,7 +452,7 @@ module Fontisan
|
|
|
446
452
|
charstrings: charstrings_offset,
|
|
447
453
|
private_dict: private_dict_offset,
|
|
448
454
|
private_dict_size: private_dict_size,
|
|
449
|
-
vstore: vstore_offset
|
|
455
|
+
vstore: vstore_offset,
|
|
450
456
|
}
|
|
451
457
|
end
|
|
452
458
|
|
|
@@ -531,23 +537,23 @@ module Fontisan
|
|
|
531
537
|
18 => :private,
|
|
532
538
|
19 => :subrs,
|
|
533
539
|
20 => :default_width_x,
|
|
534
|
-
21 => :nominal_width_x
|
|
535
|
-
|
|
540
|
+
21 => :nominal_width_x,
|
|
541
|
+
# Note: operator 24 (vstore) is CFF2-specific and handled separately
|
|
536
542
|
}
|
|
537
543
|
|
|
538
544
|
result = {}
|
|
539
545
|
dict.each do |key, value|
|
|
540
546
|
# Skip vstore (operator 24) - CFF2 specific, not in CFF DictBuilder
|
|
541
|
-
next if key
|
|
547
|
+
next if INVALID_CFF_KEYS.include?(key)
|
|
542
548
|
|
|
543
549
|
# Convert string keys to symbols for DictBuilder
|
|
544
|
-
if key.is_a?(String)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
550
|
+
symbol_key = if key.is_a?(String)
|
|
551
|
+
key.to_sym
|
|
552
|
+
elsif key.is_a?(Integer)
|
|
553
|
+
operator_map[key] || key
|
|
554
|
+
else
|
|
555
|
+
key
|
|
556
|
+
end
|
|
551
557
|
|
|
552
558
|
result[symbol_key] = value
|
|
553
559
|
end
|
|
@@ -571,4 +577,4 @@ module Fontisan
|
|
|
571
577
|
end
|
|
572
578
|
end
|
|
573
579
|
end
|
|
574
|
-
end
|
|
580
|
+
end
|
|
@@ -61,7 +61,7 @@ module Fontisan
|
|
|
61
61
|
major_version: read_uint8,
|
|
62
62
|
minor_version: read_uint8,
|
|
63
63
|
header_size: read_uint8,
|
|
64
|
-
top_dict_length: read_uint16
|
|
64
|
+
top_dict_length: read_uint16,
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
# Validate CFF2 version
|
|
@@ -105,7 +105,7 @@ module Fontisan
|
|
|
105
105
|
# Parse Variable Store structure
|
|
106
106
|
@variable_store = {
|
|
107
107
|
regions: read_region_list,
|
|
108
|
-
item_variation_data: read_item_variation_data
|
|
108
|
+
item_variation_data: read_item_variation_data,
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
@variable_store
|
|
@@ -140,7 +140,7 @@ module Fontisan
|
|
|
140
140
|
axes << {
|
|
141
141
|
start_coord: read_f2dot14,
|
|
142
142
|
peak_coord: read_f2dot14,
|
|
143
|
-
end_coord: read_f2dot14
|
|
143
|
+
end_coord: read_f2dot14,
|
|
144
144
|
}
|
|
145
145
|
end
|
|
146
146
|
|
|
@@ -158,13 +158,11 @@ module Fontisan
|
|
|
158
158
|
|
|
159
159
|
item_variation_data = []
|
|
160
160
|
|
|
161
|
-
data_count.times do |
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
# break
|
|
167
|
-
end
|
|
161
|
+
data_count.times do |_idx|
|
|
162
|
+
item_data = read_single_item_variation_data
|
|
163
|
+
item_variation_data << item_data
|
|
164
|
+
rescue EOFError
|
|
165
|
+
# break
|
|
168
166
|
end
|
|
169
167
|
|
|
170
168
|
item_variation_data
|
|
@@ -186,32 +184,32 @@ module Fontisan
|
|
|
186
184
|
|
|
187
185
|
# Read delta sets
|
|
188
186
|
delta_sets = []
|
|
189
|
-
item_count.times do |
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
delta_sets << deltas
|
|
206
|
-
rescue EOFError => e
|
|
207
|
-
# break
|
|
187
|
+
item_count.times do |_item_idx|
|
|
188
|
+
deltas = []
|
|
189
|
+
|
|
190
|
+
# Short deltas (16-bit)
|
|
191
|
+
short_delta_count.times do
|
|
192
|
+
break if @io.eof?
|
|
193
|
+
|
|
194
|
+
deltas << read_int16
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Long deltas (8-bit) for remaining regions
|
|
198
|
+
(region_index_count - short_delta_count).times do
|
|
199
|
+
break if @io.eof?
|
|
200
|
+
|
|
201
|
+
deltas << read_int8
|
|
208
202
|
end
|
|
203
|
+
|
|
204
|
+
delta_sets << deltas
|
|
205
|
+
rescue EOFError
|
|
206
|
+
# break
|
|
209
207
|
end
|
|
210
208
|
|
|
211
209
|
{
|
|
212
210
|
item_count: item_count,
|
|
213
211
|
region_indices: region_indices,
|
|
214
|
-
delta_sets: delta_sets
|
|
212
|
+
delta_sets: delta_sets,
|
|
215
213
|
}
|
|
216
214
|
end
|
|
217
215
|
|
|
@@ -249,7 +247,10 @@ module Fontisan
|
|
|
249
247
|
# @raise [EOFError] If not enough bytes available
|
|
250
248
|
def read_safely(bytes, description)
|
|
251
249
|
data = @io.read(bytes)
|
|
252
|
-
|
|
250
|
+
if data.nil? || data.bytesize < bytes
|
|
251
|
+
raise EOFError,
|
|
252
|
+
"Unexpected EOF while reading #{description}"
|
|
253
|
+
end
|
|
253
254
|
|
|
254
255
|
data
|
|
255
256
|
end
|
|
@@ -269,7 +270,8 @@ module Fontisan
|
|
|
269
270
|
|
|
270
271
|
if operator_byte?(byte)
|
|
271
272
|
operator = read_dict_operator(io, byte)
|
|
272
|
-
dict[operator] =
|
|
273
|
+
dict[operator] =
|
|
274
|
+
operands.size == 1 ? operands.first : operands.dup
|
|
273
275
|
operands.clear
|
|
274
276
|
else
|
|
275
277
|
# Operand (number)
|
|
@@ -416,4 +418,4 @@ module Fontisan
|
|
|
416
418
|
end
|
|
417
419
|
end
|
|
418
420
|
end
|
|
419
|
-
end
|
|
421
|
+
end
|
data/lib/fontisan/tables/cff2.rb
CHANGED
|
@@ -118,7 +118,8 @@ module Fontisan
|
|
|
118
118
|
resolve(component_glyph, visited, depth + 1)
|
|
119
119
|
else
|
|
120
120
|
# Convert simple glyph to outline
|
|
121
|
-
Models::Outline.from_truetype(component_glyph,
|
|
121
|
+
Models::Outline.from_truetype(component_glyph,
|
|
122
|
+
component.glyph_index)
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
# Apply transformation matrix
|
|
@@ -119,7 +119,10 @@ module Fontisan
|
|
|
119
119
|
# @raise [ArgumentError] If parameters are invalid
|
|
120
120
|
def self.calculate_error(cubic, quadratics)
|
|
121
121
|
validate_cubic_curve!(cubic)
|
|
122
|
-
|
|
122
|
+
unless quadratics.is_a?(Array)
|
|
123
|
+
raise ArgumentError,
|
|
124
|
+
"quadratics must be Array"
|
|
125
|
+
end
|
|
123
126
|
raise ArgumentError, "quadratics cannot be empty" if quadratics.empty?
|
|
124
127
|
|
|
125
128
|
max_error = 0.0
|
|
@@ -305,7 +308,8 @@ module Fontisan
|
|
|
305
308
|
required.each do |key|
|
|
306
309
|
value = quad[key]
|
|
307
310
|
unless value.is_a?(Numeric)
|
|
308
|
-
raise ArgumentError,
|
|
311
|
+
raise ArgumentError,
|
|
312
|
+
"quad[:#{key}] must be Numeric, got: #{value.class}"
|
|
309
313
|
end
|
|
310
314
|
end
|
|
311
315
|
end
|
|
@@ -324,14 +328,16 @@ module Fontisan
|
|
|
324
328
|
required.each do |key|
|
|
325
329
|
value = cubic[key]
|
|
326
330
|
unless value.is_a?(Numeric)
|
|
327
|
-
raise ArgumentError,
|
|
331
|
+
raise ArgumentError,
|
|
332
|
+
"cubic[:#{key}] must be Numeric, got: #{value.class}"
|
|
328
333
|
end
|
|
329
334
|
end
|
|
330
335
|
end
|
|
331
336
|
|
|
332
337
|
private_class_method def self.validate_max_error!(max_error)
|
|
333
338
|
unless max_error.is_a?(Numeric)
|
|
334
|
-
raise ArgumentError,
|
|
339
|
+
raise ArgumentError,
|
|
340
|
+
"max_error must be Numeric, got: #{max_error.class}"
|
|
335
341
|
end
|
|
336
342
|
|
|
337
343
|
if max_error <= 0
|
|
@@ -106,7 +106,11 @@ module Fontisan
|
|
|
106
106
|
# @raise [ArgumentError] If parameters are invalid
|
|
107
107
|
def self.build_compound_glyph(components, bbox, instructions: "".b)
|
|
108
108
|
raise ArgumentError, "components cannot be nil" if components.nil?
|
|
109
|
-
|
|
109
|
+
|
|
110
|
+
unless components.is_a?(Array)
|
|
111
|
+
raise ArgumentError,
|
|
112
|
+
"components must be Array"
|
|
113
|
+
end
|
|
110
114
|
raise ArgumentError, "components cannot be empty" if components.empty?
|
|
111
115
|
|
|
112
116
|
validate_bbox!(bbox)
|
|
@@ -114,7 +118,8 @@ module Fontisan
|
|
|
114
118
|
build_compound_glyph_data(components, bbox, instructions)
|
|
115
119
|
end
|
|
116
120
|
|
|
117
|
-
private_class_method def self.build_simple_glyph_data(contours, bbox,
|
|
121
|
+
private_class_method def self.build_simple_glyph_data(contours, bbox,
|
|
122
|
+
instructions)
|
|
118
123
|
num_contours = contours.length
|
|
119
124
|
|
|
120
125
|
# Build endPtsOfContours array
|
|
@@ -136,7 +141,8 @@ module Fontisan
|
|
|
136
141
|
|
|
137
142
|
# Header (10 bytes)
|
|
138
143
|
data << [num_contours].pack("n") # numberOfContours
|
|
139
|
-
data << [bbox[:x_min], bbox[:y_min], bbox[:x_max],
|
|
144
|
+
data << [bbox[:x_min], bbox[:y_min], bbox[:x_max],
|
|
145
|
+
bbox[:y_max]].pack("n4")
|
|
140
146
|
|
|
141
147
|
# endPtsOfContours
|
|
142
148
|
data << end_pts_of_contours.pack("n*")
|
|
@@ -155,18 +161,21 @@ module Fontisan
|
|
|
155
161
|
data
|
|
156
162
|
end
|
|
157
163
|
|
|
158
|
-
private_class_method def self.build_compound_glyph_data(components, bbox,
|
|
164
|
+
private_class_method def self.build_compound_glyph_data(components, bbox,
|
|
165
|
+
instructions)
|
|
159
166
|
data = (+"").force_encoding(Encoding::BINARY)
|
|
160
167
|
|
|
161
168
|
# Header (10 bytes) - numberOfContours = -1 for compound
|
|
162
169
|
data << [-1].pack("n") # Use signed pack, will convert to 0xFFFF
|
|
163
|
-
data << [bbox[:x_min], bbox[:y_min], bbox[:x_max],
|
|
170
|
+
data << [bbox[:x_min], bbox[:y_min], bbox[:x_max],
|
|
171
|
+
bbox[:y_max]].pack("n4")
|
|
164
172
|
|
|
165
173
|
# Encode components
|
|
166
174
|
has_instructions = instructions.bytesize.positive?
|
|
167
175
|
components.each_with_index do |component, index|
|
|
168
176
|
is_last = (index == components.length - 1)
|
|
169
|
-
component_data = encode_component(component, is_last,
|
|
177
|
+
component_data = encode_component(component, is_last,
|
|
178
|
+
has_instructions)
|
|
170
179
|
data << component_data
|
|
171
180
|
end
|
|
172
181
|
|
|
@@ -179,7 +188,8 @@ module Fontisan
|
|
|
179
188
|
data
|
|
180
189
|
end
|
|
181
190
|
|
|
182
|
-
private_class_method def self.encode_component(component, is_last,
|
|
191
|
+
private_class_method def self.encode_component(component, is_last,
|
|
192
|
+
has_instructions)
|
|
183
193
|
validate_component!(component)
|
|
184
194
|
|
|
185
195
|
glyph_index = component[:glyph_index]
|
|
@@ -344,7 +354,8 @@ module Fontisan
|
|
|
344
354
|
data
|
|
345
355
|
end
|
|
346
356
|
|
|
347
|
-
private_class_method def self.encode_coordinate_values(flags, deltas,
|
|
357
|
+
private_class_method def self.encode_coordinate_values(flags, deltas,
|
|
358
|
+
axis)
|
|
348
359
|
data = (+"").force_encoding(Encoding::BINARY)
|
|
349
360
|
short_flag = axis == :x ? X_SHORT_VECTOR : Y_SHORT_VECTOR
|
|
350
361
|
same_flag = axis == :x ? X_IS_SAME_OR_POSITIVE_X_SHORT : Y_IS_SAME_OR_POSITIVE_Y_SHORT
|
|
@@ -400,7 +411,10 @@ module Fontisan
|
|
|
400
411
|
# Convert float to F2DOT14 fixed-point format
|
|
401
412
|
# F2DOT14: 2 bits integer, 14 bits fractional
|
|
402
413
|
# Range: -2.0 to ~1.99993896484375
|
|
403
|
-
|
|
414
|
+
if value < -2.0 || value > 2.0
|
|
415
|
+
raise ArgumentError,
|
|
416
|
+
"value out of F2DOT14 range"
|
|
417
|
+
end
|
|
404
418
|
|
|
405
419
|
fixed = (value * 16_384.0).round
|
|
406
420
|
# Convert to unsigned 16-bit
|
|
@@ -434,7 +448,10 @@ module Fontisan
|
|
|
434
448
|
end
|
|
435
449
|
|
|
436
450
|
private_class_method def self.validate_component!(component)
|
|
437
|
-
|
|
451
|
+
unless component.is_a?(Hash)
|
|
452
|
+
raise ArgumentError,
|
|
453
|
+
"component must be Hash"
|
|
454
|
+
end
|
|
438
455
|
unless component[:glyph_index]
|
|
439
456
|
raise ArgumentError, "component must have :glyph_index"
|
|
440
457
|
end
|
data/lib/fontisan/tables/name.rb
CHANGED
|
@@ -173,13 +173,13 @@ module Fontisan
|
|
|
173
173
|
record = find_name_record(
|
|
174
174
|
name_id,
|
|
175
175
|
platform: PLATFORM_WINDOWS,
|
|
176
|
-
language: WINDOWS_LANGUAGE_EN_US
|
|
176
|
+
language: WINDOWS_LANGUAGE_EN_US,
|
|
177
177
|
)
|
|
178
178
|
|
|
179
179
|
record ||= find_name_record(
|
|
180
180
|
name_id,
|
|
181
181
|
platform: PLATFORM_MACINTOSH,
|
|
182
|
-
language: MAC_LANGUAGE_ENGLISH
|
|
182
|
+
language: MAC_LANGUAGE_ENGLISH,
|
|
183
183
|
)
|
|
184
184
|
|
|
185
185
|
return nil unless record
|
|
@@ -236,10 +236,10 @@ module Fontisan
|
|
|
236
236
|
decoded = case record.platform_id
|
|
237
237
|
when PLATFORM_WINDOWS, PLATFORM_UNICODE
|
|
238
238
|
string_data.dup.force_encoding("UTF-16BE")
|
|
239
|
-
|
|
239
|
+
.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
240
240
|
when PLATFORM_MACINTOSH
|
|
241
241
|
string_data.dup.force_encoding("ASCII-8BIT")
|
|
242
|
-
|
|
242
|
+
.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
243
243
|
else
|
|
244
244
|
string_data.dup.force_encoding("UTF-8")
|
|
245
245
|
end
|
|
@@ -233,7 +233,8 @@ module Fontisan
|
|
|
233
233
|
batch_entries.each do |entry|
|
|
234
234
|
relative_offset = entry.offset - batch_offset
|
|
235
235
|
tag_key = entry.tag.dup.force_encoding("UTF-8")
|
|
236
|
-
@table_data[tag_key] =
|
|
236
|
+
@table_data[tag_key] =
|
|
237
|
+
batch_data[relative_offset, entry.table_length]
|
|
237
238
|
end
|
|
238
239
|
end
|
|
239
240
|
|
|
@@ -307,6 +308,7 @@ module Fontisan
|
|
|
307
308
|
# @return [Boolean] true if table is available in current mode
|
|
308
309
|
def table_available?(tag)
|
|
309
310
|
return false unless has_table?(tag)
|
|
311
|
+
|
|
310
312
|
LoadingModes.table_allowed?(@loading_mode, tag)
|
|
311
313
|
end
|
|
312
314
|
|