fontisan 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +119 -308
- data/README.adoc +1525 -1323
- data/Rakefile +45 -47
- data/benchmark/variation_quick_bench.rb +4 -4
- data/docs/FONT_HINTING.adoc +562 -0
- data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
- data/lib/fontisan/cli.rb +92 -34
- data/lib/fontisan/collection/builder.rb +82 -0
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +21 -2
- data/lib/fontisan/commands/convert_command.rb +96 -165
- data/lib/fontisan/commands/info_command.rb +111 -5
- data/lib/fontisan/commands/instance_command.rb +77 -85
- data/lib/fontisan/commands/validate_command.rb +28 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +34 -24
- data/lib/fontisan/converters/format_converter.rb +154 -1
- data/lib/fontisan/converters/outline_converter.rb +101 -34
- data/lib/fontisan/converters/woff_writer.rb +9 -4
- data/lib/fontisan/font_loader.rb +14 -9
- data/lib/fontisan/font_writer.rb +9 -6
- data/lib/fontisan/formatters/text_formatter.rb +45 -1
- data/lib/fontisan/hints/hint_converter.rb +131 -2
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
- 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 +6 -4
- data/lib/fontisan/models/collection_brief_info.rb +31 -0
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +183 -12
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/open_type_font.rb +28 -10
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +159 -0
- data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +416 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +58 -3
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff.rb +2 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +10 -5
- data/lib/fontisan/tables/cvar.rb +2 -41
- 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/gvar.rb +2 -41
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_font.rb +27 -10
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/validation/table_validator.rb +1 -1
- data/lib/fontisan/validation/variable_font_validator.rb +218 -0
- data/lib/fontisan/variation/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +121 -13
- 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/instance_writer.rb +341 -0
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_preserver.rb +291 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +489 -468
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +54 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +37 -2
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Woff2
|
|
7
|
+
# Reconstructs glyf and loca tables from WOFF2 transformed format
|
|
8
|
+
#
|
|
9
|
+
# WOFF2 glyf table transformation splits glyph data into separate streams
|
|
10
|
+
# for better compression. This transformer reconstructs the standard
|
|
11
|
+
# `glyf` and `loca` table formats from the transformed data.
|
|
12
|
+
#
|
|
13
|
+
# Transformation format (Section 5 of WOFF2 spec):
|
|
14
|
+
# - Separate streams for nContour, nPoints, flags, x-coords, y-coords
|
|
15
|
+
# - Variable-length integer encoding (255UInt16)
|
|
16
|
+
# - Composite glyph components stored separately
|
|
17
|
+
#
|
|
18
|
+
# See: https://www.w3.org/TR/WOFF2/#glyf_table_format
|
|
19
|
+
#
|
|
20
|
+
# @example Reconstructing tables
|
|
21
|
+
# result = GlyfTransformer.reconstruct(transformed_data, num_glyphs)
|
|
22
|
+
# glyf_data = result[:glyf]
|
|
23
|
+
# loca_data = result[:loca]
|
|
24
|
+
class GlyfTransformer
|
|
25
|
+
# Glyph flags
|
|
26
|
+
ON_CURVE_POINT = 0x01
|
|
27
|
+
X_SHORT_VECTOR = 0x02
|
|
28
|
+
Y_SHORT_VECTOR = 0x04
|
|
29
|
+
REPEAT_FLAG = 0x08
|
|
30
|
+
X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR = 0x10
|
|
31
|
+
Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR = 0x20
|
|
32
|
+
|
|
33
|
+
# Composite glyph flags
|
|
34
|
+
ARG_1_AND_2_ARE_WORDS = 0x0001
|
|
35
|
+
ARGS_ARE_XY_VALUES = 0x0002
|
|
36
|
+
ROUND_XY_TO_GRID = 0x0004
|
|
37
|
+
WE_HAVE_A_SCALE = 0x0008
|
|
38
|
+
MORE_COMPONENTS = 0x0020
|
|
39
|
+
WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
|
|
40
|
+
WE_HAVE_A_TWO_BY_TWO = 0x0080
|
|
41
|
+
WE_HAVE_INSTRUCTIONS = 0x0100
|
|
42
|
+
USE_MY_METRICS = 0x0200
|
|
43
|
+
OVERLAP_COMPOUND = 0x0400
|
|
44
|
+
HAVE_VARIATIONS = 0x1000 # Variable font variation data follows
|
|
45
|
+
|
|
46
|
+
# Reconstruct glyf and loca tables from transformed data
|
|
47
|
+
#
|
|
48
|
+
# @param transformed_data [String] The transformed glyf table data
|
|
49
|
+
# @param num_glyphs [Integer] Number of glyphs from maxp table
|
|
50
|
+
# @param variable_font [Boolean] Whether this is a variable font with variation data
|
|
51
|
+
# @return [Hash] { glyf: String, loca: String }
|
|
52
|
+
# @raise [InvalidFontError] If data is corrupted or invalid
|
|
53
|
+
def self.reconstruct(transformed_data, num_glyphs, variable_font: false)
|
|
54
|
+
io = StringIO.new(transformed_data)
|
|
55
|
+
|
|
56
|
+
# Check minimum size for header
|
|
57
|
+
if io.size < 8
|
|
58
|
+
raise InvalidFontError,
|
|
59
|
+
"Transformed glyf data too small: #{io.size} bytes"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Read header
|
|
63
|
+
read_uint32(io)
|
|
64
|
+
num_glyphs_in_data = read_uint16(io)
|
|
65
|
+
index_format = read_uint16(io)
|
|
66
|
+
|
|
67
|
+
if num_glyphs_in_data != num_glyphs
|
|
68
|
+
raise InvalidFontError,
|
|
69
|
+
"Glyph count mismatch: expected #{num_glyphs}, got #{num_glyphs_in_data}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Read nContour stream
|
|
73
|
+
n_contour_data = read_stream_safely(io, "nContour",
|
|
74
|
+
variable_font: variable_font)
|
|
75
|
+
|
|
76
|
+
# Read nPoints stream
|
|
77
|
+
n_points_data = read_stream_safely(io, "nPoints",
|
|
78
|
+
variable_font: variable_font)
|
|
79
|
+
|
|
80
|
+
# Read flag stream
|
|
81
|
+
flag_data = read_stream_safely(io, "flag", variable_font: variable_font)
|
|
82
|
+
|
|
83
|
+
# Read glyph stream (coordinates, instructions, composite data)
|
|
84
|
+
glyph_data = read_stream_safely(io, "glyph",
|
|
85
|
+
variable_font: variable_font)
|
|
86
|
+
|
|
87
|
+
# Read composite stream
|
|
88
|
+
composite_data = read_stream_safely(io, "composite",
|
|
89
|
+
variable_font: variable_font)
|
|
90
|
+
|
|
91
|
+
# Read bbox stream
|
|
92
|
+
bbox_data = read_stream_safely(io, "bbox", variable_font: variable_font)
|
|
93
|
+
|
|
94
|
+
# Read instruction stream
|
|
95
|
+
instruction_data = read_stream_safely(io, "instruction",
|
|
96
|
+
variable_font: variable_font)
|
|
97
|
+
|
|
98
|
+
# Parse streams
|
|
99
|
+
n_contours = parse_n_contour_stream(StringIO.new(n_contour_data),
|
|
100
|
+
num_glyphs)
|
|
101
|
+
|
|
102
|
+
# Reconstruct glyphs
|
|
103
|
+
glyphs = reconstruct_glyphs(
|
|
104
|
+
n_contours,
|
|
105
|
+
StringIO.new(n_points_data),
|
|
106
|
+
StringIO.new(flag_data),
|
|
107
|
+
StringIO.new(glyph_data),
|
|
108
|
+
StringIO.new(composite_data),
|
|
109
|
+
StringIO.new(bbox_data),
|
|
110
|
+
StringIO.new(instruction_data),
|
|
111
|
+
variable_font: variable_font,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Build glyf and loca tables
|
|
115
|
+
build_tables(glyphs, index_format)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Safely read a stream with bounds checking
|
|
119
|
+
#
|
|
120
|
+
# @param io [StringIO] Input stream
|
|
121
|
+
# @param stream_name [String] Name of stream for error messages
|
|
122
|
+
# @param variable_font [Boolean] Whether this is a variable font (allows incomplete streams)
|
|
123
|
+
# @return [String] Stream data (empty if not available)
|
|
124
|
+
def self.read_stream_safely(io, _stream_name, variable_font: false)
|
|
125
|
+
remaining = io.size - io.pos
|
|
126
|
+
if remaining < 4
|
|
127
|
+
# Not enough data for stream size - return empty stream
|
|
128
|
+
return ""
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Read stream size safely
|
|
132
|
+
size_bytes = io.read(4)
|
|
133
|
+
return "" unless size_bytes && size_bytes.bytesize == 4
|
|
134
|
+
|
|
135
|
+
stream_size = size_bytes.unpack1("N")
|
|
136
|
+
remaining = io.size - io.pos
|
|
137
|
+
|
|
138
|
+
if remaining < stream_size
|
|
139
|
+
# Stream size extends beyond available data
|
|
140
|
+
# Read what we can
|
|
141
|
+
io.read(remaining) || ""
|
|
142
|
+
# For variable fonts, we may have incomplete streams - just return what we have
|
|
143
|
+
|
|
144
|
+
else
|
|
145
|
+
io.read(stream_size) || ""
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Read variable-length 255UInt16 integer
|
|
150
|
+
#
|
|
151
|
+
# Format from WOFF2 spec:
|
|
152
|
+
# - value < 253: one byte
|
|
153
|
+
# - value == 253: 253 + next uint16
|
|
154
|
+
# - value == 254: 253 * 2 + next uint16
|
|
155
|
+
# - value == 255: 253 * 3 + next uint16
|
|
156
|
+
#
|
|
157
|
+
# @param io [StringIO] Input stream
|
|
158
|
+
# @return [Integer] Decoded value, or 0 if not enough data
|
|
159
|
+
def self.read_255_uint16(io)
|
|
160
|
+
return 0 if io.eof? || (io.size - io.pos) < 1
|
|
161
|
+
|
|
162
|
+
code_byte = io.read(1)
|
|
163
|
+
return 0 unless code_byte && code_byte.bytesize == 1
|
|
164
|
+
|
|
165
|
+
code = code_byte.unpack1("C")
|
|
166
|
+
|
|
167
|
+
case code
|
|
168
|
+
when 255
|
|
169
|
+
return 0 if io.eof? || (io.size - io.pos) < 2
|
|
170
|
+
|
|
171
|
+
value_bytes = io.read(2)
|
|
172
|
+
return 0 unless value_bytes && value_bytes.bytesize == 2
|
|
173
|
+
|
|
174
|
+
759 + value_bytes.unpack1("n") # 253 * 3 + value
|
|
175
|
+
when 254
|
|
176
|
+
return 0 if io.eof? || (io.size - io.pos) < 2
|
|
177
|
+
|
|
178
|
+
value_bytes = io.read(2)
|
|
179
|
+
return 0 unless value_bytes && value_bytes.bytesize == 2
|
|
180
|
+
|
|
181
|
+
506 + value_bytes.unpack1("n") # 253 * 2 + value
|
|
182
|
+
when 253
|
|
183
|
+
return 0 if io.eof? || (io.size - io.pos) < 2
|
|
184
|
+
|
|
185
|
+
value_bytes = io.read(2)
|
|
186
|
+
return 0 unless value_bytes && value_bytes.bytesize == 2
|
|
187
|
+
|
|
188
|
+
253 + value_bytes.unpack1("n")
|
|
189
|
+
else
|
|
190
|
+
code
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Parse nContour stream
|
|
195
|
+
#
|
|
196
|
+
# @param io [StringIO] Input stream
|
|
197
|
+
# @param num_glyphs [Integer] Number of glyphs
|
|
198
|
+
# @return [Array<Integer>] Number of contours per glyph (-1 for composite)
|
|
199
|
+
def self.parse_n_contour_stream(io, num_glyphs)
|
|
200
|
+
n_contours = []
|
|
201
|
+
num_glyphs.times do
|
|
202
|
+
# For variable fonts, stream may be incomplete
|
|
203
|
+
break if io.eof? || (io.size - io.pos) < 2
|
|
204
|
+
|
|
205
|
+
value = read_int16(io)
|
|
206
|
+
n_contours << value
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Pad with zeros if we have fewer contours than glyphs
|
|
210
|
+
while n_contours.size < num_glyphs
|
|
211
|
+
n_contours << 0
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
n_contours
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Reconstruct all glyphs
|
|
218
|
+
#
|
|
219
|
+
# @param n_contours [Array<Integer>] Contour counts
|
|
220
|
+
# @param n_points_io [StringIO] Points stream
|
|
221
|
+
# @param flag_io [StringIO] Flag stream
|
|
222
|
+
# @param glyph_io [StringIO] Glyph data stream
|
|
223
|
+
# @param composite_io [StringIO] Composite glyph stream
|
|
224
|
+
# @param bbox_io [StringIO] Bounding box stream
|
|
225
|
+
# @param instruction_io [StringIO] Instruction stream
|
|
226
|
+
# @param variable_font [Boolean] Whether this is a variable font
|
|
227
|
+
# @return [Array<String>] Reconstructed glyph data
|
|
228
|
+
def self.reconstruct_glyphs(n_contours, n_points_io, flag_io, glyph_io,
|
|
229
|
+
composite_io, bbox_io, instruction_io, variable_font: false)
|
|
230
|
+
glyphs = []
|
|
231
|
+
|
|
232
|
+
n_contours.each do |num_contours|
|
|
233
|
+
if num_contours.zero?
|
|
234
|
+
# Empty glyph
|
|
235
|
+
glyphs << ""
|
|
236
|
+
elsif num_contours.positive?
|
|
237
|
+
# Simple glyph
|
|
238
|
+
glyphs << reconstruct_simple_glyph(
|
|
239
|
+
num_contours, n_points_io, flag_io,
|
|
240
|
+
glyph_io, bbox_io, instruction_io
|
|
241
|
+
)
|
|
242
|
+
elsif num_contours == -1
|
|
243
|
+
# Composite glyph
|
|
244
|
+
glyphs << reconstruct_composite_glyph(
|
|
245
|
+
composite_io, bbox_io, instruction_io, variable_font: variable_font
|
|
246
|
+
)
|
|
247
|
+
else
|
|
248
|
+
raise InvalidFontError, "Invalid nContours value: #{num_contours}"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
glyphs
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Reconstruct a simple glyph
|
|
256
|
+
#
|
|
257
|
+
# @param num_contours [Integer] Number of contours
|
|
258
|
+
# @param n_points_io [StringIO] Points stream
|
|
259
|
+
# @param flag_io [StringIO] Flag stream
|
|
260
|
+
# @param glyph_io [StringIO] Glyph data stream
|
|
261
|
+
# @param bbox_io [StringIO] Bounding box stream
|
|
262
|
+
# @param instruction_io [StringIO] Instruction stream
|
|
263
|
+
# @return [String] Glyph data in standard format
|
|
264
|
+
def self.reconstruct_simple_glyph(num_contours, n_points_io, flag_io,
|
|
265
|
+
glyph_io, bbox_io, instruction_io)
|
|
266
|
+
# Read end points of contours
|
|
267
|
+
end_pts_of_contours = []
|
|
268
|
+
num_contours.times do
|
|
269
|
+
if end_pts_of_contours.empty?
|
|
270
|
+
end_pts_of_contours << read_255_uint16(n_points_io)
|
|
271
|
+
else
|
|
272
|
+
delta = read_255_uint16(n_points_io)
|
|
273
|
+
end_pts_of_contours << end_pts_of_contours.last + delta + 1
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
total_points = end_pts_of_contours.last + 1
|
|
278
|
+
|
|
279
|
+
# Read flags
|
|
280
|
+
flags = read_flags(flag_io, total_points)
|
|
281
|
+
|
|
282
|
+
# Read coordinates
|
|
283
|
+
x_coordinates = read_coordinates(glyph_io, flags, X_SHORT_VECTOR,
|
|
284
|
+
X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR)
|
|
285
|
+
y_coordinates = read_coordinates(glyph_io, flags, Y_SHORT_VECTOR,
|
|
286
|
+
Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR)
|
|
287
|
+
|
|
288
|
+
# Read bounding box safely
|
|
289
|
+
bbox_remaining = bbox_io.size - bbox_io.pos
|
|
290
|
+
if bbox_remaining < 8
|
|
291
|
+
# Not enough data, use default bounding box
|
|
292
|
+
x_min = y_min = x_max = y_max = 0
|
|
293
|
+
else
|
|
294
|
+
bbox_bytes = bbox_io.read(8)
|
|
295
|
+
if bbox_bytes && bbox_bytes.bytesize == 8
|
|
296
|
+
x_min, y_min, x_max, y_max = bbox_bytes.unpack("n4")
|
|
297
|
+
# Convert to signed
|
|
298
|
+
x_min = x_min > 0x7FFF ? x_min - 0x10000 : x_min
|
|
299
|
+
y_min = y_min > 0x7FFF ? y_min - 0x10000 : y_min
|
|
300
|
+
x_max = x_max > 0x7FFF ? x_max - 0x10000 : x_max
|
|
301
|
+
y_max = y_max > 0x7FFF ? y_max - 0x10000 : y_max
|
|
302
|
+
else
|
|
303
|
+
x_min = y_min = x_max = y_max = 0
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Read instructions safely
|
|
308
|
+
instruction_length = 0
|
|
309
|
+
instructions = ""
|
|
310
|
+
|
|
311
|
+
inst_remaining = instruction_io.size - instruction_io.pos
|
|
312
|
+
if inst_remaining >= 2
|
|
313
|
+
inst_length_data = read_255_uint16(instruction_io)
|
|
314
|
+
if inst_length_data
|
|
315
|
+
instruction_length = inst_length_data
|
|
316
|
+
if instruction_length.positive?
|
|
317
|
+
inst_remaining = instruction_io.size - instruction_io.pos
|
|
318
|
+
instructions = if inst_remaining >= instruction_length
|
|
319
|
+
instruction_io.read(instruction_length) || ""
|
|
320
|
+
else
|
|
321
|
+
# Read what we can
|
|
322
|
+
instruction_io.read(inst_remaining) || ""
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Build glyph data in standard format
|
|
329
|
+
build_simple_glyph_data(num_contours, x_min, y_min, x_max, y_max,
|
|
330
|
+
end_pts_of_contours, instructions, flags,
|
|
331
|
+
x_coordinates, y_coordinates)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Reconstruct a composite glyph
|
|
335
|
+
#
|
|
336
|
+
# @param composite_io [StringIO] Composite stream
|
|
337
|
+
# @param bbox_io [StringIO] Bounding box stream
|
|
338
|
+
# @param instruction_io [StringIO] Instruction stream
|
|
339
|
+
# @param variable_font [Boolean] Whether this is a variable font
|
|
340
|
+
# @return [String] Glyph data in standard format
|
|
341
|
+
def self.reconstruct_composite_glyph(composite_io, bbox_io,
|
|
342
|
+
instruction_io, variable_font: false)
|
|
343
|
+
# Track available bytes to prevent EOF errors
|
|
344
|
+
composite_size = composite_io.size - composite_io.pos
|
|
345
|
+
|
|
346
|
+
# Validate minimum size (at least flags + glyph_index + args)
|
|
347
|
+
return "" if composite_size < 8
|
|
348
|
+
|
|
349
|
+
# Read bounding box safely
|
|
350
|
+
bbox_remaining = bbox_io.size - bbox_io.pos
|
|
351
|
+
if bbox_remaining < 8
|
|
352
|
+
# Not enough data for bounding box, return empty glyph
|
|
353
|
+
return ""
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
bbox_bytes = bbox_io.read(8)
|
|
357
|
+
unless bbox_bytes && bbox_bytes.bytesize == 8
|
|
358
|
+
return ""
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
x_min, y_min, x_max, y_max = bbox_bytes.unpack("n4")
|
|
362
|
+
# Convert to signed
|
|
363
|
+
x_min = x_min > 0x7FFF ? x_min - 0x10000 : x_min
|
|
364
|
+
y_min = y_min > 0x7FFF ? y_min - 0x10000 : y_min
|
|
365
|
+
x_max = x_max > 0x7FFF ? x_max - 0x10000 : x_max
|
|
366
|
+
y_max = y_max > 0x7FFF ? y_max - 0x10000 : y_max
|
|
367
|
+
|
|
368
|
+
# Read composite data
|
|
369
|
+
composite_data = +""
|
|
370
|
+
has_instructions = false
|
|
371
|
+
has_variations = false
|
|
372
|
+
|
|
373
|
+
loop do
|
|
374
|
+
# Check if we have enough bytes for flags and glyph_index
|
|
375
|
+
remaining = composite_io.size - composite_io.pos
|
|
376
|
+
break if composite_io.eof? || remaining < 4
|
|
377
|
+
|
|
378
|
+
# Read flags and glyph_index safely
|
|
379
|
+
component_header = composite_io.read(4)
|
|
380
|
+
break unless component_header && component_header.bytesize == 4
|
|
381
|
+
|
|
382
|
+
flags, glyph_index = component_header.unpack("n2")
|
|
383
|
+
|
|
384
|
+
# Write flags and index
|
|
385
|
+
composite_data << [flags].pack("n")
|
|
386
|
+
composite_data << [glyph_index].pack("n")
|
|
387
|
+
|
|
388
|
+
# Read arguments (depend on flags)
|
|
389
|
+
remaining = composite_io.size - composite_io.pos
|
|
390
|
+
if (flags & ARG_1_AND_2_ARE_WORDS).zero?
|
|
391
|
+
break if composite_io.eof? || remaining < 2
|
|
392
|
+
|
|
393
|
+
arg_bytes = composite_io.read(2)
|
|
394
|
+
break unless arg_bytes && arg_bytes.bytesize == 2
|
|
395
|
+
|
|
396
|
+
arg1, arg2 = arg_bytes.unpack("c2")
|
|
397
|
+
composite_data << [arg1, arg2].pack("c2")
|
|
398
|
+
else
|
|
399
|
+
break if composite_io.eof? || remaining < 4
|
|
400
|
+
|
|
401
|
+
arg_bytes = composite_io.read(4)
|
|
402
|
+
break unless arg_bytes && arg_bytes.bytesize == 4
|
|
403
|
+
|
|
404
|
+
arg1, arg2 = arg_bytes.unpack("n2")
|
|
405
|
+
# Convert to signed
|
|
406
|
+
arg1 = arg1 > 0x7FFF ? arg1 - 0x10000 : arg1
|
|
407
|
+
arg2 = arg2 > 0x7FFF ? arg2 - 0x10000 : arg2
|
|
408
|
+
composite_data << [arg1, arg2].pack("n2")
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Read transformation matrix (depends on flags) with bounds checking
|
|
412
|
+
if (flags & WE_HAVE_A_SCALE) != 0
|
|
413
|
+
remaining = composite_io.size - composite_io.pos
|
|
414
|
+
break if composite_io.eof? || remaining < 2
|
|
415
|
+
|
|
416
|
+
scale_bytes = composite_io.read(2)
|
|
417
|
+
break unless scale_bytes && scale_bytes.bytesize == 2
|
|
418
|
+
|
|
419
|
+
scale = scale_bytes.unpack1("n")
|
|
420
|
+
composite_data << [scale].pack("n")
|
|
421
|
+
elsif (flags & WE_HAVE_AN_X_AND_Y_SCALE) != 0
|
|
422
|
+
remaining = composite_io.size - composite_io.pos
|
|
423
|
+
break if composite_io.eof? || remaining < 4
|
|
424
|
+
|
|
425
|
+
scale_bytes = composite_io.read(4)
|
|
426
|
+
break unless scale_bytes && scale_bytes.bytesize == 4
|
|
427
|
+
|
|
428
|
+
x_scale, y_scale = scale_bytes.unpack("n2")
|
|
429
|
+
composite_data << [x_scale, y_scale].pack("n2")
|
|
430
|
+
elsif (flags & WE_HAVE_A_TWO_BY_TWO) != 0
|
|
431
|
+
remaining = composite_io.size - composite_io.pos
|
|
432
|
+
break if composite_io.eof? || remaining < 8
|
|
433
|
+
|
|
434
|
+
matrix_bytes = composite_io.read(8)
|
|
435
|
+
break unless matrix_bytes && matrix_bytes.bytesize == 8
|
|
436
|
+
|
|
437
|
+
x_scale, scale01, scale10, y_scale = matrix_bytes.unpack("n4")
|
|
438
|
+
composite_data << [x_scale, scale01, scale10, y_scale].pack("n4")
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Check for variable font variation data
|
|
442
|
+
# Only parse if this is a variable font and the flag is set
|
|
443
|
+
if variable_font && (flags & HAVE_VARIATIONS) != 0
|
|
444
|
+
has_variations = true
|
|
445
|
+
# Read tuple variation count and data
|
|
446
|
+
remaining = composite_io.size - composite_io.pos
|
|
447
|
+
if !composite_io.eof? && remaining >= 2
|
|
448
|
+
# Read tuple count safely
|
|
449
|
+
tuple_bytes = composite_io.read(2)
|
|
450
|
+
if tuple_bytes && tuple_bytes.bytesize == 2
|
|
451
|
+
tuple_count = tuple_bytes.unpack1("n")
|
|
452
|
+
composite_data << [tuple_count].pack("n")
|
|
453
|
+
|
|
454
|
+
# Each tuple has variation data - read and preserve it
|
|
455
|
+
tuple_count.times do
|
|
456
|
+
remaining = composite_io.size - composite_io.pos
|
|
457
|
+
break if composite_io.eof? || remaining < 4
|
|
458
|
+
|
|
459
|
+
# Read variation data (2 int16 values per tuple)
|
|
460
|
+
var_bytes = composite_io.read(4)
|
|
461
|
+
break unless var_bytes && var_bytes.bytesize == 4
|
|
462
|
+
|
|
463
|
+
var1, var2 = var_bytes.unpack("n2")
|
|
464
|
+
# Convert to signed if needed
|
|
465
|
+
var1 = var1 > 0x7FFF ? var1 - 0x10000 : var1
|
|
466
|
+
var2 = var2 > 0x7FFF ? var2 - 0x10000 : var2
|
|
467
|
+
composite_data << [var1, var2].pack("n2")
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
has_instructions = (flags & WE_HAVE_INSTRUCTIONS) != 0
|
|
474
|
+
|
|
475
|
+
break if (flags & MORE_COMPONENTS).zero?
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Add instructions if present
|
|
479
|
+
instructions = +""
|
|
480
|
+
if has_instructions
|
|
481
|
+
# Read instruction length safely
|
|
482
|
+
remaining = instruction_io.size - instruction_io.pos
|
|
483
|
+
if !instruction_io.eof? && remaining >= 2
|
|
484
|
+
length_bytes = instruction_io.read(2)
|
|
485
|
+
if length_bytes && length_bytes.bytesize == 2
|
|
486
|
+
instruction_length = length_bytes.unpack1("n")
|
|
487
|
+
if instruction_length.positive?
|
|
488
|
+
remaining = instruction_io.size - instruction_io.pos
|
|
489
|
+
instructions = if remaining >= instruction_length
|
|
490
|
+
instruction_io.read(instruction_length) || ""
|
|
491
|
+
else
|
|
492
|
+
# Read what we can
|
|
493
|
+
instruction_io.read(remaining) || ""
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Build composite glyph data
|
|
501
|
+
data = +""
|
|
502
|
+
data << [-1].pack("n") # numberOfContours = -1
|
|
503
|
+
data << [x_min, y_min, x_max, y_max].pack("n4")
|
|
504
|
+
data << composite_data
|
|
505
|
+
data << [instructions.bytesize].pack("n") if has_instructions
|
|
506
|
+
data << instructions if has_instructions
|
|
507
|
+
|
|
508
|
+
data
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Read flags with repeat handling
|
|
512
|
+
#
|
|
513
|
+
# @param io [StringIO] Flag stream
|
|
514
|
+
# @param count [Integer] Number of flags to read
|
|
515
|
+
# @return [Array<Integer>] Flag values
|
|
516
|
+
def self.read_flags(io, count)
|
|
517
|
+
flags = []
|
|
518
|
+
|
|
519
|
+
while flags.size < count
|
|
520
|
+
# EOF protection for variable fonts
|
|
521
|
+
break if io.eof? || (io.size - io.pos) < 1
|
|
522
|
+
|
|
523
|
+
flag = read_uint8(io)
|
|
524
|
+
flags << flag
|
|
525
|
+
|
|
526
|
+
if (flag & REPEAT_FLAG) != 0
|
|
527
|
+
break if io.eof? || (io.size - io.pos) < 1
|
|
528
|
+
|
|
529
|
+
repeat_count = read_uint8(io)
|
|
530
|
+
repeat_count.times { flags << flag }
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Pad with zero flags if needed
|
|
535
|
+
while flags.size < count
|
|
536
|
+
flags << 0
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
flags
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Read coordinates
|
|
543
|
+
#
|
|
544
|
+
# @param io [StringIO] Glyph stream
|
|
545
|
+
# @param flags [Array<Integer>] Flag values
|
|
546
|
+
# @param short_flag [Integer] Flag bit for short vector
|
|
547
|
+
# @param same_or_positive_flag [Integer] Flag bit for same/positive
|
|
548
|
+
# @return [Array<Integer>] Coordinate values
|
|
549
|
+
def self.read_coordinates(io, flags, short_flag, same_or_positive_flag)
|
|
550
|
+
coords = []
|
|
551
|
+
value = 0
|
|
552
|
+
|
|
553
|
+
flags.each do |flag|
|
|
554
|
+
# EOF protection
|
|
555
|
+
if (flag & short_flag) != 0
|
|
556
|
+
break if io.eof? || (io.size - io.pos) < 1
|
|
557
|
+
|
|
558
|
+
# Short vector (one byte)
|
|
559
|
+
delta = read_uint8(io)
|
|
560
|
+
delta = -delta if (flag & same_or_positive_flag).zero?
|
|
561
|
+
elsif (flag & same_or_positive_flag) != 0
|
|
562
|
+
# Same as previous (delta = 0)
|
|
563
|
+
delta = 0
|
|
564
|
+
else
|
|
565
|
+
break if io.eof? || (io.size - io.pos) < 2
|
|
566
|
+
|
|
567
|
+
# Long vector (two bytes, signed)
|
|
568
|
+
delta = read_int16(io)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
value += delta
|
|
572
|
+
coords << value
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Pad with last value if needed
|
|
576
|
+
last_val = coords.last || 0
|
|
577
|
+
while coords.size < flags.size
|
|
578
|
+
coords << last_val
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
coords
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# Build simple glyph data in standard format
|
|
585
|
+
#
|
|
586
|
+
# @return [String] Glyph data
|
|
587
|
+
def self.build_simple_glyph_data(num_contours, x_min, y_min, x_max, y_max,
|
|
588
|
+
end_pts, instructions, flags, x_coords, y_coords)
|
|
589
|
+
data = +""
|
|
590
|
+
data << [num_contours].pack("n")
|
|
591
|
+
data << [x_min, y_min, x_max, y_max].pack("n4")
|
|
592
|
+
|
|
593
|
+
end_pts.each { |pt| data << [pt].pack("n") }
|
|
594
|
+
|
|
595
|
+
data << [instructions.bytesize].pack("n")
|
|
596
|
+
data << instructions
|
|
597
|
+
|
|
598
|
+
flags.each { |flag| data << [flag].pack("C") }
|
|
599
|
+
|
|
600
|
+
# Write x-coordinates
|
|
601
|
+
prev_x = 0
|
|
602
|
+
x_coords.each do |x|
|
|
603
|
+
delta = x - prev_x
|
|
604
|
+
prev_x = x
|
|
605
|
+
|
|
606
|
+
data << if delta.abs <= 255
|
|
607
|
+
[delta.abs].pack("C")
|
|
608
|
+
else
|
|
609
|
+
[delta].pack("n")
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# Write y-coordinates
|
|
614
|
+
prev_y = 0
|
|
615
|
+
y_coords.each do |y|
|
|
616
|
+
delta = y - prev_y
|
|
617
|
+
prev_y = y
|
|
618
|
+
|
|
619
|
+
data << if delta.abs <= 255
|
|
620
|
+
[delta.abs].pack("C")
|
|
621
|
+
else
|
|
622
|
+
[delta].pack("n")
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
data
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Build glyf and loca tables
|
|
630
|
+
#
|
|
631
|
+
# @param glyphs [Array<String>] Glyph data
|
|
632
|
+
# @param index_format [Integer] Loca format (0 = short, 1 = long)
|
|
633
|
+
# @return [Hash] { glyf: String, loca: String }
|
|
634
|
+
def self.build_tables(glyphs, index_format)
|
|
635
|
+
glyf_data = +""
|
|
636
|
+
loca_offsets = [0]
|
|
637
|
+
|
|
638
|
+
glyphs.each do |glyph|
|
|
639
|
+
glyf_data << glyph
|
|
640
|
+
|
|
641
|
+
# Add padding to 4-byte boundary
|
|
642
|
+
padding = (4 - (glyph.bytesize % 4)) % 4
|
|
643
|
+
glyf_data << ("\x00" * padding)
|
|
644
|
+
|
|
645
|
+
loca_offsets << glyf_data.bytesize
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# Build loca table
|
|
649
|
+
loca_data = +""
|
|
650
|
+
if index_format.zero?
|
|
651
|
+
# Short format (divide offsets by 2)
|
|
652
|
+
loca_offsets.each do |offset|
|
|
653
|
+
loca_data << [offset / 2].pack("n")
|
|
654
|
+
end
|
|
655
|
+
else
|
|
656
|
+
# Long format
|
|
657
|
+
loca_offsets.each do |offset|
|
|
658
|
+
loca_data << [offset].pack("N")
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
{ glyf: glyf_data, loca: loca_data }
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# Helper methods for reading binary data
|
|
666
|
+
|
|
667
|
+
def self.read_uint8(io)
|
|
668
|
+
io.read(1)&.unpack1("C") || raise(EOFError, "Unexpected end of stream")
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def self.read_int8(io)
|
|
672
|
+
io.read(1)&.unpack1("c") || raise(EOFError, "Unexpected end of stream")
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def self.read_uint16(io)
|
|
676
|
+
io.read(2)&.unpack1("n") || raise(EOFError, "Unexpected end of stream")
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def self.read_int16(io)
|
|
680
|
+
value = read_uint16(io)
|
|
681
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def self.read_uint32(io)
|
|
685
|
+
io.read(4)&.unpack1("N") || raise(EOFError, "Unexpected end of stream")
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def self.read_f2dot14(io)
|
|
689
|
+
read_uint16(io)
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
end
|