fontisan 0.4.12 → 0.4.14
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/README.adoc +1 -1
- data/docs/WOFF_WOFF2_FORMATS.adoc +73 -0
- data/lib/fontisan/converters/woff2_encoder.rb +147 -242
- data/lib/fontisan/converters/woff_writer.rb +94 -59
- data/lib/fontisan/sfnt_font.rb +7 -2
- data/lib/fontisan/tables/head.rb +10 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/collection_encoder.rb +288 -0
- data/lib/fontisan/woff2/directory.rb +37 -40
- data/lib/fontisan/woff2/encoder_rules.rb +73 -0
- data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +400 -0
- data/lib/fontisan/woff2/glyf_loca_transform.rb +330 -0
- data/lib/fontisan/woff2/triplet_codec.rb +181 -0
- data/lib/fontisan/woff2.rb +5 -1
- data/lib/fontisan/woff2_font.rb +23 -40
- data/lib/fontisan/woff_font.rb +5 -2
- metadata +7 -3
- data/lib/fontisan/woff2/glyf_transformer.rb +0 -716
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Woff2
|
|
5
|
+
# Applies WOFF2 spec "encoder MUST" rules to a table collection before
|
|
6
|
+
# the table directory is built and brotli compression is applied.
|
|
7
|
+
#
|
|
8
|
+
# Each spec-mandated transform lives here so the encoder stays a thin
|
|
9
|
+
# orchestrator and new rules are MECE/discoverable instead of sprinkled
|
|
10
|
+
# as inline conditionals across Woff2Encoder.
|
|
11
|
+
#
|
|
12
|
+
# Reference: W3C WOFF2 spec, section 5 ("Recommended table directory").
|
|
13
|
+
class EncoderRules
|
|
14
|
+
# Tables that MUST NOT appear in WOFF2 output.
|
|
15
|
+
#
|
|
16
|
+
# Per spec section 5: "The compliant WOFF2 encoder MUST remove the
|
|
17
|
+
# DSIG table from an input font data, prior to applying
|
|
18
|
+
# transformations and entropy coding steps." Chrome's OpenType
|
|
19
|
+
# Sanitizer (OTS) rejects WOFF2 files that still contain DSIG.
|
|
20
|
+
EXCLUDED_TABLES = %w[DSIG].freeze
|
|
21
|
+
|
|
22
|
+
# Tables that MUST be transformed (when present) per WOFF2 spec
|
|
23
|
+
# section 5.3: glyf and loca are paired — both are either
|
|
24
|
+
# transformed (version 0) or null-transformed (version 3) together.
|
|
25
|
+
# Browsers (Chrome OTS) only accept the transformed form.
|
|
26
|
+
TRANSFORMED_TABLES = %w[glyf loca].freeze
|
|
27
|
+
|
|
28
|
+
# Apply all WOFF2 encoder rules to `table_data` in place.
|
|
29
|
+
#
|
|
30
|
+
# @param table_data [Hash{String => String}] Map of tag → binary
|
|
31
|
+
# table data. Mutated.
|
|
32
|
+
# @return [Hash{String => String}] The same hash, for chaining.
|
|
33
|
+
def self.apply!(table_data)
|
|
34
|
+
exclude_tables!(table_data)
|
|
35
|
+
mark_lossless_modifying!(table_data)
|
|
36
|
+
table_data
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Whether `tag` is dropped by WOFF2 spec rules.
|
|
40
|
+
def self.excluded?(tag)
|
|
41
|
+
EXCLUDED_TABLES.include?(tag)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Drop spec-excluded tables from the collection.
|
|
45
|
+
def self.exclude_tables!(table_data)
|
|
46
|
+
EXCLUDED_TABLES.each { |tag| table_data.delete(tag) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Set head.flags bit 11 to indicate the font was losslessly modified.
|
|
50
|
+
#
|
|
51
|
+
# Per spec section 5: "The WOFF 2.0 encoders MUST also set bit 11 of
|
|
52
|
+
# the 'flags' field of the head table ... to indicate that a recreated
|
|
53
|
+
# font file was subjected to lossless modifying transform."
|
|
54
|
+
#
|
|
55
|
+
# Also forces head.indexToLocFormat=0 when the glyf/loca tables are
|
|
56
|
+
# transformed, because Chrome's OTS only accepts WOFF2 glyf
|
|
57
|
+
# indexFormat=0 (see GlyfLocaTransform). For the reconstructed font
|
|
58
|
+
# to be self-consistent, head.indexToLocFormat must match the loca
|
|
59
|
+
# format that the decoder will produce (always short, format=0).
|
|
60
|
+
#
|
|
61
|
+
# Uses the model-driven Head BinData record so the bits are set via a
|
|
62
|
+
# named attribute on a typed object, not via raw byte slicing.
|
|
63
|
+
def self.mark_lossless_modifying!(table_data)
|
|
64
|
+
return unless table_data.key?("head")
|
|
65
|
+
|
|
66
|
+
head = Tables::Head.read(table_data["head"])
|
|
67
|
+
head.flags |= Tables::Head::FLAG_LOSSLESS_MODIFYING
|
|
68
|
+
head.index_to_loc_format = 0
|
|
69
|
+
table_data["head"] = head.to_binary_s
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Woff2
|
|
7
|
+
# Reconstructs glyf and loca tables from the WOFF2 transformed glyf
|
|
8
|
+
# stream per spec section 5.1. This is the decoder counterpart of
|
|
9
|
+
# {Woff2::GlyfLocaTransform}.
|
|
10
|
+
#
|
|
11
|
+
# The transformed glyf table is split into 7 streams preceded by a
|
|
12
|
+
# 36-byte header (uint16 version + uint16 optionFlags + uint16 numGlyphs
|
|
13
|
+
# + uint16 indexFormat + 7 × uint32 stream sizes). An optional
|
|
14
|
+
# overlapSimpleBitmap follows when optionFlags bit 0 is set.
|
|
15
|
+
#
|
|
16
|
+
# Reference: W3C WOFF2 spec, section 5.1.
|
|
17
|
+
class GlyfLocaReconstruct
|
|
18
|
+
# TrueType simple-glyph flag bits per OpenType spec.
|
|
19
|
+
FLAG_ON_CURVE = 0x01
|
|
20
|
+
FLAG_X_SHORT = 0x02
|
|
21
|
+
FLAG_Y_SHORT = 0x04
|
|
22
|
+
FLAG_REPEAT = 0x08
|
|
23
|
+
FLAG_X_SAME_OR_POS = 0x10
|
|
24
|
+
FLAG_Y_SAME_OR_POS = 0x20
|
|
25
|
+
FLAG_OVERLAP_SIMPLE = 0x40
|
|
26
|
+
|
|
27
|
+
# TrueType composite-glyph flag bits.
|
|
28
|
+
ARG_1_AND_2_ARE_WORDS = 0x0001
|
|
29
|
+
WE_HAVE_A_SCALE = 0x0008
|
|
30
|
+
MORE_COMPONENTS = 0x0020
|
|
31
|
+
WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
|
|
32
|
+
WE_HAVE_A_TWO_BY_TWO = 0x0080
|
|
33
|
+
WE_HAVE_INSTRUCTIONS = 0x0100
|
|
34
|
+
|
|
35
|
+
# 8-byte header + 7 × 4-byte stream sizes.
|
|
36
|
+
HEADER_SIZE = 36
|
|
37
|
+
|
|
38
|
+
attr_reader :num_glyphs, :index_format
|
|
39
|
+
|
|
40
|
+
# @param transformed_glyf [String] bytes of the transformed glyf table
|
|
41
|
+
# @param num_glyphs [Integer] from maxp
|
|
42
|
+
# @param index_format [Integer] 0 (short) or 1 (long), from head
|
|
43
|
+
def initialize(transformed_glyf:, num_glyphs:, index_format:)
|
|
44
|
+
@data = transformed_glyf
|
|
45
|
+
@num_glyphs = num_glyphs
|
|
46
|
+
@index_format = index_format
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Reconstruct glyf and loca tables.
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash{Symbol => String}] `{ glyf:, loca: }`
|
|
52
|
+
def reconstruct
|
|
53
|
+
header = parse_header
|
|
54
|
+
streams = read_streams(header)
|
|
55
|
+
|
|
56
|
+
glyf = String.new(encoding: Encoding::BINARY)
|
|
57
|
+
offsets = [0]
|
|
58
|
+
|
|
59
|
+
num_glyphs.times do |glyph_id|
|
|
60
|
+
glyph = decode_glyph(glyph_id, streams)
|
|
61
|
+
glyf << glyph
|
|
62
|
+
offsets << glyf.bytesize
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
{ glyf:, loca: build_loca(offsets) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Parsed header. Holds the 4 uint16 fields plus the 7 uint32 stream
|
|
71
|
+
# sizes. Stored as a Hash so the keys document themselves.
|
|
72
|
+
def parse_header
|
|
73
|
+
version, option_flags, ng, idx_fmt = @data[0, 8].unpack("S>S>S>S>")
|
|
74
|
+
sizes = @data[8, 28].unpack("L>7")
|
|
75
|
+
|
|
76
|
+
if ng != @num_glyphs
|
|
77
|
+
raise InvalidFontError,
|
|
78
|
+
"WOFF2 glyf numGlyphs mismatch: header says #{ng}, expected #{@num_glyphs}"
|
|
79
|
+
end
|
|
80
|
+
if idx_fmt != @index_format
|
|
81
|
+
raise InvalidFontError,
|
|
82
|
+
"WOFF2 glyf indexFormat mismatch: header says #{idx_fmt}, expected #{@index_format}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
version:,
|
|
87
|
+
option_flags:,
|
|
88
|
+
has_overlap_bitmap: (option_flags & 0x01).nonzero?,
|
|
89
|
+
stream_sizes: sizes,
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Slice the 7 streams out of the data block and set up per-stream
|
|
94
|
+
# StringIO cursors. The bboxStream is split into bboxBitmap (fixed
|
|
95
|
+
# size per numGlyphs) and bboxStream (variable, conditionally read
|
|
96
|
+
# per glyph). The optional overlapSimpleBitmap is read at the end.
|
|
97
|
+
def read_streams(header)
|
|
98
|
+
pos = HEADER_SIZE
|
|
99
|
+
bitmap_size = ((@num_glyphs + 31) >> 5) << 2
|
|
100
|
+
overlap_size = header[:has_overlap_bitmap] ? ((@num_glyphs + 7) >> 3) : 0
|
|
101
|
+
|
|
102
|
+
names = %i[n_contour n_points flags glyph composite bbox instructions]
|
|
103
|
+
streams = {}
|
|
104
|
+
names.each_with_index do |name, i|
|
|
105
|
+
size = header[:stream_sizes][i]
|
|
106
|
+
streams[name] = StringIO.new(@data[pos, size])
|
|
107
|
+
pos += size
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# bboxStream begins with bboxBitmap (4 × floor((numGlyphs+31)/32)).
|
|
111
|
+
bbox_blob = streams[:bbox].string
|
|
112
|
+
streams[:bbox_bitmap] = bbox_blob[0, bitmap_size] || ""
|
|
113
|
+
streams[:bbox_entries] = StringIO.new(bbox_blob[bitmap_size..] || "")
|
|
114
|
+
|
|
115
|
+
# overlapSimpleBitmap follows all streams when optionFlags bit 0 set.
|
|
116
|
+
streams[:overlap_bitmap] = header[:has_overlap_bitmap] ? @data[pos, overlap_size] : ""
|
|
117
|
+
|
|
118
|
+
# nContour is read positionally (int16 per glyph).
|
|
119
|
+
nc_data = streams[:n_contour].string
|
|
120
|
+
streams[:n_contour_values] = nc_data.unpack("s>*")
|
|
121
|
+
|
|
122
|
+
streams
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def decode_glyph(glyph_id, streams)
|
|
126
|
+
num_contours = streams[:n_contour_values][glyph_id]
|
|
127
|
+
|
|
128
|
+
case num_contours
|
|
129
|
+
when 0
|
|
130
|
+
# Empty glyph: just an entry in loca (loca[n] == loca[n-1]).
|
|
131
|
+
""
|
|
132
|
+
when -1
|
|
133
|
+
decode_composite(glyph_id, streams)
|
|
134
|
+
else
|
|
135
|
+
decode_simple(glyph_id, num_contours, streams)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Decode a simple glyph per spec section 5.1 "Decoding of Simple Glyphs".
|
|
140
|
+
def decode_simple(glyph_id, num_contours, streams)
|
|
141
|
+
# Step 1: read num_contours 255UInt16 values from nPointsStream to
|
|
142
|
+
# build endPtsOfContours[].
|
|
143
|
+
end_pts = []
|
|
144
|
+
cumulative = -1
|
|
145
|
+
num_contours.times do
|
|
146
|
+
pts_of_contour = read_255_uint16(streams[:n_points])
|
|
147
|
+
cumulative += pts_of_contour
|
|
148
|
+
end_pts << cumulative
|
|
149
|
+
end
|
|
150
|
+
total_points = end_pts.last + 1
|
|
151
|
+
|
|
152
|
+
# Step 2: read total_points flag bytes from flagStream (triplet flags).
|
|
153
|
+
triplet_flags = streams[:flags].read(total_points).bytes
|
|
154
|
+
|
|
155
|
+
# Step 3: read coordinate triplets from glyphStream (1-4 bytes per
|
|
156
|
+
# point depending on flag). Decode to (dx, dy) deltas.
|
|
157
|
+
xs = []
|
|
158
|
+
ys = []
|
|
159
|
+
on_curve_flags = []
|
|
160
|
+
x = 0
|
|
161
|
+
y = 0
|
|
162
|
+
triplet_flags.each do |flag|
|
|
163
|
+
dx, dy, oc = TripletCodec.decode(flag, read_triplet_payload(flag, streams[:glyph]))
|
|
164
|
+
x += dx
|
|
165
|
+
y += dy
|
|
166
|
+
xs << x
|
|
167
|
+
ys << y
|
|
168
|
+
on_curve_flags << (oc ? FLAG_ON_CURVE : 0)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Step 4-5: read instructionLength from glyphStream, bytes from instructionStream.
|
|
172
|
+
inst_len = read_255_uint16(streams[:glyph])
|
|
173
|
+
instructions = streams[:instructions].read(inst_len)
|
|
174
|
+
|
|
175
|
+
# Bbox: emit explicit bbox if bboxBitmap bit is set for this glyph,
|
|
176
|
+
# otherwise recalculate from coordinates.
|
|
177
|
+
bbox = if bbox_bit_set?(streams[:bbox_bitmap], glyph_id)
|
|
178
|
+
read_bbox(streams[:bbox_entries])
|
|
179
|
+
else
|
|
180
|
+
calc_bounds(xs, ys) || [0, 0, 0, 0]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Re-encode in standard TrueType format.
|
|
184
|
+
build_simple_glyph_bytes(num_contours:, bbox:, end_pts:,
|
|
185
|
+
on_curve_flags:, xs:, ys:, instructions:,
|
|
186
|
+
overlap: overlap_bit_set?(streams[:overlap_bitmap], glyph_id))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def decode_composite(_glyph_id, streams)
|
|
190
|
+
# Composite components are stored verbatim in compositeStream. Walk
|
|
191
|
+
# the component records to find their total byte length, then copy.
|
|
192
|
+
# Per spec section 5.1 "Decoding of Composite Glyphs": each component
|
|
193
|
+
# has a uint16 flags word, then 4-14 argument bytes.
|
|
194
|
+
composite_io = streams[:composite]
|
|
195
|
+
component_start = composite_io.pos
|
|
196
|
+
have_instructions = false
|
|
197
|
+
loop do
|
|
198
|
+
break if composite_io.eof?
|
|
199
|
+
|
|
200
|
+
flags = read_uint16_io(composite_io)
|
|
201
|
+
composite_io.read(2) # glyphIndex
|
|
202
|
+
composite_io.read((flags & ARG_1_AND_2_ARE_WORDS).nonzero? ? 4 : 2)
|
|
203
|
+
composite_io.read(composite_transform_size(flags))
|
|
204
|
+
have_instructions = (flags & WE_HAVE_INSTRUCTIONS).nonzero?
|
|
205
|
+
break if (flags & MORE_COMPONENTS).zero?
|
|
206
|
+
end
|
|
207
|
+
component_end = composite_io.pos
|
|
208
|
+
component_bytes = composite_io.string[component_start...component_end]
|
|
209
|
+
|
|
210
|
+
instructions = String.new(encoding: Encoding::BINARY)
|
|
211
|
+
if have_instructions
|
|
212
|
+
inst_len = read_255_uint16(streams[:glyph])
|
|
213
|
+
instructions = streams[:instructions].read(inst_len)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Composite glyphs always have explicit bbox in bboxStream.
|
|
217
|
+
bbox = read_bbox(streams[:bbox_entries])
|
|
218
|
+
|
|
219
|
+
# num_contours = -1 (int16), bbox 4 × int16, components, optional instructions
|
|
220
|
+
out = String.new(encoding: Encoding::BINARY)
|
|
221
|
+
out << [-1].pack("s>")
|
|
222
|
+
out << bbox.pack("s>*")
|
|
223
|
+
out << component_bytes
|
|
224
|
+
# If the last component had WE_HAVE_INSTRUCTIONS, the standard
|
|
225
|
+
# TrueType composite format expects a uint16 instructionLength
|
|
226
|
+
# followed by the instruction bytes.
|
|
227
|
+
if have_instructions
|
|
228
|
+
out << [instructions.bytesize].pack("n")
|
|
229
|
+
out << instructions
|
|
230
|
+
end
|
|
231
|
+
out
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def composite_transform_size(flags)
|
|
235
|
+
if (flags & WE_HAVE_A_SCALE).nonzero? then 2
|
|
236
|
+
elsif (flags & WE_HAVE_AN_X_AND_Y_SCALE).nonzero? then 4
|
|
237
|
+
elsif (flags & WE_HAVE_A_TWO_BY_TWO).nonzero? then 8
|
|
238
|
+
else
|
|
239
|
+
0
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Read 1-4 payload bytes for a triplet based on the flag's index bits.
|
|
244
|
+
def read_triplet_payload(flag, io)
|
|
245
|
+
idx = flag & 0x7F
|
|
246
|
+
n_bytes = if idx < 84 then 1
|
|
247
|
+
elsif idx < 120 then 2
|
|
248
|
+
elsif idx < 124 then 3
|
|
249
|
+
else 4
|
|
250
|
+
end
|
|
251
|
+
bytes = io.read(n_bytes)
|
|
252
|
+
bytes ? bytes.bytes : Array.new(n_bytes, 0)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def read_255_uint16(io)
|
|
256
|
+
return 0 if io.eof?
|
|
257
|
+
|
|
258
|
+
code = io.read(1)&.unpack1("C")
|
|
259
|
+
return 0 unless code
|
|
260
|
+
|
|
261
|
+
case code
|
|
262
|
+
when 0..252 then code
|
|
263
|
+
when 253
|
|
264
|
+
b = io.read(1)&.unpack1("C") || 0
|
|
265
|
+
253 + b
|
|
266
|
+
when 254
|
|
267
|
+
io.read(2)&.unpack1("n") || 0
|
|
268
|
+
|
|
269
|
+
when 255
|
|
270
|
+
v = io.read(2)&.unpack1("n") || 0
|
|
271
|
+
v + 506
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def read_uint16_io(io)
|
|
276
|
+
io.read(2)&.unpack1("n") || 0
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def bbox_bit_set?(bbox_bitmap, glyph_id)
|
|
280
|
+
return false if bbox_bitmap.nil? || bbox_bitmap.empty?
|
|
281
|
+
|
|
282
|
+
byte_idx = glyph_id >> 3
|
|
283
|
+
bit_idx = glyph_id & 7
|
|
284
|
+
(bbox_bitmap.getbyte(byte_idx) & (0x80 >> bit_idx)).nonzero?
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def overlap_bit_set?(overlap_bitmap, glyph_id)
|
|
288
|
+
return false unless overlap_bitmap && !overlap_bitmap.empty?
|
|
289
|
+
|
|
290
|
+
byte_idx = glyph_id >> 3
|
|
291
|
+
bit_idx = glyph_id & 7
|
|
292
|
+
(overlap_bitmap.getbyte(byte_idx) & (0x80 >> bit_idx)).nonzero?
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def read_bbox(io)
|
|
296
|
+
bytes = io.read(8)
|
|
297
|
+
return [0, 0, 0, 0] unless bytes&.bytesize == 8
|
|
298
|
+
|
|
299
|
+
bytes.unpack("s>*")
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def calc_bounds(xs, ys)
|
|
303
|
+
return nil if xs.empty?
|
|
304
|
+
|
|
305
|
+
[xs.min, ys.min, xs.max, ys.max]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Build a standard TrueType simple-glyph record from per-point arrays.
|
|
309
|
+
# Re-encodes coordinates using the standard TrueType flag scheme based
|
|
310
|
+
# on per-point delta magnitudes (per OFF section 5.3.3 spec rules).
|
|
311
|
+
def build_simple_glyph_bytes(num_contours:, bbox:, end_pts:, on_curve_flags:,
|
|
312
|
+
xs:, ys:, instructions:, overlap:)
|
|
313
|
+
out = String.new(encoding: Encoding::BINARY)
|
|
314
|
+
out << [num_contours].pack("s>")
|
|
315
|
+
out << bbox.pack("s>*")
|
|
316
|
+
out << end_pts.pack("n*")
|
|
317
|
+
out << [instructions.bytesize].pack("n")
|
|
318
|
+
out << instructions
|
|
319
|
+
|
|
320
|
+
prev_x = prev_y = 0
|
|
321
|
+
tt_flags = []
|
|
322
|
+
x_bytes = String.new(encoding: Encoding::BINARY)
|
|
323
|
+
y_bytes = String.new(encoding: Encoding::BINARY)
|
|
324
|
+
|
|
325
|
+
xs.each_with_index do |x, i|
|
|
326
|
+
y = ys[i]
|
|
327
|
+
dx = x - prev_x
|
|
328
|
+
dy = y - prev_y
|
|
329
|
+
prev_x = x
|
|
330
|
+
prev_y = y
|
|
331
|
+
|
|
332
|
+
base = on_curve_flags[i]
|
|
333
|
+
base |= FLAG_OVERLAP_SIMPLE if overlap && i.zero?
|
|
334
|
+
|
|
335
|
+
flag = encode_axis_bits(base, x_bytes, short_bit: FLAG_X_SHORT,
|
|
336
|
+
same_bit: FLAG_X_SAME_OR_POS,
|
|
337
|
+
value: dx)
|
|
338
|
+
flag = encode_axis_bits(flag, y_bytes, short_bit: FLAG_Y_SHORT,
|
|
339
|
+
same_bit: FLAG_Y_SAME_OR_POS,
|
|
340
|
+
value: dy)
|
|
341
|
+
tt_flags << flag
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
out << encode_flags_rle(tt_flags)
|
|
345
|
+
out << x_bytes
|
|
346
|
+
out << y_bytes
|
|
347
|
+
out
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Encode one axis delta into standard TrueType byte layout. Returns
|
|
351
|
+
# the updated flag value (X bits if short_bit is FLAG_X_SHORT, Y bits
|
|
352
|
+
# if short_bit is FLAG_Y_SHORT).
|
|
353
|
+
def encode_axis_bits(base, out, short_bit:, same_bit:, value:)
|
|
354
|
+
if value.zero?
|
|
355
|
+
base | same_bit
|
|
356
|
+
elsif value.abs <= 255
|
|
357
|
+
flag = base | short_bit
|
|
358
|
+
flag |= same_bit if value.positive?
|
|
359
|
+
out << (value.abs & 0xFF)
|
|
360
|
+
flag
|
|
361
|
+
else
|
|
362
|
+
out << [value & 0xFFFF].pack("n")
|
|
363
|
+
base
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Run-length encode the flags array per OFF spec: when two or more
|
|
368
|
+
# consecutive flags are identical, emit REPEAT_FLAG followed by count.
|
|
369
|
+
def encode_flags_rle(flags)
|
|
370
|
+
out = String.new(encoding: Encoding::BINARY)
|
|
371
|
+
i = 0
|
|
372
|
+
while i < flags.size
|
|
373
|
+
flag = flags[i]
|
|
374
|
+
count = 1
|
|
375
|
+
while i + count < flags.size && flags[i + count] == flag && count < 255
|
|
376
|
+
count += 1
|
|
377
|
+
end
|
|
378
|
+
if count > 1
|
|
379
|
+
out << (flag | FLAG_REPEAT).chr(Encoding::BINARY)
|
|
380
|
+
out << count.chr(Encoding::BINARY)
|
|
381
|
+
i += count
|
|
382
|
+
else
|
|
383
|
+
out << flag.chr(Encoding::BINARY)
|
|
384
|
+
i += 1
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
out
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Build loca table from accumulated glyph offsets per spec section 5.3.
|
|
391
|
+
def build_loca(offsets)
|
|
392
|
+
if @index_format.zero?
|
|
393
|
+
offsets.map { |o| o / 2 }.pack("n*")
|
|
394
|
+
else
|
|
395
|
+
offsets.pack("N*")
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|