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
|
@@ -239,8 +239,11 @@ module Fontisan
|
|
|
239
239
|
# 3-byte signed integer (16-bit)
|
|
240
240
|
b1 = io.getbyte
|
|
241
241
|
b2 = io.getbyte
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
if b1.nil? || b2.nil?
|
|
243
|
+
raise CorruptedTableError,
|
|
244
|
+
"Unexpected end of CharString reading shortint"
|
|
245
|
+
end
|
|
246
|
+
|
|
244
247
|
value = (b1 << 8) | b2
|
|
245
248
|
value > 0x7FFF ? value - 0x10000 : value
|
|
246
249
|
when 32..246
|
|
@@ -249,20 +252,29 @@ module Fontisan
|
|
|
249
252
|
when 247..250
|
|
250
253
|
# Positive 2-byte integer: +108 to +1131
|
|
251
254
|
b2 = io.getbyte
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
if b2.nil?
|
|
256
|
+
raise CorruptedTableError,
|
|
257
|
+
"Unexpected end of CharString reading positive integer"
|
|
258
|
+
end
|
|
259
|
+
|
|
254
260
|
(byte - 247) * 256 + b2 + 108
|
|
255
261
|
when 251..254
|
|
256
262
|
# Negative 2-byte integer: -108 to -1131
|
|
257
263
|
b2 = io.getbyte
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
if b2.nil?
|
|
265
|
+
raise CorruptedTableError,
|
|
266
|
+
"Unexpected end of CharString reading negative integer"
|
|
267
|
+
end
|
|
268
|
+
|
|
260
269
|
-(byte - 251) * 256 - b2 - 108
|
|
261
270
|
when 255
|
|
262
271
|
# 5-byte signed integer (32-bit) as fixed-point 16.16
|
|
263
272
|
bytes = io.read(4)
|
|
264
|
-
|
|
265
|
-
|
|
273
|
+
if bytes.nil? || bytes.length < 4
|
|
274
|
+
raise CorruptedTableError,
|
|
275
|
+
"Unexpected end of CharString reading fixed-point"
|
|
276
|
+
end
|
|
277
|
+
|
|
266
278
|
value = bytes.unpack1("l>") # Signed 32-bit big-endian
|
|
267
279
|
value / 65536.0 # Convert to float
|
|
268
280
|
else
|
|
@@ -368,7 +380,8 @@ module Fontisan
|
|
|
368
380
|
# rmoveto takes 2 operands, so if stack has 3 and width not parsed,
|
|
369
381
|
# first is width
|
|
370
382
|
parse_width_for_operator(width_parsed, 2)
|
|
371
|
-
return if @stack.size < 2
|
|
383
|
+
return if @stack.size < 2 # Need at least 2 values
|
|
384
|
+
|
|
372
385
|
dy = @stack.pop
|
|
373
386
|
dx = @stack.pop
|
|
374
387
|
@x += dx
|
|
@@ -383,7 +396,8 @@ module Fontisan
|
|
|
383
396
|
# hmoveto takes 1 operand, so if stack has 2 and width not parsed,
|
|
384
397
|
# first is width
|
|
385
398
|
parse_width_for_operator(width_parsed, 1)
|
|
386
|
-
return if @stack.empty?
|
|
399
|
+
return if @stack.empty? # Need at least 1 value
|
|
400
|
+
|
|
387
401
|
dx = @stack.pop || 0
|
|
388
402
|
@x += dx
|
|
389
403
|
@path << { type: :move_to, x: @x, y: @y }
|
|
@@ -396,7 +410,8 @@ module Fontisan
|
|
|
396
410
|
# vmoveto takes 1 operand, so if stack has 2 and width not parsed,
|
|
397
411
|
# first is width
|
|
398
412
|
parse_width_for_operator(width_parsed, 1)
|
|
399
|
-
return if @stack.empty?
|
|
413
|
+
return if @stack.empty? # Need at least 1 value
|
|
414
|
+
|
|
400
415
|
dy = @stack.pop || 0
|
|
401
416
|
@y += dy
|
|
402
417
|
@path << { type: :move_to, x: @x, y: @y }
|
|
@@ -688,7 +703,7 @@ module Fontisan
|
|
|
688
703
|
dy3 = @stack.shift
|
|
689
704
|
|
|
690
705
|
x1 = @x + dx1
|
|
691
|
-
y1 = @y +dy1
|
|
706
|
+
y1 = @y + dy1
|
|
692
707
|
x2 = x1 + dx2
|
|
693
708
|
y2 = y1 + dy2
|
|
694
709
|
@x = x2 + dx3
|
|
@@ -785,6 +800,7 @@ module Fontisan
|
|
|
785
800
|
# Call local subroutine
|
|
786
801
|
def callsubr
|
|
787
802
|
return if @stack.empty?
|
|
803
|
+
|
|
788
804
|
subr_num = @stack.pop
|
|
789
805
|
return unless subr_num # Guard against empty stack
|
|
790
806
|
|
|
@@ -798,6 +814,7 @@ module Fontisan
|
|
|
798
814
|
# Call global subroutine
|
|
799
815
|
def callgsubr
|
|
800
816
|
return if @stack.empty?
|
|
817
|
+
|
|
801
818
|
subr_num = @stack.pop
|
|
802
819
|
return unless subr_num # Guard against empty stack
|
|
803
820
|
|
|
@@ -838,6 +855,7 @@ module Fontisan
|
|
|
838
855
|
# Arithmetic operators
|
|
839
856
|
def arithmetic_add
|
|
840
857
|
return if @stack.size < 2
|
|
858
|
+
|
|
841
859
|
b = @stack.pop
|
|
842
860
|
a = @stack.pop
|
|
843
861
|
@stack << (a + b)
|
|
@@ -845,6 +863,7 @@ module Fontisan
|
|
|
845
863
|
|
|
846
864
|
def arithmetic_sub
|
|
847
865
|
return if @stack.size < 2
|
|
866
|
+
|
|
848
867
|
b = @stack.pop
|
|
849
868
|
a = @stack.pop
|
|
850
869
|
@stack << (a - b)
|
|
@@ -852,6 +871,7 @@ module Fontisan
|
|
|
852
871
|
|
|
853
872
|
def arithmetic_mul
|
|
854
873
|
return if @stack.size < 2
|
|
874
|
+
|
|
855
875
|
b = @stack.pop
|
|
856
876
|
a = @stack.pop
|
|
857
877
|
@stack << (a * b)
|
|
@@ -859,26 +879,32 @@ module Fontisan
|
|
|
859
879
|
|
|
860
880
|
def arithmetic_div
|
|
861
881
|
return if @stack.size < 2
|
|
882
|
+
|
|
862
883
|
b = @stack.pop
|
|
863
884
|
a = @stack.pop
|
|
864
885
|
return if b.zero?
|
|
886
|
+
|
|
865
887
|
@stack << (a / b.to_f)
|
|
866
888
|
end
|
|
867
889
|
|
|
868
890
|
def arithmetic_neg
|
|
869
891
|
return if @stack.empty?
|
|
892
|
+
|
|
870
893
|
@stack << -@stack.pop
|
|
871
894
|
end
|
|
872
895
|
|
|
873
896
|
def arithmetic_abs
|
|
874
897
|
return if @stack.empty?
|
|
898
|
+
|
|
875
899
|
@stack << @stack.pop.abs
|
|
876
900
|
end
|
|
877
901
|
|
|
878
902
|
def arithmetic_sqrt
|
|
879
903
|
return if @stack.empty?
|
|
904
|
+
|
|
880
905
|
val = @stack.pop
|
|
881
906
|
return if val.negative?
|
|
907
|
+
|
|
882
908
|
@stack << Math.sqrt(val)
|
|
883
909
|
end
|
|
884
910
|
|
|
@@ -130,7 +130,7 @@ module Fontisan
|
|
|
130
130
|
type: :operator,
|
|
131
131
|
name: operator,
|
|
132
132
|
operands: operand_stack.dup,
|
|
133
|
-
hint_data: hint_data
|
|
133
|
+
hint_data: hint_data,
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
# Clear operand stack
|
|
@@ -163,7 +163,7 @@ module Fontisan
|
|
|
163
163
|
# @param byte [Integer] Byte value
|
|
164
164
|
# @return [Boolean] True if operator byte
|
|
165
165
|
def operator_byte?(byte)
|
|
166
|
-
|
|
166
|
+
byte <= 31 && byte != 28 # Operators are 0-31 except 28 (shortint)
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
# Read operator from CharString
|
|
@@ -200,8 +200,11 @@ module Fontisan
|
|
|
200
200
|
# 3-byte signed integer (16-bit)
|
|
201
201
|
b1 = io.getbyte
|
|
202
202
|
b2 = io.getbyte
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
if b1.nil? || b2.nil?
|
|
204
|
+
raise CorruptedTableError,
|
|
205
|
+
"Unexpected end of CharString reading shortint"
|
|
206
|
+
end
|
|
207
|
+
|
|
205
208
|
value = (b1 << 8) | b2
|
|
206
209
|
value > 0x7FFF ? value - 0x10000 : value
|
|
207
210
|
when 32..246
|
|
@@ -210,20 +213,29 @@ module Fontisan
|
|
|
210
213
|
when 247..250
|
|
211
214
|
# Positive 2-byte integer: +108 to +1131
|
|
212
215
|
b2 = io.getbyte
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
if b2.nil?
|
|
217
|
+
raise CorruptedTableError,
|
|
218
|
+
"Unexpected end of CharString reading positive integer"
|
|
219
|
+
end
|
|
220
|
+
|
|
215
221
|
(byte - 247) * 256 + b2 + 108
|
|
216
222
|
when 251..254
|
|
217
223
|
# Negative 2-byte integer: -108 to -1131
|
|
218
224
|
b2 = io.getbyte
|
|
219
|
-
|
|
220
|
-
|
|
225
|
+
if b2.nil?
|
|
226
|
+
raise CorruptedTableError,
|
|
227
|
+
"Unexpected end of CharString reading negative integer"
|
|
228
|
+
end
|
|
229
|
+
|
|
221
230
|
-(byte - 251) * 256 - b2 - 108
|
|
222
231
|
when 255
|
|
223
232
|
# 5-byte signed integer (32-bit) as fixed-point 16.16
|
|
224
233
|
bytes = io.read(4)
|
|
225
|
-
|
|
226
|
-
|
|
234
|
+
if bytes.nil? || bytes.length < 4
|
|
235
|
+
raise CorruptedTableError,
|
|
236
|
+
"Unexpected end of CharString reading fixed-point"
|
|
237
|
+
end
|
|
238
|
+
|
|
227
239
|
value = bytes.unpack1("l>") # Signed 32-bit big-endian
|
|
228
240
|
value / 65536.0 # Convert to float
|
|
229
241
|
else
|
|
@@ -234,4 +246,4 @@ module Fontisan
|
|
|
234
246
|
end
|
|
235
247
|
end
|
|
236
248
|
end
|
|
237
|
-
end
|
|
249
|
+
end
|
|
@@ -56,7 +56,7 @@ module Fontisan
|
|
|
56
56
|
# @yield [operations] Block to modify operations
|
|
57
57
|
# @yieldparam operations [Array<Hash>] Parsed operations
|
|
58
58
|
# @yieldreturn [Array<Hash>] Modified operations
|
|
59
|
-
def modify_charstring(glyph_index
|
|
59
|
+
def modify_charstring(glyph_index)
|
|
60
60
|
# Get original CharString data
|
|
61
61
|
original_data = @source_index[glyph_index]
|
|
62
62
|
return unless original_data
|
|
@@ -66,7 +66,7 @@ module Fontisan
|
|
|
66
66
|
operations = parser.parse
|
|
67
67
|
|
|
68
68
|
# Apply modification
|
|
69
|
-
modified_operations =
|
|
69
|
+
modified_operations = yield(operations)
|
|
70
70
|
|
|
71
71
|
# Build new CharString
|
|
72
72
|
new_data = CharStringBuilder.build_from_operations(modified_operations)
|
|
@@ -86,13 +86,13 @@ module Fontisan
|
|
|
86
86
|
charstrings = []
|
|
87
87
|
|
|
88
88
|
(0...@source_index.count).each do |i|
|
|
89
|
-
if @modifications.key?(i)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
charstrings << if @modifications.key?(i)
|
|
90
|
+
# Use modified CharString
|
|
91
|
+
@modifications[i]
|
|
92
|
+
else
|
|
93
|
+
# Use original CharString
|
|
94
|
+
@source_index[i]
|
|
95
|
+
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
# Build INDEX
|
|
@@ -108,10 +108,10 @@ module Fontisan
|
|
|
108
108
|
# @yieldparam glyph_index [Integer] Current glyph index
|
|
109
109
|
# @yieldparam operations [Array<Hash>] Parsed operations
|
|
110
110
|
# @yieldreturn [Array<Hash>] Modified operations
|
|
111
|
-
def batch_modify(glyph_indices
|
|
111
|
+
def batch_modify(glyph_indices)
|
|
112
112
|
glyph_indices.each do |glyph_index|
|
|
113
113
|
modify_charstring(glyph_index) do |operations|
|
|
114
|
-
|
|
114
|
+
yield(glyph_index, operations)
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
end
|
|
@@ -124,10 +124,10 @@ module Fontisan
|
|
|
124
124
|
# @yieldparam glyph_index [Integer] Current glyph index
|
|
125
125
|
# @yieldparam operations [Array<Hash>] Parsed operations
|
|
126
126
|
# @yieldreturn [Array<Hash>] Modified operations
|
|
127
|
-
def modify_all
|
|
127
|
+
def modify_all
|
|
128
128
|
(0...@source_index.count).each do |i|
|
|
129
129
|
modify_charstring(i) do |operations|
|
|
130
|
-
|
|
130
|
+
yield(i, operations)
|
|
131
131
|
end
|
|
132
132
|
end
|
|
133
133
|
end
|
|
@@ -169,4 +169,4 @@ module Fontisan
|
|
|
169
169
|
end
|
|
170
170
|
end
|
|
171
171
|
end
|
|
172
|
-
end
|
|
172
|
+
end
|
|
@@ -93,7 +93,10 @@ module Fontisan
|
|
|
93
93
|
dict_hash.each do |operator_name, value|
|
|
94
94
|
# Get operator bytes
|
|
95
95
|
operator_bytes = operator_for_name(operator_name)
|
|
96
|
-
|
|
96
|
+
unless operator_bytes
|
|
97
|
+
raise ArgumentError,
|
|
98
|
+
"Unknown operator: #{operator_name}"
|
|
99
|
+
end
|
|
97
100
|
|
|
98
101
|
# Write operands (value can be single value or array)
|
|
99
102
|
if value.is_a?(Array)
|
|
@@ -120,7 +120,7 @@ module Fontisan
|
|
|
120
120
|
type: :operator,
|
|
121
121
|
name: operator,
|
|
122
122
|
operands: args,
|
|
123
|
-
hint_data: nil
|
|
123
|
+
hint_data: nil,
|
|
124
124
|
}]
|
|
125
125
|
end
|
|
126
126
|
|
|
@@ -144,7 +144,7 @@ module Fontisan
|
|
|
144
144
|
type: :operator,
|
|
145
145
|
name: :hintmask,
|
|
146
146
|
operands: [],
|
|
147
|
-
hint_data: hint_data
|
|
147
|
+
hint_data: hint_data,
|
|
148
148
|
}]
|
|
149
149
|
end
|
|
150
150
|
|
|
@@ -168,7 +168,7 @@ module Fontisan
|
|
|
168
168
|
type: :operator,
|
|
169
169
|
name: :cntrmask,
|
|
170
170
|
operands: [],
|
|
171
|
-
hint_data: hint_data
|
|
171
|
+
hint_data: hint_data,
|
|
172
172
|
}]
|
|
173
173
|
end
|
|
174
174
|
|
|
@@ -188,6 +188,7 @@ module Fontisan
|
|
|
188
188
|
vvcurveto hhcurveto vhcurveto hvcurveto
|
|
189
189
|
]
|
|
190
190
|
|
|
191
|
+
# rubocop:disable Style/CombinableLoops
|
|
191
192
|
# Find first path operator
|
|
192
193
|
operations.each_with_index do |op, index|
|
|
193
194
|
return index if path_operators.include?(op[:name])
|
|
@@ -197,6 +198,7 @@ module Fontisan
|
|
|
197
198
|
operations.each_with_index do |op, index|
|
|
198
199
|
return index if op[:name] == :endchar
|
|
199
200
|
end
|
|
201
|
+
# rubocop:enable Style/CombinableLoops
|
|
200
202
|
|
|
201
203
|
# Empty or malformed - inject at start
|
|
202
204
|
0
|
|
@@ -204,4 +206,4 @@ module Fontisan
|
|
|
204
206
|
end
|
|
205
207
|
end
|
|
206
208
|
end
|
|
207
|
-
end
|
|
209
|
+
end
|
|
@@ -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
|