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
|
@@ -5,44 +5,46 @@ require "json"
|
|
|
5
5
|
|
|
6
6
|
module Fontisan
|
|
7
7
|
module Export
|
|
8
|
-
#
|
|
8
|
+
# Serializes individual font tables for export.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
# - Fully parsed
|
|
12
|
-
#
|
|
13
|
-
# -
|
|
10
|
+
# Tables fall into three categories:
|
|
11
|
+
# - Fully parsed: BinData records whose `snapshot` hash is exported
|
|
12
|
+
# as JSON fields.
|
|
13
|
+
# - Binary-only: opaque blobs encoded as hex or base64.
|
|
14
|
+
# - Mixed: tables with a small typed summary alongside the binary
|
|
15
|
+
# payload (glyf, loca, cmap, CFF).
|
|
14
16
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# data = serializer.serialize(head_table, "head")
|
|
18
|
-
#
|
|
19
|
-
# @example Serializing binary table
|
|
20
|
-
# data = serializer.serialize_binary(raw_data, "DSIG")
|
|
17
|
+
# All table objects are BinData::Record subclasses; we trust that
|
|
18
|
+
# interface rather than duck-typing each field.
|
|
21
19
|
class TableSerializer
|
|
22
|
-
# Tables
|
|
20
|
+
# Tables whose BinData fields are exported as JSON.
|
|
23
21
|
FULLY_PARSED_TABLES = %w[
|
|
24
22
|
head hhea maxp post OS/2 name
|
|
25
23
|
fvar HVAR VVAR MVAR cvar gvar
|
|
26
24
|
].freeze
|
|
27
25
|
|
|
28
|
-
# Tables
|
|
26
|
+
# Tables stored as opaque binary with no field extraction.
|
|
29
27
|
BINARY_ONLY_TABLES = %w[
|
|
30
28
|
cvt fpgm prep gasp DSIG GDEF GPOS GSUB
|
|
31
29
|
].freeze
|
|
32
30
|
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
# Tables that ship a summary alongside the raw binary.
|
|
32
|
+
MIXED_SUMMARIES = {
|
|
33
|
+
"glyf" => GlyphSummary,
|
|
34
|
+
"loca" => LocaSummary,
|
|
35
|
+
"cmap" => CmapSummary,
|
|
36
|
+
"CFF" => CffSummary,
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# @param binary_format [Symbol] :hex or :base64
|
|
36
40
|
def initialize(binary_format: :hex)
|
|
37
41
|
@binary_format = binary_format
|
|
38
42
|
validate_binary_format!
|
|
39
43
|
end
|
|
40
44
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# @
|
|
44
|
-
# @param tag [String] The table tag
|
|
45
|
-
# @return [Hash] Serialized table data
|
|
45
|
+
# @param table [BinData::Record] the table object
|
|
46
|
+
# @param tag [String] table tag
|
|
47
|
+
# @return [Hash] serialized payload
|
|
46
48
|
def serialize(table, tag)
|
|
47
49
|
if fully_parsed?(tag)
|
|
48
50
|
serialize_parsed(table, tag)
|
|
@@ -53,185 +55,41 @@ module Fontisan
|
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
# @
|
|
59
|
-
# @param tag [String] The table tag
|
|
60
|
-
# @return [Hash] Serialized data with parsed flag
|
|
61
|
-
def serialize_parsed(table, tag)
|
|
62
|
-
fields = extract_fields(table)
|
|
63
|
-
{
|
|
64
|
-
tag: tag,
|
|
65
|
-
parsed: true,
|
|
66
|
-
fields: fields.to_json,
|
|
67
|
-
data: nil,
|
|
68
|
-
}
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Serialize a binary-only table
|
|
72
|
-
#
|
|
73
|
-
# @param data [String] Binary data
|
|
74
|
-
# @param tag [String] The table tag
|
|
75
|
-
# @return [Hash] Serialized data with binary content
|
|
58
|
+
# @param data [String] binary bytes
|
|
59
|
+
# @param tag [String] table tag
|
|
60
|
+
# @return [Hash]
|
|
76
61
|
def serialize_binary(data, tag)
|
|
77
|
-
|
|
78
|
-
{
|
|
79
|
-
tag: tag,
|
|
80
|
-
parsed: false,
|
|
81
|
-
data: encoded,
|
|
82
|
-
fields: nil,
|
|
83
|
-
}
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Serialize tables with mixed content (summary + binary)
|
|
87
|
-
#
|
|
88
|
-
# @param table [Object] The table object
|
|
89
|
-
# @param tag [String] The table tag
|
|
90
|
-
# @return [Hash] Serialized data with both fields and binary
|
|
91
|
-
def serialize_mixed(table, tag)
|
|
92
|
-
summary = create_summary(table, tag)
|
|
93
|
-
binary = table.respond_to?(:to_binary_s) ? table.to_binary_s : ""
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
tag: tag,
|
|
97
|
-
parsed: true,
|
|
98
|
-
fields: summary.to_json,
|
|
99
|
-
data: encode_binary(binary),
|
|
100
|
-
}
|
|
62
|
+
{ tag: tag, parsed: false, data: encode_binary(data), fields: nil }
|
|
101
63
|
end
|
|
102
64
|
|
|
103
65
|
private
|
|
104
66
|
|
|
105
|
-
# Check if table is fully parsed
|
|
106
|
-
#
|
|
107
|
-
# @param tag [String] Table tag
|
|
108
|
-
# @return [Boolean]
|
|
109
67
|
def fully_parsed?(tag)
|
|
110
68
|
FULLY_PARSED_TABLES.include?(tag)
|
|
111
69
|
end
|
|
112
70
|
|
|
113
|
-
# Check if table is binary-only
|
|
114
|
-
#
|
|
115
|
-
# @param tag [String] Table tag
|
|
116
|
-
# @return [Boolean]
|
|
117
71
|
def binary_only?(tag)
|
|
118
72
|
BINARY_ONLY_TABLES.include?(tag)
|
|
119
73
|
end
|
|
120
74
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
# @param table [Object] The table object
|
|
124
|
-
# @return [Hash] Field names and values
|
|
125
|
-
def extract_fields(table)
|
|
126
|
-
fields = {}
|
|
127
|
-
|
|
128
|
-
# Get all instance variables
|
|
129
|
-
table.instance_variables.each do |var|
|
|
130
|
-
name = var.to_s.delete("@")
|
|
131
|
-
value = table.instance_variable_get(var)
|
|
132
|
-
fields[name] = serialize_value(value)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
fields
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Serialize individual field value
|
|
139
|
-
#
|
|
140
|
-
# @param value [Object] The value to serialize
|
|
141
|
-
# @return [Object] Serialized value
|
|
142
|
-
def serialize_value(value)
|
|
143
|
-
case value
|
|
144
|
-
when Integer, Float, String, TrueClass, FalseClass, NilClass
|
|
145
|
-
value
|
|
146
|
-
when Array
|
|
147
|
-
value.map { |v| serialize_value(v) }
|
|
148
|
-
when Hash
|
|
149
|
-
value.transform_values { |v| serialize_value(v) }
|
|
150
|
-
when Time
|
|
151
|
-
value.iso8601
|
|
152
|
-
else
|
|
153
|
-
# For complex objects, try to extract fields
|
|
154
|
-
if value.respond_to?(:instance_variables)
|
|
155
|
-
extract_fields(value)
|
|
156
|
-
else
|
|
157
|
-
value.to_s
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Create summary for mixed-content tables
|
|
163
|
-
#
|
|
164
|
-
# @param table [Object] The table object
|
|
165
|
-
# @param tag [String] Table tag
|
|
166
|
-
# @return [Hash] Summary information
|
|
167
|
-
def create_summary(table, tag)
|
|
168
|
-
case tag
|
|
169
|
-
when "glyf"
|
|
170
|
-
create_glyf_summary(table)
|
|
171
|
-
when "loca"
|
|
172
|
-
create_loca_summary(table)
|
|
173
|
-
when "cmap"
|
|
174
|
-
create_cmap_summary(table)
|
|
175
|
-
when "CFF"
|
|
176
|
-
create_cff_summary(table)
|
|
177
|
-
else
|
|
178
|
-
{ type: "binary", size: table.to_binary_s.bytesize }
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Create glyf table summary
|
|
183
|
-
#
|
|
184
|
-
# @param table [Object] glyf table
|
|
185
|
-
# @return [Hash] Summary
|
|
186
|
-
def create_glyf_summary(table)
|
|
187
|
-
{
|
|
188
|
-
type: "glyf",
|
|
189
|
-
num_glyphs: table.respond_to?(:glyphs) ? table.glyphs.length : 0,
|
|
190
|
-
note: "Outline data stored as binary",
|
|
191
|
-
}
|
|
75
|
+
def serialize_parsed(table, tag)
|
|
76
|
+
{ tag: tag, parsed: true, fields: table.snapshot.to_json, data: nil }
|
|
192
77
|
end
|
|
193
78
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
{
|
|
200
|
-
type: "loca",
|
|
201
|
-
num_offsets: table.respond_to?(:offsets) ? table.offsets.length : 0,
|
|
202
|
-
format: table.respond_to?(:format) ? table.format : nil,
|
|
203
|
-
}
|
|
79
|
+
def serialize_mixed(table, tag)
|
|
80
|
+
summary = summary_for(table, tag)
|
|
81
|
+
{ tag: tag, parsed: true,
|
|
82
|
+
fields: summary.to_json,
|
|
83
|
+
data: encode_binary(table.to_binary_s) }
|
|
204
84
|
end
|
|
205
85
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
# @return [Hash] Summary
|
|
210
|
-
def create_cmap_summary(table)
|
|
211
|
-
{
|
|
212
|
-
type: "cmap",
|
|
213
|
-
version: table.respond_to?(:version) ? table.version : 0,
|
|
214
|
-
note: "Character mappings stored as binary",
|
|
215
|
-
}
|
|
216
|
-
end
|
|
86
|
+
def summary_for(table, tag)
|
|
87
|
+
builder = MIXED_SUMMARIES[tag]
|
|
88
|
+
return { type: "binary", size: table.to_binary_s.bytesize } unless builder
|
|
217
89
|
|
|
218
|
-
|
|
219
|
-
#
|
|
220
|
-
# @param table [Object] CFF table
|
|
221
|
-
# @return [Hash] Summary
|
|
222
|
-
def create_cff_summary(_table)
|
|
223
|
-
{
|
|
224
|
-
type: "CFF",
|
|
225
|
-
note: "CharString data stored as binary",
|
|
226
|
-
}
|
|
90
|
+
builder.call(table)
|
|
227
91
|
end
|
|
228
92
|
|
|
229
|
-
public
|
|
230
|
-
|
|
231
|
-
# Encode binary data based on format
|
|
232
|
-
#
|
|
233
|
-
# @param data [String] Binary data
|
|
234
|
-
# @return [String] Encoded data
|
|
235
93
|
def encode_binary(data)
|
|
236
94
|
case @binary_format
|
|
237
95
|
when :hex
|
|
@@ -241,16 +99,43 @@ module Fontisan
|
|
|
241
99
|
end
|
|
242
100
|
end
|
|
243
101
|
|
|
244
|
-
# Validate binary format option
|
|
245
|
-
#
|
|
246
|
-
# @raise [ArgumentError] if format is invalid
|
|
247
102
|
def validate_binary_format!
|
|
248
|
-
|
|
249
|
-
return if
|
|
103
|
+
valid = %i[hex base64]
|
|
104
|
+
return if valid.include?(@binary_format)
|
|
250
105
|
|
|
251
106
|
raise ArgumentError,
|
|
252
107
|
"Invalid binary format: #{@binary_format}. " \
|
|
253
|
-
"Must be one of: #{
|
|
108
|
+
"Must be one of: #{valid.join(', ')}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Summary builders for mixed-content tables. Each is a callable
|
|
112
|
+
# that receives the typed table and returns a JSON-able Hash.
|
|
113
|
+
|
|
114
|
+
module GlyphSummary
|
|
115
|
+
def self.call(table)
|
|
116
|
+
{ type: "glyf", num_glyphs: table.glyphs.length,
|
|
117
|
+
note: "Outline data stored as binary" }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
module LocaSummary
|
|
122
|
+
def self.call(table)
|
|
123
|
+
{ type: "loca", num_offsets: table.offsets.length,
|
|
124
|
+
format: table.format }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
module CmapSummary
|
|
129
|
+
def self.call(table)
|
|
130
|
+
{ type: "cmap", version: table.version,
|
|
131
|
+
note: "Character mappings stored as binary" }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
module CffSummary
|
|
136
|
+
def self.call(_table)
|
|
137
|
+
{ type: "CFF", note: "CharString data stored as binary" }
|
|
138
|
+
end
|
|
254
139
|
end
|
|
255
140
|
end
|
|
256
141
|
end
|
|
@@ -63,26 +63,26 @@ module Fontisan
|
|
|
63
63
|
|
|
64
64
|
# Transform individual table
|
|
65
65
|
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
#
|
|
66
|
+
public
|
|
67
|
+
|
|
68
|
+
# Per-tag TTX model transformer dispatch.
|
|
69
|
+
TABLE_TRANSFORMERS = {
|
|
70
|
+
"head" => [HeadTransformer, :head_table=],
|
|
71
|
+
"hhea" => [HheaTransformer, :hhea_table=],
|
|
72
|
+
"maxp" => [MaxpTransformer, :maxp_table=],
|
|
73
|
+
"name" => [NameTransformer, :name_table=],
|
|
74
|
+
"OS/2" => [Os2Transformer, :os2_table=],
|
|
75
|
+
"post" => [PostTransformer, :post_table=],
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
69
78
|
def transform_table(ttx, tag)
|
|
70
79
|
table = @font.table(tag)
|
|
71
80
|
return unless table
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
ttx.hhea_table = HheaTransformer.transform(table)
|
|
78
|
-
when "maxp"
|
|
79
|
-
ttx.maxp_table = MaxpTransformer.transform(table)
|
|
80
|
-
when "name"
|
|
81
|
-
ttx.name_table = NameTransformer.transform(table)
|
|
82
|
-
when "OS/2"
|
|
83
|
-
ttx.os2_table = Os2Transformer.transform(table)
|
|
84
|
-
when "post"
|
|
85
|
-
ttx.post_table = PostTransformer.transform(table)
|
|
82
|
+
entry = TABLE_TRANSFORMERS[tag]
|
|
83
|
+
if entry
|
|
84
|
+
transformer, setter = entry
|
|
85
|
+
ttx.public_send(setter, transformer.transform(table))
|
|
86
86
|
else
|
|
87
87
|
# Fallback to binary table
|
|
88
88
|
binary_table = transform_binary_table(tag, table)
|
|
@@ -103,7 +103,7 @@ module Fontisan
|
|
|
103
103
|
# @param table [Object] Table object
|
|
104
104
|
# @return [Models::Ttx::Tables::BinaryTable, nil] Binary table model
|
|
105
105
|
def transform_binary_table(tag, table)
|
|
106
|
-
binary_data = table
|
|
106
|
+
binary_data = table ? table.to_binary_s : ""
|
|
107
107
|
return nil if binary_data.empty?
|
|
108
108
|
|
|
109
109
|
Models::Ttx::Tables::BinaryTable.new.tap do |bin_table|
|
|
@@ -140,7 +140,7 @@ module Fontisan
|
|
|
140
140
|
# @return [String] Glyph name
|
|
141
141
|
def get_glyph_name(glyph_id)
|
|
142
142
|
post = @font.table("post")
|
|
143
|
-
if post.
|
|
143
|
+
if post.is_a?(Tables::Post) && post.glyph_names
|
|
144
144
|
post.glyph_names[glyph_id] || ".notdef"
|
|
145
145
|
elsif glyph_id.zero?
|
|
146
146
|
".notdef"
|
|
@@ -64,7 +64,7 @@ module Fontisan
|
|
|
64
64
|
# @param value [Object] BinData value or integer
|
|
65
65
|
# @return [Integer] Native integer
|
|
66
66
|
def to_int(value)
|
|
67
|
-
value
|
|
67
|
+
Integer(value)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# Generate GlyphOrder section (required first)
|
|
@@ -84,8 +84,28 @@ module Fontisan
|
|
|
84
84
|
# Generate individual table XML
|
|
85
85
|
#
|
|
86
86
|
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
public
|
|
88
|
+
|
|
89
|
+
# Per-tag emitter dispatch table. Adding a new table's TTX
|
|
90
|
+
# emitter means adding one entry here, not editing generate_table.
|
|
91
|
+
TABLE_EMITTERS = {
|
|
92
|
+
"head" => :generate_head_table,
|
|
93
|
+
"hhea" => :generate_hhea_table,
|
|
94
|
+
"maxp" => :generate_maxp_table,
|
|
95
|
+
"post" => :generate_post_table,
|
|
96
|
+
"name" => :generate_name_table,
|
|
97
|
+
"cmap" => :generate_cmap_table,
|
|
98
|
+
"loca" => :generate_loca_table,
|
|
99
|
+
"glyf" => :generate_glyf_table,
|
|
100
|
+
"CFF" => :generate_cff_table,
|
|
101
|
+
"CFF " => :generate_cff_table,
|
|
102
|
+
"hmtx" => :generate_hmtx_table,
|
|
103
|
+
"fvar" => :generate_fvar_table,
|
|
104
|
+
}.freeze
|
|
105
|
+
|
|
106
|
+
# Variation tags all share the same emitter shape.
|
|
107
|
+
VARIATION_TAGS = %w[gvar cvar HVAR VVAR MVAR].freeze
|
|
108
|
+
|
|
89
109
|
def generate_table(xml, tag)
|
|
90
110
|
table = @font.table(tag)
|
|
91
111
|
|
|
@@ -97,36 +117,15 @@ module Fontisan
|
|
|
97
117
|
return
|
|
98
118
|
end
|
|
99
119
|
|
|
100
|
-
|
|
101
|
-
when "head"
|
|
102
|
-
generate_head_table(xml, table)
|
|
103
|
-
when "hhea"
|
|
104
|
-
generate_hhea_table(xml, table)
|
|
105
|
-
when "maxp"
|
|
106
|
-
generate_maxp_table(xml, table)
|
|
107
|
-
when "post"
|
|
108
|
-
generate_post_table(xml, table)
|
|
109
|
-
when "name"
|
|
110
|
-
generate_name_table(xml, table)
|
|
111
|
-
when "OS/2"
|
|
112
|
-
# Skip OS/2 for now - Nokogiri builder can't handle slashes in element names
|
|
113
|
-
# TODO: Implement OS/2 table generation with proper XML escaping
|
|
120
|
+
if tag == "OS/2"
|
|
114
121
|
xml.comment(" OS/2 table skipped - requires special XML handling ")
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
generate_cff_table(xml, table)
|
|
123
|
-
when "CFF "
|
|
124
|
-
generate_cff_table(xml, table)
|
|
125
|
-
when "hmtx"
|
|
126
|
-
generate_hmtx_table(xml, table)
|
|
127
|
-
when "fvar"
|
|
128
|
-
generate_fvar_table(xml, table)
|
|
129
|
-
when "gvar", "cvar", "HVAR", "VVAR", "MVAR"
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
emitter = TABLE_EMITTERS[tag]
|
|
126
|
+
if emitter
|
|
127
|
+
method(emitter).call(xml, table)
|
|
128
|
+
elsif VARIATION_TAGS.include?(tag)
|
|
130
129
|
generate_variation_table(xml, tag, table)
|
|
131
130
|
else
|
|
132
131
|
generate_binary_table(xml, tag, table)
|
|
@@ -293,8 +292,8 @@ module Fontisan
|
|
|
293
292
|
# @param table [Object] Table object
|
|
294
293
|
# @return [void]
|
|
295
294
|
def generate_binary_table(xml, tag, table)
|
|
296
|
-
binary_data = table.
|
|
297
|
-
xml.
|
|
295
|
+
binary_data = table.to_binary_s
|
|
296
|
+
xml.public_send(tag.to_sym) do
|
|
298
297
|
xml.hexdata do
|
|
299
298
|
xml.text("\n #{format_hex_data(binary_data)}\n ")
|
|
300
299
|
end
|
|
@@ -310,7 +309,7 @@ module Fontisan
|
|
|
310
309
|
def generate_binary_table_from_data(xml, tag, data)
|
|
311
310
|
# Remove trailing space from tag for XML element name
|
|
312
311
|
clean_tag = tag.strip
|
|
313
|
-
xml.
|
|
312
|
+
xml.public_send(clean_tag.to_sym) do
|
|
314
313
|
xml.hexdata do
|
|
315
314
|
xml.text("\n #{format_hex_data(data)}\n ")
|
|
316
315
|
end
|
|
@@ -432,7 +431,7 @@ module Fontisan
|
|
|
432
431
|
# @return [String] Glyph name
|
|
433
432
|
def get_glyph_name(glyph_id)
|
|
434
433
|
post = @font.table("post")
|
|
435
|
-
if post.
|
|
434
|
+
if post.is_a?(Tables::Post) && post.glyph_names
|
|
436
435
|
post.glyph_names[glyph_id] || ".notdef"
|
|
437
436
|
elsif glyph_id.zero?
|
|
438
437
|
".notdef"
|
|
@@ -471,7 +470,7 @@ module Fontisan
|
|
|
471
470
|
# @param width [Integer] Minimum hex width
|
|
472
471
|
# @return [String] Hex string (e.g., "0x1234")
|
|
473
472
|
def format_hex(value, width: 8)
|
|
474
|
-
int_value = value
|
|
473
|
+
int_value = Integer(value)
|
|
475
474
|
"0x#{int_value.to_s(16).rjust(width, '0')}"
|
|
476
475
|
end
|
|
477
476
|
|
data/lib/fontisan/font_writer.rb
CHANGED
|
@@ -165,7 +165,7 @@ module Fontisan
|
|
|
165
165
|
checksum = calculate_table_checksum(data)
|
|
166
166
|
|
|
167
167
|
# Calculate padding to 4-byte boundary
|
|
168
|
-
padding_length = Utilities::Padding.boundary(data)
|
|
168
|
+
padding_length = Utilities::Padding.boundary(data.bytesize)
|
|
169
169
|
padding = "\0" * padding_length
|
|
170
170
|
|
|
171
171
|
entries << {
|
|
@@ -352,15 +352,13 @@ module Fontisan
|
|
|
352
352
|
next unless glyph&.compound? && glyph.compound?
|
|
353
353
|
|
|
354
354
|
# Add component glyph IDs
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
to_process << component_id
|
|
363
|
-
end
|
|
355
|
+
glyph&.components&.each do |component|
|
|
356
|
+
component_id = component[:glyph_index]
|
|
357
|
+
next unless glyph_exists?(component_id)
|
|
358
|
+
|
|
359
|
+
unless result.include?(component_id)
|
|
360
|
+
result.add(component_id)
|
|
361
|
+
to_process << component_id
|
|
364
362
|
end
|
|
365
363
|
end
|
|
366
364
|
end
|