fontisan 0.4.42 → 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/09-type1-cff-seac-expansion.md +22 -0
- data/TODO.bug-fixes/10-ttf-type1-composite-glyphs.md +23 -0
- data/TODO.bug-fixes/11-remaining-respond-to.md +25 -0
- 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 +29 -2
- 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 +9 -15
- 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/outline_extractor.rb +1 -5
- data/lib/fontisan/sfnt_font.rb +3 -3
- 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/font_face_generator.rb +0 -4
- data/lib/fontisan/svg/font_generator.rb +0 -4
- 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 +4 -4
- 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 +21 -23
- 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 +88 -26
- 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 +11 -10
- data/lib/fontisan/woff_font.rb +2 -1
- data/lib/fontisan.rb +1 -0
- metadata +27 -2
|
@@ -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
|
|
@@ -10,6 +10,9 @@ module Fontisan
|
|
|
10
10
|
# across nine call sites before extraction; this module is the
|
|
11
11
|
# single source of truth.
|
|
12
12
|
#
|
|
13
|
+
# All public methods take an Integer size. Callers with a String
|
|
14
|
+
# compute `.bytesize` first.
|
|
15
|
+
#
|
|
13
16
|
# @example Get padding length
|
|
14
17
|
# Padding.boundary(13) # => 3
|
|
15
18
|
# Padding.boundary(13, boundary: 4) # => 3
|
|
@@ -21,14 +24,13 @@ module Fontisan
|
|
|
21
24
|
module Padding
|
|
22
25
|
# Count of trailing pad bytes needed to align `size` to `boundary`.
|
|
23
26
|
#
|
|
24
|
-
# @param size [Integer
|
|
25
|
-
#
|
|
27
|
+
# @param size [Integer] Size in bytes (callers with a String
|
|
28
|
+
# compute `.bytesize` first).
|
|
26
29
|
# @param boundary [Integer] Alignment boundary in bytes (default
|
|
27
30
|
# `Constants::TABLE_ALIGNMENT`).
|
|
28
31
|
# @return [Integer] Number of pad bytes in 0...boundary-1
|
|
29
32
|
def self.boundary(size, boundary: Constants::TABLE_ALIGNMENT)
|
|
30
|
-
|
|
31
|
-
(boundary - (length % boundary)) % boundary
|
|
33
|
+
(boundary - (size % boundary)) % boundary
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
# Return `bytes` followed by null padding to the requested boundary.
|
|
@@ -38,7 +40,7 @@ module Fontisan
|
|
|
38
40
|
# @return [String] The original string if already aligned, else a
|
|
39
41
|
# new binary string with trailing nulls.
|
|
40
42
|
def self.pad(bytes, boundary: Constants::TABLE_ALIGNMENT)
|
|
41
|
-
pad_count = boundary(bytes, boundary:)
|
|
43
|
+
pad_count = boundary(bytes.bytesize, boundary:)
|
|
42
44
|
return bytes if pad_count.zero?
|
|
43
45
|
|
|
44
46
|
out = bytes.dup.force_encoding(Encoding::BINARY)
|
|
@@ -48,12 +50,11 @@ module Fontisan
|
|
|
48
50
|
|
|
49
51
|
# Aligned size of `size` after padding to `boundary`.
|
|
50
52
|
#
|
|
51
|
-
# @param size [Integer
|
|
53
|
+
# @param size [Integer] Original size in bytes.
|
|
52
54
|
# @param boundary [Integer] Alignment boundary in bytes.
|
|
53
55
|
# @return [Integer] Size after alignment (multiple of `boundary`).
|
|
54
56
|
def self.aligned_size(size, boundary: Constants::TABLE_ALIGNMENT)
|
|
55
|
-
|
|
56
|
-
length + boundary(length, boundary:)
|
|
57
|
+
size + boundary(size, boundary:)
|
|
57
58
|
end
|
|
58
59
|
end
|
|
59
60
|
end
|
|
@@ -246,7 +246,7 @@ module Fontisan
|
|
|
246
246
|
# @param font [Object] Font object
|
|
247
247
|
# @return [Boolean] true if TrueType
|
|
248
248
|
def truetype_font?(font)
|
|
249
|
-
return false unless font.
|
|
249
|
+
return false unless font.is_a?(SfntSource)
|
|
250
250
|
|
|
251
251
|
font.has_table?("glyf")
|
|
252
252
|
end
|
|
@@ -256,7 +256,7 @@ module Fontisan
|
|
|
256
256
|
# @param font [Object] Font object
|
|
257
257
|
# @return [Boolean] true if CFF
|
|
258
258
|
def cff_font?(font)
|
|
259
|
-
return false unless font.
|
|
259
|
+
return false unless font.is_a?(SfntSource)
|
|
260
260
|
|
|
261
261
|
font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
262
262
|
end
|
|
@@ -290,10 +290,10 @@ module Fontisan
|
|
|
290
290
|
}
|
|
291
291
|
end
|
|
292
292
|
|
|
293
|
-
# Get field value
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
# Get field value via the table's snapshot hash (BinData records
|
|
294
|
+
# expose all declared fields through #snapshot).
|
|
295
|
+
snapshot = table.is_a?(BinData::Record) ? table.snapshot : {}
|
|
296
|
+
value = snapshot[field_key.to_s] || snapshot[field_key.to_sym]
|
|
297
297
|
|
|
298
298
|
result = check_def[:block].call(table, value)
|
|
299
299
|
passed = result != false && !result.nil?
|
|
@@ -260,20 +260,13 @@ module Fontisan
|
|
|
260
260
|
# @param tag [String] Table tag
|
|
261
261
|
# @return [Object, nil] Table object or nil
|
|
262
262
|
def load_table(tag)
|
|
263
|
-
return nil unless @font.
|
|
263
|
+
return nil unless @font.is_a?(SfntSource)
|
|
264
264
|
|
|
265
265
|
data = @font.table_data(tag)
|
|
266
266
|
return nil if data.nil? || data.empty?
|
|
267
267
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
when "fvar" then Tables::Fvar
|
|
271
|
-
when "gvar" then Tables::Gvar
|
|
272
|
-
when "HVAR" then Tables::Hvar
|
|
273
|
-
when "VVAR" then Tables::Vvar
|
|
274
|
-
when "MVAR" then Tables::Mvar
|
|
275
|
-
else return nil
|
|
276
|
-
end
|
|
268
|
+
table_class = Tables::Registry.for(tag)
|
|
269
|
+
return nil unless table_class
|
|
277
270
|
|
|
278
271
|
table_class.read(data)
|
|
279
272
|
rescue StandardError => e
|
|
@@ -293,7 +286,7 @@ module Fontisan
|
|
|
293
286
|
end
|
|
294
287
|
|
|
295
288
|
# Try VVAR
|
|
296
|
-
if @vvar.
|
|
289
|
+
if @vvar.is_a?(Tables::Vvar) && @vvar.item_variation_store
|
|
297
290
|
return @vvar.item_variation_store.variation_region_list
|
|
298
291
|
end
|
|
299
292
|
|
|
@@ -221,17 +221,17 @@ module Fontisan
|
|
|
221
221
|
|
|
222
222
|
# Similar to HVAR but for vertical metrics
|
|
223
223
|
# VVAR has the same structure as HVAR
|
|
224
|
-
if
|
|
224
|
+
if (delta_set = @vvar.advance_height_delta_set(glyph_id))
|
|
225
225
|
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
226
226
|
result[:advance_height] = apply_rounding(accumulated)
|
|
227
227
|
end
|
|
228
228
|
|
|
229
|
-
if
|
|
229
|
+
if (delta_set = @vvar.tsb_delta_set(glyph_id))
|
|
230
230
|
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
231
231
|
result[:tsb] = apply_rounding(accumulated)
|
|
232
232
|
end
|
|
233
233
|
|
|
234
|
-
if
|
|
234
|
+
if (delta_set = @vvar.bsb_delta_set(glyph_id))
|
|
235
235
|
accumulated = calculate_accumulated_delta(delta_set, region_scalars)
|
|
236
236
|
result[:bsb] = apply_rounding(accumulated)
|
|
237
237
|
end
|
|
@@ -65,19 +65,22 @@ options = {})
|
|
|
65
65
|
File.binwrite(output_path, binary)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# Per-tag table update dispatch. Each entry maps a tag to
|
|
69
|
+
# [method_name, context_key] for looking up the right args.
|
|
70
|
+
TABLE_UPDATERS = {
|
|
71
|
+
"hmtx" => %i[update_hmtx_table varied_metrics],
|
|
72
|
+
"hhea" => %i[update_hhea_table font_metrics],
|
|
73
|
+
"OS/2" => %i[update_os2_table font_metrics],
|
|
74
|
+
"head" => %i[update_head_table options],
|
|
75
|
+
}.freeze
|
|
76
|
+
|
|
68
77
|
private
|
|
69
78
|
|
|
70
|
-
# Collect all tables for static font
|
|
71
|
-
#
|
|
72
|
-
# @param varied_metrics [Hash] Varied glyph metrics
|
|
73
|
-
# @param font_metrics [Hash] Varied font metrics
|
|
74
|
-
# @param options [Hash] Build options
|
|
75
|
-
# @return [Hash<String, String>] Map of table tag to binary data
|
|
76
79
|
def collect_tables(varied_metrics, font_metrics, options)
|
|
77
80
|
tables = {}
|
|
78
81
|
|
|
79
82
|
# Get all table tags from font
|
|
80
|
-
table_tags = @font.
|
|
83
|
+
table_tags = @font.is_a?(SfntSource) ? @font.table_names : []
|
|
81
84
|
|
|
82
85
|
table_tags.each do |tag|
|
|
83
86
|
# Skip variation tables
|
|
@@ -88,17 +91,14 @@ options = {})
|
|
|
88
91
|
next if original_data.nil? || original_data.empty?
|
|
89
92
|
|
|
90
93
|
# Update specific tables with varied data
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
when "head"
|
|
99
|
-
update_head_table(original_data, options)
|
|
94
|
+
updater = TABLE_UPDATERS[tag]
|
|
95
|
+
tables[tag] = if updater
|
|
96
|
+
method_name, ctx_key = updater
|
|
97
|
+
ctx = { varied_metrics: varied_metrics,
|
|
98
|
+
font_metrics: font_metrics,
|
|
99
|
+
options: options }
|
|
100
|
+
method(method_name).call(original_data, ctx[ctx_key])
|
|
100
101
|
else
|
|
101
|
-
# Copy unchanged
|
|
102
102
|
original_data
|
|
103
103
|
end
|
|
104
104
|
end
|
|
@@ -182,12 +182,8 @@ options = {})
|
|
|
182
182
|
data = @font.table_data(tag)
|
|
183
183
|
return nil if data.nil? || data.empty?
|
|
184
184
|
|
|
185
|
-
table_class =
|
|
186
|
-
|
|
187
|
-
when "maxp" then Tables::Maxp
|
|
188
|
-
when "head" then Tables::Head
|
|
189
|
-
else return nil
|
|
190
|
-
end
|
|
185
|
+
table_class = Tables::Registry.for(tag)
|
|
186
|
+
return nil unless table_class
|
|
191
187
|
|
|
192
188
|
table_class.read(data)
|
|
193
189
|
rescue StandardError
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Variation
|
|
7
|
+
# Wraps a variable font to substitute specific table tags with
|
|
8
|
+
# instance-generated equivalents while forwarding everything else
|
|
9
|
+
# to the original font. Used by VariableSvgGenerator to feed the
|
|
10
|
+
# SVG generator an instance-shaped font without rewriting it.
|
|
11
|
+
#
|
|
12
|
+
# Delegation uses SimpleDelegator so the wrapper is transparent:
|
|
13
|
+
# the original font's interface is preserved verbatim. Only
|
|
14
|
+
# #has_table? is overridden to also report instance-supplied tags.
|
|
15
|
+
#
|
|
16
|
+
# NOTE: #table currently delegates to the original font for any tag
|
|
17
|
+
# not in the instance map. The previous implementation always
|
|
18
|
+
# delegated regardless of @table_data contents, which SvgGenerator
|
|
19
|
+
# depended on (it expects parsed BinData records, not raw bytes).
|
|
20
|
+
# Until SvgGenerator can consume raw instance bytes, this wrapper
|
|
21
|
+
# preserves that behavior. The @table_data map is still exposed so
|
|
22
|
+
# downstream code that wants the raw bytes can read them.
|
|
23
|
+
class InstanceFontWrapper < SimpleDelegator
|
|
24
|
+
# @return [Hash<String, String>] instance-generated table bytes by tag
|
|
25
|
+
attr_reader :table_data
|
|
26
|
+
|
|
27
|
+
# @param original_font [TrueTypeFont, OpenTypeFont] the variable font
|
|
28
|
+
# @param instance_tables [Hash<String, String>] instance-generated bytes
|
|
29
|
+
def initialize(original_font, instance_tables)
|
|
30
|
+
super(original_font)
|
|
31
|
+
@table_data = instance_tables
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param tag [String] table tag
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def has_table?(tag)
|
|
37
|
+
@table_data.key?(tag) || __getobj__.has_table?(tag)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -251,24 +251,26 @@ module Fontisan
|
|
|
251
251
|
# Parse table data into table object
|
|
252
252
|
#
|
|
253
253
|
# @param tag [String] Table tag
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
public
|
|
255
|
+
|
|
256
|
+
# Per-tag table parser dispatch. Each entry maps tag → [class, init_mode].
|
|
257
|
+
# :parse means call obj.parse(data); :data= means call obj.data = data.
|
|
258
|
+
TABLE_PARSERS = {
|
|
259
|
+
"head" => [Tables::Head, :parse],
|
|
260
|
+
"maxp" => [Tables::Maxp, :parse],
|
|
261
|
+
"loca" => [Tables::Loca, :data_assign],
|
|
262
|
+
"glyf" => [Tables::Glyf, :data_assign],
|
|
263
|
+
"CFF " => [Tables::Cff, :parse],
|
|
264
|
+
"CFF2" => [Tables::Cff2, :parse],
|
|
265
|
+
}.freeze
|
|
266
|
+
|
|
256
267
|
def parse_table(tag, data)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
Tables::Maxp.new.tap { |t| t.parse(data) }
|
|
264
|
-
when "loca"
|
|
265
|
-
Tables::Loca.new.tap { |t| t.data = data }
|
|
266
|
-
when "glyf"
|
|
267
|
-
Tables::Glyf.new.tap { |t| t.data = data }
|
|
268
|
-
when "CFF "
|
|
269
|
-
Tables::Cff.new.tap { |t| t.parse(data) }
|
|
270
|
-
when "CFF2"
|
|
271
|
-
Tables::Cff2.new.tap { |t| t.parse(data) }
|
|
268
|
+
entry = TABLE_PARSERS[tag]
|
|
269
|
+
if entry
|
|
270
|
+
klass, init_mode = entry
|
|
271
|
+
obj = klass.new
|
|
272
|
+
init_mode == :parse ? obj.parse(data) : obj.data = data
|
|
273
|
+
obj
|
|
272
274
|
else
|
|
273
275
|
# For other tables, return a simple object that just holds data
|
|
274
276
|
Object.new.tap do |obj|
|
|
@@ -213,26 +213,23 @@ module Fontisan
|
|
|
213
213
|
end
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
#
|
|
219
|
-
|
|
216
|
+
public
|
|
217
|
+
|
|
218
|
+
# Maps MVAR metric tags to [table_tag, method] for base value lookup.
|
|
219
|
+
METRIC_SOURCES = {
|
|
220
|
+
"hasc" => ["hhea", :ascender],
|
|
221
|
+
"hdsc" => ["hhea", :descender],
|
|
222
|
+
"hlgp" => ["hhea", :line_gap],
|
|
223
|
+
"xhgt" => ["OS/2", :sx_height],
|
|
224
|
+
"cpht" => ["OS/2", :s_cap_height],
|
|
225
|
+
}.freeze
|
|
226
|
+
|
|
220
227
|
def get_base_metric_value(tag)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
when "hlgp"
|
|
227
|
-
variation_table("hhea")&.line_gap
|
|
228
|
-
when "xhgt"
|
|
229
|
-
os2 = variation_table("OS/2")
|
|
230
|
-
os2&.s_x_height if os2.respond_to?(:s_x_height)
|
|
231
|
-
when "cpht"
|
|
232
|
-
os2 = variation_table("OS/2")
|
|
233
|
-
os2&.s_cap_height if os2.respond_to?(:s_cap_height)
|
|
234
|
-
# Add more metrics as needed
|
|
235
|
-
end
|
|
228
|
+
source = METRIC_SOURCES[tag]
|
|
229
|
+
return nil unless source
|
|
230
|
+
|
|
231
|
+
table_tag, method = source
|
|
232
|
+
variation_table(table_tag)&.public_send(method)
|
|
236
233
|
end
|
|
237
234
|
|
|
238
235
|
# Update font metric in appropriate table
|
|
@@ -307,7 +304,7 @@ module Fontisan
|
|
|
307
304
|
return unless hhea
|
|
308
305
|
|
|
309
306
|
# Update the field if hhea supports it
|
|
310
|
-
hhea.number_of_h_metrics = count
|
|
307
|
+
hhea.number_of_h_metrics = count
|
|
311
308
|
end
|
|
312
309
|
end
|
|
313
310
|
end
|