fontisan 0.4.43 → 0.4.44
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/TODO.bug-fixes/12-type1-generators-duck-typing.md +80 -0
- data/TODO.bug-fixes/13-hint-extractor-duck-typing.md +30 -0
- data/TODO.bug-fixes/14-font-interface-type-checks.md +82 -0
- data/TODO.bug-fixes/15-glyph-type-checks-and-simple-glyph-points-bug.md +49 -0
- data/TODO.bug-fixes/16-variation-table-checks.md +31 -0
- data/TODO.bug-fixes/17-ufo-and-stitcher-cleanup.md +32 -0
- data/TODO.bug-fixes/18-generic-value-and-io-checks.md +56 -0
- data/TODO.bug-fixes/19-serialization-migration.md +52 -0
- data/TODO.bug-fixes/20-variation-validator-type-checks.md +29 -0
- data/TODO.bug-fixes/21-xml-builder-send.md +24 -0
- data/TODO.bug-fixes/22-type1-converter-font-info.md +25 -0
- data/TODO.bug-fixes/23-boundary-type-checks.md +31 -0
- data/TODO.bug-fixes/24-validator-dynamic-field-key.md +25 -0
- data/TODO.bug-fixes/25-serialization-migration.md +49 -0
- data/TODO.bug-fixes/26-variation-preserver-spec-migration.md +25 -0
- data/TODO.bug-fixes/27-table-class-registry.md +50 -0
- data/TODO.bug-fixes/28-spec-doubles-cleanup.md +40 -0
- data/TODO.bug-fixes/29-strategy-registries.md +30 -0
- data/TODO.bug-fixes/README.md +24 -0
- data/lib/fontisan/binary/base_record.rb +13 -12
- data/lib/fontisan/collection/builder.rb +3 -5
- data/lib/fontisan/collection/dfont_builder.rb +4 -11
- data/lib/fontisan/collection/writer.rb +1 -1
- data/lib/fontisan/commands/pack_command.rb +4 -4
- data/lib/fontisan/commands/unpack_command.rb +1 -1
- data/lib/fontisan/commands/validate_command.rb +1 -1
- data/lib/fontisan/converters/format_converter.rb +2 -2
- data/lib/fontisan/converters/glyf_table_builder.rb +1 -1
- data/lib/fontisan/converters/outline_converter.rb +2 -6
- data/lib/fontisan/converters/table_copier.rb +4 -12
- data/lib/fontisan/converters/type1_converter.rb +12 -37
- data/lib/fontisan/converters/woff2_encoder.rb +8 -10
- data/lib/fontisan/converters/woff_writer.rb +3 -3
- data/lib/fontisan/export/exporter.rb +8 -9
- data/lib/fontisan/export/table_serializer.rb +71 -186
- data/lib/fontisan/export/transformers/font_to_ttx.rb +18 -18
- data/lib/fontisan/export/transformers/head_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/hhea_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/maxp_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/name_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/os2_transformer.rb +1 -1
- data/lib/fontisan/export/transformers/post_transformer.rb +1 -1
- data/lib/fontisan/export/ttx_generator.rb +36 -37
- data/lib/fontisan/font_writer.rb +1 -1
- data/lib/fontisan/glyph_accessor.rb +7 -9
- data/lib/fontisan/hints/postscript_hint_extractor.rb +24 -64
- data/lib/fontisan/hints/truetype_hint_extractor.rb +1 -1
- data/lib/fontisan/models/ttx/tables/binary_table.rb +1 -1
- data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
- data/lib/fontisan/sfnt_font.rb +2 -0
- data/lib/fontisan/sfnt_source.rb +28 -0
- data/lib/fontisan/stitcher/source.rb +8 -10
- data/lib/fontisan/subset/builder.rb +2 -2
- data/lib/fontisan/subset/table_strategy/cff2.rb +2 -3
- data/lib/fontisan/svg_to_glyf/assembler.rb +59 -20
- data/lib/fontisan/svg_to_glyf/document.rb +28 -9
- data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +34 -17
- data/lib/fontisan/tables/cff/charstring.rb +3 -0
- data/lib/fontisan/tables/cff/dict.rb +6 -3
- data/lib/fontisan/tables/cff/index.rb +1 -1
- data/lib/fontisan/tables/cff/private_dict_writer.rb +2 -2
- data/lib/fontisan/tables/cff/table_builder.rb +1 -1
- data/lib/fontisan/tables/cff.rb +3 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +2 -4
- data/lib/fontisan/tables/cff2.rb +3 -0
- data/lib/fontisan/tables/cmap.rb +3 -0
- data/lib/fontisan/tables/cvar.rb +3 -0
- data/lib/fontisan/tables/fvar.rb +3 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +9 -0
- data/lib/fontisan/tables/glyf.rb +12 -8
- data/lib/fontisan/tables/glyf_table.rb +1 -1
- data/lib/fontisan/tables/gvar.rb +3 -0
- data/lib/fontisan/tables/head.rb +3 -0
- data/lib/fontisan/tables/hhea.rb +3 -0
- data/lib/fontisan/tables/hmtx.rb +3 -7
- data/lib/fontisan/tables/hvar.rb +4 -0
- data/lib/fontisan/tables/loca.rb +3 -0
- data/lib/fontisan/tables/maxp.rb +3 -0
- data/lib/fontisan/tables/mvar.rb +3 -0
- data/lib/fontisan/tables/name.rb +3 -0
- data/lib/fontisan/tables/os2.rb +3 -0
- data/lib/fontisan/tables/post.rb +3 -0
- data/lib/fontisan/tables/registry.rb +71 -0
- data/lib/fontisan/tables/vvar.rb +3 -0
- data/lib/fontisan/tables.rb +2 -0
- data/lib/fontisan/type1/afm_generator.rb +12 -65
- data/lib/fontisan/type1/charstring_converter.rb +2 -2
- data/lib/fontisan/type1/conversion_options.rb +3 -2
- data/lib/fontisan/type1/decryptor.rb +1 -1
- data/lib/fontisan/type1/generator.rb +14 -9
- data/lib/fontisan/type1/inf_generator.rb +10 -35
- data/lib/fontisan/type1/pfa_generator.rb +36 -26
- data/lib/fontisan/type1/pfb_generator.rb +24 -12
- data/lib/fontisan/type1/pfm_generator.rb +14 -31
- data/lib/fontisan/type1/ttf_to_type1_converter.rb +30 -24
- data/lib/fontisan/ufo/bounds.rb +91 -0
- data/lib/fontisan/ufo/compile/fvar.rb +2 -2
- data/lib/fontisan/ufo/contour.rb +5 -0
- data/lib/fontisan/ufo/convert/from_bin_data.rb +15 -29
- data/lib/fontisan/ufo/info.rb +4 -0
- data/lib/fontisan/ufo/point.rb +0 -7
- data/lib/fontisan/ufo.rb +1 -0
- data/lib/fontisan/utilities/brotli_wrapper.rb +3 -5
- data/lib/fontisan/utilities/padding.rb +9 -8
- data/lib/fontisan/validation/collection_validator.rb +2 -2
- data/lib/fontisan/validators/validator.rb +4 -4
- data/lib/fontisan/variable/delta_applicator.rb +4 -11
- data/lib/fontisan/variable/metric_delta_processor.rb +3 -3
- data/lib/fontisan/variable/static_font_builder.rb +19 -23
- data/lib/fontisan/variation/instance_font_wrapper.rb +41 -0
- data/lib/fontisan/variation/instance_writer.rb +19 -17
- data/lib/fontisan/variation/metrics_adjuster.rb +17 -20
- data/lib/fontisan/variation/validator.rb +26 -38
- data/lib/fontisan/variation/variable_svg_generator.rb +0 -48
- data/lib/fontisan/variation/variation_preserver.rb +2 -3
- data/lib/fontisan/variation.rb +1 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/sfnt_checksum.rb +1 -1
- data/lib/fontisan/woff2/table_transformer.rb +22 -29
- data/lib/fontisan/woff2_font.rb +9 -8
- data/lib/fontisan/woff_font.rb +2 -0
- data/lib/fontisan.rb +1 -0
- metadata +24 -2
|
@@ -134,15 +134,11 @@ module Fontisan
|
|
|
134
134
|
|
|
135
135
|
# Font info
|
|
136
136
|
if name_table
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
dict << "/Version (#{version}) def" if version
|
|
140
|
-
end
|
|
137
|
+
version = name_table.english_name(Tables::Name::VERSION)
|
|
138
|
+
dict << "/Version (#{version}) def" if version
|
|
141
139
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
dict << "/Notice (#{copyright}) def" if copyright
|
|
145
|
-
end
|
|
140
|
+
copyright = name_table.english_name(Tables::Name::COPYRIGHT)
|
|
141
|
+
dict << "/Notice (#{copyright}) def" if copyright
|
|
146
142
|
end
|
|
147
143
|
|
|
148
144
|
dict << "currentdict end"
|
|
@@ -161,12 +157,12 @@ module Fontisan
|
|
|
161
157
|
# Blue values (for hinting)
|
|
162
158
|
# These are typically derived from the font's alignment zones
|
|
163
159
|
os2 = @font.table(Constants::OS2_TAG)
|
|
164
|
-
if os2
|
|
160
|
+
if os2&.s_typo_ascender
|
|
165
161
|
blue_values = [
|
|
166
|
-
@scaler.scale(os2.
|
|
167
|
-
@scaler.scale(os2.
|
|
168
|
-
@scaler.scale(os2.
|
|
169
|
-
@scaler.scale(os2.
|
|
162
|
+
@scaler.scale(os2.s_typo_descender || -200),
|
|
163
|
+
@scaler.scale(os2.s_typo_descender || -200) + 20,
|
|
164
|
+
@scaler.scale(os2.s_typo_ascender),
|
|
165
|
+
@scaler.scale(os2.s_typo_ascender) + 10,
|
|
170
166
|
]
|
|
171
167
|
dict << "/BlueValues {#{blue_values.join(' ')}} def"
|
|
172
168
|
else
|
|
@@ -178,15 +174,15 @@ module Fontisan
|
|
|
178
174
|
dict << "/BlueFuzz 1 def"
|
|
179
175
|
|
|
180
176
|
# Stem snap hints
|
|
181
|
-
if os2
|
|
177
|
+
if os2&.us_weight_class
|
|
182
178
|
stem_width = @scaler.scale([100, 80,
|
|
183
|
-
90][os2.
|
|
179
|
+
90][os2.us_weight_class / 100] || 80)
|
|
184
180
|
dict << "/StemSnapH [#{stem_width}] def"
|
|
185
181
|
dict << "/StemSnapV [#{stem_width}] def"
|
|
186
182
|
end
|
|
187
183
|
|
|
188
184
|
# Force bold flag
|
|
189
|
-
dict << if os2
|
|
185
|
+
dict << if os2&.us_weight_class && os2.us_weight_class >= 700
|
|
190
186
|
"/ForceBold true def"
|
|
191
187
|
else
|
|
192
188
|
"/ForceBold false def"
|
|
@@ -286,33 +282,33 @@ module Fontisan
|
|
|
286
282
|
|
|
287
283
|
# Generate a simple CharString for a glyph
|
|
288
284
|
#
|
|
289
|
-
# @param glyf_table [
|
|
285
|
+
# @param glyf_table [Tables::Glyf] TTF glyf table
|
|
290
286
|
# @param gid [Integer] Glyph ID
|
|
291
287
|
# @return [String] Type 1 CharString data
|
|
292
288
|
def simple_charstring(glyf_table, gid)
|
|
293
289
|
glyph = glyf_table.glyph(gid)
|
|
294
290
|
|
|
295
291
|
# Empty or compound glyph
|
|
296
|
-
if glyph.nil? || glyph.
|
|
292
|
+
if glyph.nil? || glyph.num_contours.zero? || glyph.compound?
|
|
297
293
|
# Return empty charstring (hsbw + endchar)
|
|
298
294
|
return [0, 500, 14].pack("C*")
|
|
299
295
|
end
|
|
300
296
|
|
|
301
297
|
# For simple glyphs without curve conversion, generate minimal charstring
|
|
302
|
-
lsb =
|
|
303
|
-
|
|
304
|
-
bytes = [13, lsb, width] # hsbw command (13)
|
|
298
|
+
lsb, width = hmtx_metrics_for(gid)
|
|
299
|
+
bytes = [13, @scaler.scale(lsb), @scaler.scale(width)] # hsbw command (13)
|
|
305
300
|
|
|
306
301
|
# Add simple line commands (very basic)
|
|
307
|
-
|
|
302
|
+
points = glyph.points
|
|
303
|
+
unless points.empty?
|
|
308
304
|
# Just draw lines between consecutive on-curve points
|
|
309
305
|
prev_point = nil
|
|
310
|
-
|
|
311
|
-
next unless point
|
|
306
|
+
points.each do |point|
|
|
307
|
+
next unless point[:on_curve]
|
|
312
308
|
|
|
313
309
|
if prev_point
|
|
314
|
-
dx = @scaler.scale(point
|
|
315
|
-
dy = @scaler.scale(point
|
|
310
|
+
dx = @scaler.scale(point[:x]) - @scaler.scale(prev_point[:x])
|
|
311
|
+
dy = @scaler.scale(point[:y]) - @scaler.scale(prev_point[:y])
|
|
316
312
|
bytes << 5 # rlineto
|
|
317
313
|
bytes.concat(encode_number(dx))
|
|
318
314
|
bytes.concat(encode_number(dy))
|
|
@@ -325,6 +321,20 @@ module Fontisan
|
|
|
325
321
|
bytes.pack("C*")
|
|
326
322
|
end
|
|
327
323
|
|
|
324
|
+
# Look up LSB and advance width from hmtx for a glyph.
|
|
325
|
+
#
|
|
326
|
+
# @param gid [Integer] Glyph ID
|
|
327
|
+
# @return [Array(Integer, Integer)] [lsb, advance_width]
|
|
328
|
+
def hmtx_metrics_for(gid)
|
|
329
|
+
hmtx = @font.table(Constants::HMTX_TAG)
|
|
330
|
+
return [0, 500] unless hmtx
|
|
331
|
+
|
|
332
|
+
metric = hmtx.metric_for(gid)
|
|
333
|
+
return [0, 500] unless metric
|
|
334
|
+
|
|
335
|
+
[metric[:lsb] || 0, metric[:advance_width] || 500]
|
|
336
|
+
end
|
|
337
|
+
|
|
328
338
|
# Encode a number for Type 1 CharString
|
|
329
339
|
#
|
|
330
340
|
# @param value [Integer] Number to encode
|
|
@@ -159,15 +159,15 @@ module Fontisan
|
|
|
159
159
|
|
|
160
160
|
# Stem snap hints
|
|
161
161
|
os2 = @font.table(Constants::OS2_TAG)
|
|
162
|
-
if os2
|
|
162
|
+
if os2&.us_weight_class
|
|
163
163
|
stem_width = @scaler.scale([100, 80,
|
|
164
|
-
90][os2.
|
|
164
|
+
90][os2.us_weight_class / 100] || 80)
|
|
165
165
|
dict << "/StemSnapH [#{stem_width}] def"
|
|
166
166
|
dict << "/StemSnapV [#{stem_width}] def"
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
# Force bold flag
|
|
170
|
-
dict << if os2
|
|
170
|
+
dict << if os2&.us_weight_class && os2.us_weight_class >= 700
|
|
171
171
|
"/ForceBold true def"
|
|
172
172
|
else
|
|
173
173
|
"/ForceBold false def"
|
|
@@ -261,30 +261,42 @@ module Fontisan
|
|
|
261
261
|
glyph = glyf_table.glyph(gid)
|
|
262
262
|
|
|
263
263
|
# Empty or compound glyph
|
|
264
|
-
if glyph.nil? || glyph.
|
|
264
|
+
if glyph.nil? || glyph.num_contours.zero? || glyph.compound?
|
|
265
265
|
# Return empty charstring (hsbw + endchar)
|
|
266
266
|
return [0, 500, 14].pack("C*")
|
|
267
267
|
end
|
|
268
268
|
|
|
269
269
|
# For simple glyphs without curve conversion, generate line-based charstring
|
|
270
270
|
# This is a simplified implementation
|
|
271
|
-
lsb =
|
|
272
|
-
|
|
273
|
-
bytes = [0, lsb, width] # hsbw
|
|
271
|
+
lsb, width = hmtx_metrics_for(gid)
|
|
272
|
+
bytes = [0, @scaler.scale(lsb), @scaler.scale(width)] # hsbw
|
|
274
273
|
|
|
275
274
|
# Add lines between points (simplified)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
275
|
+
points = glyph.points
|
|
276
|
+
points.each do |point|
|
|
277
|
+
next unless point[:on_curve]
|
|
279
278
|
|
|
280
|
-
|
|
281
|
-
end
|
|
279
|
+
# This is very simplified - proper implementation would handle curves
|
|
282
280
|
end
|
|
283
281
|
|
|
284
282
|
bytes << 14 # endchar
|
|
285
283
|
bytes.pack("C*")
|
|
286
284
|
end
|
|
287
285
|
|
|
286
|
+
# Look up LSB and advance width from hmtx for a glyph.
|
|
287
|
+
#
|
|
288
|
+
# @param gid [Integer] Glyph ID
|
|
289
|
+
# @return [Array(Integer, Integer)] [lsb, advance_width]
|
|
290
|
+
def hmtx_metrics_for(gid)
|
|
291
|
+
hmtx = @font.table(Constants::HMTX_TAG)
|
|
292
|
+
return [0, 500] unless hmtx
|
|
293
|
+
|
|
294
|
+
metric = hmtx.metric_for(gid)
|
|
295
|
+
return [0, 500] unless metric
|
|
296
|
+
|
|
297
|
+
[metric[:lsb] || 0, metric[:advance_width] || 500]
|
|
298
|
+
end
|
|
299
|
+
|
|
288
300
|
# Build second ASCII segment (trailer)
|
|
289
301
|
#
|
|
290
302
|
# @return [String] ASCII trailer
|
|
@@ -145,7 +145,7 @@ module Fontisan
|
|
|
145
145
|
return widths unless cmap
|
|
146
146
|
|
|
147
147
|
# Get Unicode mappings
|
|
148
|
-
mappings = if cmap
|
|
148
|
+
mappings = if cmap
|
|
149
149
|
cmap.unicode_mappings || {}
|
|
150
150
|
else
|
|
151
151
|
{}
|
|
@@ -369,18 +369,14 @@ module Fontisan
|
|
|
369
369
|
metrics << [0].pack("V")
|
|
370
370
|
|
|
371
371
|
# etmCapHeight (4 bytes)
|
|
372
|
-
cap_height =
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
os2.s_typo_ascender
|
|
376
|
-
else
|
|
377
|
-
@metrics.ascent || 1000
|
|
378
|
-
end
|
|
372
|
+
cap_height = os2.s_cap_height ||
|
|
373
|
+
os2.s_typo_ascender ||
|
|
374
|
+
@metrics.ascent || 1000
|
|
379
375
|
metrics << [@scaler.scale(cap_height)].pack("V")
|
|
380
376
|
|
|
381
377
|
# etmXHeight (4 bytes)
|
|
382
|
-
x_height = if os2.
|
|
383
|
-
os2.
|
|
378
|
+
x_height = if os2.sx_height&.positive?
|
|
379
|
+
os2.sx_height
|
|
384
380
|
else
|
|
385
381
|
# Fallback: use roughly half the ascent for x-height
|
|
386
382
|
(@metrics.ascent / 2) || 500
|
|
@@ -503,11 +499,7 @@ module Fontisan
|
|
|
503
499
|
name_table = @font.table(Constants::NAME_TAG)
|
|
504
500
|
return "" unless name_table
|
|
505
501
|
|
|
506
|
-
|
|
507
|
-
name_table.copyright(1) || name_table.copyright(3) || ""
|
|
508
|
-
else
|
|
509
|
-
""
|
|
510
|
-
end
|
|
502
|
+
name_table.english_name(Tables::Name::COPYRIGHT) || ""
|
|
511
503
|
end
|
|
512
504
|
|
|
513
505
|
# Extract face name from font
|
|
@@ -515,18 +507,13 @@ module Fontisan
|
|
|
515
507
|
# @return [String] Face name
|
|
516
508
|
def extract_face_name
|
|
517
509
|
name_table = @font.table(Constants::NAME_TAG)
|
|
518
|
-
return
|
|
510
|
+
return @font.post_script_name.to_s unless name_table
|
|
519
511
|
|
|
520
512
|
# Try full font name first, then font family, then postscript name
|
|
521
|
-
face_name =
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
elsif name_table.respond_to?(:postscript_name)
|
|
526
|
-
name_table.postscript_name(1) || name_table.postscript_name(3) || ""
|
|
527
|
-
else
|
|
528
|
-
@font.post_script_name || ""
|
|
529
|
-
end
|
|
513
|
+
face_name = name_table.english_name(Tables::Name::FULL_NAME) ||
|
|
514
|
+
name_table.english_name(Tables::Name::FAMILY) ||
|
|
515
|
+
name_table.english_name(Tables::Name::POSTSCRIPT_NAME) ||
|
|
516
|
+
@font.post_script_name
|
|
530
517
|
|
|
531
518
|
face_name.to_s
|
|
532
519
|
end
|
|
@@ -538,11 +525,7 @@ module Fontisan
|
|
|
538
525
|
os2 = @font.table(Constants::OS2_TAG)
|
|
539
526
|
return 400 unless os2
|
|
540
527
|
|
|
541
|
-
weight_class =
|
|
542
|
-
os2.us_weight_class
|
|
543
|
-
elsif os2.respond_to?(:weight_class)
|
|
544
|
-
os2.weight_class
|
|
545
|
-
end
|
|
528
|
+
weight_class = os2.us_weight_class
|
|
546
529
|
return 400 unless weight_class
|
|
547
530
|
|
|
548
531
|
# Map OS/2 weight class to PFM weight
|
|
@@ -564,7 +547,7 @@ module Fontisan
|
|
|
564
547
|
# @return [Integer] Pitch and family byte
|
|
565
548
|
def pitch_and_family_value
|
|
566
549
|
post = @font.table(Constants::POST_TAG)
|
|
567
|
-
is_fixed = post
|
|
550
|
+
is_fixed = post&.is_fixed_pitch || false
|
|
568
551
|
|
|
569
552
|
pitch = is_fixed ? FIXED_PITCH : VARIABLE_PITCH
|
|
570
553
|
|
|
@@ -97,23 +97,23 @@ module Fontisan
|
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
# Convert simple glyph
|
|
100
|
-
convert_simple_glyph(glyph)
|
|
100
|
+
convert_simple_glyph(glyph, gid)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Convert a simple glyph to CharString
|
|
104
104
|
#
|
|
105
|
-
# @param glyph [
|
|
105
|
+
# @param glyph [Tables::SimpleGlyph] TTF simple glyph
|
|
106
|
+
# @param gid [Integer] Glyph ID (for hmtx lookup)
|
|
106
107
|
# @return [String] Type 1 CharString data
|
|
107
|
-
def convert_simple_glyph(glyph)
|
|
108
|
+
def convert_simple_glyph(glyph, gid)
|
|
108
109
|
commands = []
|
|
109
110
|
points = extract_points(glyph)
|
|
110
111
|
|
|
111
112
|
return empty_charstring if points.empty?
|
|
112
113
|
|
|
113
114
|
# Start with hsbw (horizontal side bearing and width)
|
|
114
|
-
lsb =
|
|
115
|
-
|
|
116
|
-
commands << [HSBW, lsb, width]
|
|
115
|
+
lsb, width = hmtx_metrics_for(gid)
|
|
116
|
+
commands << [HSBW, @scaler.scale(lsb), @scaler.scale(width)]
|
|
117
117
|
|
|
118
118
|
# Convert contours to Type 1 commands
|
|
119
119
|
contour_commands = convert_contours(points)
|
|
@@ -126,23 +126,34 @@ module Fontisan
|
|
|
126
126
|
encode_charstring(commands)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
-
# Extract points from a simple glyph
|
|
129
|
+
# Extract points from a simple glyph, scaled to target UPM.
|
|
130
130
|
#
|
|
131
|
-
# @param glyph [
|
|
132
|
-
# @return [Array<Hash>] Array of
|
|
131
|
+
# @param glyph [Tables::SimpleGlyph] TTF simple glyph
|
|
132
|
+
# @return [Array<Hash>] Array of {x:, y:, on_curve:} hashes
|
|
133
133
|
def extract_points(glyph)
|
|
134
|
-
return [] unless glyph.
|
|
135
|
-
|
|
136
|
-
points
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
on_curve: point.on_curve?,
|
|
134
|
+
return [] unless glyph.is_a?(Tables::SimpleGlyph)
|
|
135
|
+
|
|
136
|
+
glyph.points.map do |point|
|
|
137
|
+
{
|
|
138
|
+
x: @scaler.scale(point[:x]),
|
|
139
|
+
y: @scaler.scale(point[:y]),
|
|
140
|
+
on_curve: point[:on_curve],
|
|
142
141
|
}
|
|
143
142
|
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Look up LSB and advance width from hmtx for a glyph.
|
|
146
|
+
#
|
|
147
|
+
# @param gid [Integer] Glyph ID
|
|
148
|
+
# @return [Array(Integer, Integer)] [lsb, advance_width]
|
|
149
|
+
def hmtx_metrics_for(gid)
|
|
150
|
+
hmtx = @font.table(Constants::HMTX_TAG)
|
|
151
|
+
return [0, 500] unless hmtx
|
|
152
|
+
|
|
153
|
+
metric = hmtx.metric_for(gid)
|
|
154
|
+
return [0, 500] unless metric
|
|
144
155
|
|
|
145
|
-
|
|
156
|
+
[metric[:lsb] || 0, metric[:advance_width] || 500]
|
|
146
157
|
end
|
|
147
158
|
|
|
148
159
|
# Convert contours to Type 1 commands
|
|
@@ -298,12 +309,7 @@ module Fontisan
|
|
|
298
309
|
dx = tx - transform_x(prev_x, prev_y, matrix)
|
|
299
310
|
dy = ty - transform_y(prev_x, prev_y, matrix)
|
|
300
311
|
|
|
301
|
-
|
|
302
|
-
if on_curve
|
|
303
|
-
commands << [RLINETO, dx.to_i, dy.to_i]
|
|
304
|
-
else
|
|
305
|
-
commands << [RLINETO, dx.to_i, dy.to_i]
|
|
306
|
-
end
|
|
312
|
+
commands << [RLINETO, dx.to_i, dy.to_i]
|
|
307
313
|
prev_x = pt[:x].to_f
|
|
308
314
|
prev_y = pt[:y].to_f
|
|
309
315
|
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Ufo
|
|
5
|
+
# Immutable axis-aligned bounding box over outline points.
|
|
6
|
+
#
|
|
7
|
+
# Bounds is a value object: equality and hash are based on the four
|
|
8
|
+
# coordinates, not identity. Bounds composes via #union so callers
|
|
9
|
+
# can fold bounds across many contours or many glyphs without
|
|
10
|
+
# mutating either side.
|
|
11
|
+
class Bounds
|
|
12
|
+
include Comparable
|
|
13
|
+
|
|
14
|
+
attr_reader :min_x, :min_y, :max_x, :max_y
|
|
15
|
+
|
|
16
|
+
def initialize(min_x:, min_y:, max_x:, max_y:)
|
|
17
|
+
@min_x = min_x.to_f
|
|
18
|
+
@min_y = min_y.to_f
|
|
19
|
+
@max_x = max_x.to_f
|
|
20
|
+
@max_y = max_y.to_f
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param contours [Array<Contour>, Enumerable<Contour>]
|
|
24
|
+
# @return [Bounds]
|
|
25
|
+
def self.measure(contours)
|
|
26
|
+
min_x = min_y = +Float::INFINITY
|
|
27
|
+
max_x = max_y = -Float::INFINITY
|
|
28
|
+
saw_point = false
|
|
29
|
+
|
|
30
|
+
contours.each do |contour|
|
|
31
|
+
contour.points.each do |pt|
|
|
32
|
+
saw_point = true
|
|
33
|
+
min_x = pt.x if pt.x < min_x
|
|
34
|
+
min_y = pt.y if pt.y < min_y
|
|
35
|
+
max_x = pt.x if pt.x > max_x
|
|
36
|
+
max_y = pt.y if pt.y > max_y
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return empty unless saw_point
|
|
41
|
+
|
|
42
|
+
new(min_x: min_x, min_y: min_y, max_x: max_x, max_y: max_y)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Bounds] zero-extent bounds anchored at the origin
|
|
46
|
+
def self.empty
|
|
47
|
+
new(min_x: 0, min_y: 0, max_x: 0, max_y: 0)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def width
|
|
51
|
+
max_x - min_x
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def height
|
|
55
|
+
max_y - min_y
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A bounds is empty when it has zero extent in both axes. Note that
|
|
59
|
+
# a contour containing a single point yields zero-extent bounds but
|
|
60
|
+
# is not "empty" in the geometric sense — callers that need to
|
|
61
|
+
# distinguish should test the contour directly.
|
|
62
|
+
def empty?
|
|
63
|
+
width.zero? && height.zero?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Smallest bounds containing both self and other.
|
|
67
|
+
#
|
|
68
|
+
# @param other [Bounds]
|
|
69
|
+
# @return [Bounds]
|
|
70
|
+
def union(other)
|
|
71
|
+
self.class.new(
|
|
72
|
+
min_x: [@min_x, other.min_x].min,
|
|
73
|
+
min_y: [@min_y, other.min_y].min,
|
|
74
|
+
max_x: [@max_x, other.max_x].max,
|
|
75
|
+
max_y: [@max_y, other.max_y].max,
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def <=>(other)
|
|
80
|
+
return nil unless other.is_a?(Bounds)
|
|
81
|
+
|
|
82
|
+
[@min_x, @min_y, @max_x, @max_y] <=>
|
|
83
|
+
[other.min_x, other.min_y, other.max_x, other.max_y]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def hash
|
|
87
|
+
[@min_x, @min_y, @max_x, @max_y].hash
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -20,8 +20,8 @@ module Fontisan
|
|
|
20
20
|
# @param instances [Array<Hash>] named instance definitions
|
|
21
21
|
# @return [String, nil] fvar table bytes, or nil if no axes
|
|
22
22
|
def self.build(font, axes: nil, instances: nil)
|
|
23
|
-
axes ||= font.info.
|
|
24
|
-
instances ||= font.info.
|
|
23
|
+
axes ||= font.info.axes
|
|
24
|
+
instances ||= font.info.named_instances
|
|
25
25
|
|
|
26
26
|
axes ||= []
|
|
27
27
|
return nil if axes.empty?
|
data/lib/fontisan/ufo/contour.rb
CHANGED
|
@@ -61,8 +61,7 @@ module Fontisan
|
|
|
61
61
|
name_table = font.table("name")
|
|
62
62
|
return unless name_table
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
records.each do |record|
|
|
64
|
+
name_table.name_records.each do |record|
|
|
66
65
|
next unless record.platform_id == 3 && record.encoding_id == 1 # Windows Unicode BMP
|
|
67
66
|
|
|
68
67
|
value = decode_name_value(record, name_table)
|
|
@@ -79,9 +78,7 @@ module Fontisan
|
|
|
79
78
|
end
|
|
80
79
|
|
|
81
80
|
def self.decode_name_value(record, name_table)
|
|
82
|
-
raw =
|
|
83
|
-
name_table.string_for_record(record)
|
|
84
|
-
end
|
|
81
|
+
raw = name_table.string_for_record(record)
|
|
85
82
|
|
|
86
83
|
if raw && raw.encoding == Encoding::UTF_16BE
|
|
87
84
|
raw.encode("UTF-8")
|
|
@@ -100,12 +97,7 @@ module Fontisan
|
|
|
100
97
|
post = font.table("post")
|
|
101
98
|
return unless post
|
|
102
99
|
|
|
103
|
-
|
|
104
|
-
info.italic_angle = post.italic_angle
|
|
105
|
-
elsif post.respond_to?(:italic_angle_raw)
|
|
106
|
-
raw = post.italic_angle_raw
|
|
107
|
-
info.italic_angle = raw.to_i / 65536.0
|
|
108
|
-
end
|
|
100
|
+
info.italic_angle = post.italic_angle
|
|
109
101
|
rescue NoMethodError
|
|
110
102
|
# post table may not have italic_angle
|
|
111
103
|
end
|
|
@@ -128,7 +120,7 @@ module Fontisan
|
|
|
128
120
|
cmap_table = font.table("cmap")
|
|
129
121
|
return {} unless cmap_table
|
|
130
122
|
|
|
131
|
-
mappings = cmap_table.
|
|
123
|
+
mappings = cmap_table.unicode_mappings || {}
|
|
132
124
|
# Invert: gid → [codepoints]
|
|
133
125
|
inverted = Hash.new { |h, k| h[k] = [] }
|
|
134
126
|
mappings.each { |cp, gid| inverted[gid] << cp }
|
|
@@ -146,13 +138,11 @@ module Fontisan
|
|
|
146
138
|
num_glyphs = maxp&.num_glyphs || 0
|
|
147
139
|
|
|
148
140
|
# Hmtx requires context-aware parsing before metric_for works.
|
|
149
|
-
|
|
150
|
-
hmtx.parse_with_context(num_h_metrics, num_glyphs)
|
|
151
|
-
end
|
|
141
|
+
hmtx.parse_with_context(num_h_metrics, num_glyphs)
|
|
152
142
|
|
|
153
143
|
widths = {}
|
|
154
144
|
num_glyphs.times do |gid|
|
|
155
|
-
metric = hmtx.
|
|
145
|
+
metric = hmtx.metric_for(gid)
|
|
156
146
|
widths[gid] = metric ? metric[:advance_width] : 0
|
|
157
147
|
end
|
|
158
148
|
widths
|
|
@@ -169,10 +159,8 @@ module Fontisan
|
|
|
169
159
|
return unless glyf && loca && head
|
|
170
160
|
|
|
171
161
|
# Tables need context-aware initialization before per-glyph access.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
num_glyphs)
|
|
175
|
-
end
|
|
162
|
+
loca.parse_with_context(head.index_to_loc_format,
|
|
163
|
+
num_glyphs)
|
|
176
164
|
|
|
177
165
|
num_glyphs.times do |gid|
|
|
178
166
|
glyph_name = glyph_name_for(font, gid) || "glyph#{gid}"
|
|
@@ -189,7 +177,7 @@ module Fontisan
|
|
|
189
177
|
|
|
190
178
|
if simple.is_a?(Fontisan::Tables::SimpleGlyph)
|
|
191
179
|
extract_simple_contours(simple, ufo_glyph)
|
|
192
|
-
elsif simple.
|
|
180
|
+
elsif simple.is_a?(Fontisan::Tables::CompoundGlyph) && simple.compound?
|
|
193
181
|
extract_compound_contours(simple, ufo_glyph, glyf, loca, head)
|
|
194
182
|
end
|
|
195
183
|
|
|
@@ -309,7 +297,7 @@ module Fontisan
|
|
|
309
297
|
visited = visited.dup.add(compound.glyph_id)
|
|
310
298
|
|
|
311
299
|
compound.components.each do |component|
|
|
312
|
-
next unless component.
|
|
300
|
+
next unless component.args_are_xy?
|
|
313
301
|
|
|
314
302
|
raw = begin
|
|
315
303
|
glyf.glyph_for(component.glyph_index, loca, head)
|
|
@@ -320,9 +308,9 @@ module Fontisan
|
|
|
320
308
|
|
|
321
309
|
matrix = component.transformation_matrix
|
|
322
310
|
|
|
323
|
-
if raw.
|
|
311
|
+
if raw.is_a?(Fontisan::Tables::SimpleGlyph) && raw.simple?
|
|
324
312
|
flatten_simple_component(raw, ufo_glyph, matrix)
|
|
325
|
-
elsif raw.
|
|
313
|
+
elsif raw.is_a?(Fontisan::Tables::CompoundGlyph) && raw.compound?
|
|
326
314
|
flatten_compound(raw, ufo_glyph, glyf, loca, head, visited, depth + 1)
|
|
327
315
|
end
|
|
328
316
|
end
|
|
@@ -358,12 +346,10 @@ module Fontisan
|
|
|
358
346
|
post = font.table("post")
|
|
359
347
|
return nil unless post
|
|
360
348
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return name unless name.nil? || name.empty?
|
|
364
|
-
end
|
|
349
|
+
name = post.glyph_name(gid)
|
|
350
|
+
return nil if name.nil? || name.empty?
|
|
365
351
|
|
|
366
|
-
|
|
352
|
+
name
|
|
367
353
|
rescue NoMethodError
|
|
368
354
|
nil
|
|
369
355
|
end
|
data/lib/fontisan/ufo/info.rb
CHANGED
|
@@ -26,6 +26,10 @@ module Fontisan
|
|
|
26
26
|
|
|
27
27
|
attr_accessor(*STANDARD_FIELDS)
|
|
28
28
|
|
|
29
|
+
# Variation font metadata. Populated by UFO 3 extensions or by
|
|
30
|
+
# explicit compiler configuration; nil when not set.
|
|
31
|
+
attr_accessor :axes, :named_instances
|
|
32
|
+
|
|
29
33
|
# Catch-all for non-standard (vendor-specific) fields.
|
|
30
34
|
attr_accessor :extras
|
|
31
35
|
|
data/lib/fontisan/ufo/point.rb
CHANGED
|
@@ -27,13 +27,6 @@ module Fontisan
|
|
|
27
27
|
def off_curve?
|
|
28
28
|
@type == "offcurve" || @type == "qcurve"
|
|
29
29
|
end
|
|
30
|
-
|
|
31
|
-
# @return [Hash] suitable for `to_glif` output
|
|
32
|
-
def to_h
|
|
33
|
-
h = { x: @x, y: @y, type: @type }
|
|
34
|
-
h[:smooth] = true if @smooth
|
|
35
|
-
h
|
|
36
|
-
end
|
|
37
30
|
end
|
|
38
31
|
end
|
|
39
32
|
end
|
data/lib/fontisan/ufo.rb
CHANGED
|
@@ -24,6 +24,7 @@ module Fontisan
|
|
|
24
24
|
autoload :LayerSet, "fontisan/ufo/layer_set"
|
|
25
25
|
autoload :Glyph, "fontisan/ufo/glyph"
|
|
26
26
|
autoload :Contour, "fontisan/ufo/contour"
|
|
27
|
+
autoload :Bounds, "fontisan/ufo/bounds"
|
|
27
28
|
autoload :Point, "fontisan/ufo/point"
|
|
28
29
|
autoload :Component, "fontisan/ufo/component"
|
|
29
30
|
autoload :Anchor, "fontisan/ufo/anchor"
|
|
@@ -129,13 +129,11 @@ module Fontisan
|
|
|
129
129
|
# @param data [String] Data to validate
|
|
130
130
|
# @raise [ArgumentError] If data is invalid
|
|
131
131
|
def validate_data!(data)
|
|
132
|
-
if data.nil?
|
|
133
|
-
raise ArgumentError, "Data cannot be nil"
|
|
134
|
-
end
|
|
132
|
+
raise ArgumentError, "Data cannot be nil" if data.nil?
|
|
135
133
|
|
|
136
|
-
unless data.
|
|
134
|
+
unless data.is_a?(String)
|
|
137
135
|
raise ArgumentError,
|
|
138
|
-
"Data must be a String
|
|
136
|
+
"Data must be a String, got #{data.class}"
|
|
139
137
|
end
|
|
140
138
|
end
|
|
141
139
|
end
|