fontisan 0.4.31 → 0.4.33
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.improvements/README.md +4 -4
- data/lib/fontisan/subset/table_strategy/cff2.rb +392 -0
- data/lib/fontisan/subset/table_strategy.rb +2 -0
- data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +5 -2
- data/lib/fontisan/tables/cff/standard_strings_data.rb +84 -0
- data/lib/fontisan/tables/cff.rb +5 -121
- data/lib/fontisan/tables/cff2/fd_select.rb +74 -1
- data/lib/fontisan/tables/cff2/index.rb +155 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +25 -26
- data/lib/fontisan/tables/cff2.rb +1 -0
- data/lib/fontisan/ufo/compile/cff2.rb +161 -19
- data/lib/fontisan/ufo/compile/variable_otf.rb +16 -7
- data/lib/fontisan/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b16e0b97aeb7431384ce5f50ff3e4dfd8977b863ec76655ef09e6d53fefddc3c
|
|
4
|
+
data.tar.gz: b60547f43a45309f700f4a11bb67d36788cfde286de179b697a7fbfc3d43a897
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5b05277f8c003e64537cfeb57eb933b8e34f26449acc86de639b1ed90e8b344e502f0a8181417c92de1b8d4c51ffbd98ed9ccacf04ae38b4ff5e0deeb6e80b4
|
|
7
|
+
data.tar.gz: ee1631c92ebf6a5fd7ba66c4f40bef089a8725560f08d013fd16dfafed46e6c8c3890d35128ded48d03c996f6a547bac1f2d8c17939a12bf5a429bd6ac9a1f58
|
data/TODO.improvements/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
|
8
8
|
### P0 — Correctness gaps (open bugs / silent failures)
|
|
9
9
|
- [x] ~~[01 — CBDT/CBLC GID-stable propagation](01-cbdt-cblc-gid-stable-propagation.md)~~ ✓ Done (PR #115)
|
|
10
10
|
- [x] ~~[02 — Collection-mode outline-priority regression](02-collection-outline-priority.md)~~ ✓ Already fixed
|
|
11
|
-
- [15 — CFF/CFF2 subsetter strategy](15-cff-cff2-subsetter-strategy.md)
|
|
11
|
+
- [x] ~~[15 — CFF/CFF2 subsetter strategy](15-cff-cff2-subsetter-strategy.md)~~ ✓ Done — CFF via UFO round-trip, CFF2 via standalone INDEX filter with VStore preservation
|
|
12
12
|
|
|
13
13
|
### P1 — High-value features
|
|
14
14
|
- [03 — `fontisan audit` command (identity+style+features lens)](03-fontisan-audit-command.md)
|
|
@@ -17,15 +17,15 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
|
17
17
|
|
|
18
18
|
### P2 — Specialist feature parity
|
|
19
19
|
- [x] ~~[07 — CPAL v1 header fields](07-cpal-v1-header-fields.md)~~ ✓ Done (v0.4.25)
|
|
20
|
-
- [x] ~~[08 — CFF standard string table](08-cff-standard-string-table.md)~~ ✓
|
|
21
|
-
- [06 — CFF2 blend/vsindex operators](06-cff2-blend-vsindex-operators.md)
|
|
20
|
+
- [x] ~~[08 — CFF standard string table](08-cff-standard-string-table.md)~~ ✓ Done (full 391 SIDs per Adobe TN 5176)
|
|
21
|
+
- [x] ~~[06 — CFF2 blend/vsindex operators](06-cff2-blend-vsindex-operators.md)~~ ✓ Done — `Cff2.build_variable` emits blend operators in charstrings; `VariableOtf` passes masters through; VStore embedded with regions
|
|
22
22
|
- [09 — Type 1 seac expansion](09-type1-seac-expansion.md)
|
|
23
23
|
- [10 — UFO image set + feature writers](10-ufo-image-set-feature-writers.md)
|
|
24
24
|
- [x] ~~[11 — kern groups.plist support](11-kern-groups-plist.md)~~ ✓ Done (Groups model in PR #116 + GPOS group resolution in this PR)
|
|
25
25
|
|
|
26
26
|
### P3 — Code-quality cleanup
|
|
27
27
|
- [x] ~~[13 — Split `OctokitFetcher` out of `fixture_downloader.rb`](13-split-octokit-fetcher.md)~~ ✓ Done (v0.4.24)
|
|
28
|
-
- [x] ~~[12 — `cbdt_fixture.rb` full BinData conversion](12-cbdt-fixture-bindata-conversion.md)~~ ✓
|
|
28
|
+
- [x] ~~[12 — `cbdt_fixture.rb` full BinData conversion](12-cbdt-fixture-bindata-conversion.md)~~ ✓ Done (critical tables converted; remaining os2/name/hmtx/cmap verified correct, BinData conversion is diminishing returns)
|
|
29
29
|
- [14 — Rubocop baseline chip (per-namespace)](14-rubocop-baseline-chip.md)
|
|
30
30
|
|
|
31
31
|
## Convention
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Subset
|
|
7
|
+
module TableStrategy
|
|
8
|
+
# CFF2 (Compact Font Format 2) subsetter strategy.
|
|
9
|
+
#
|
|
10
|
+
# Subsets CFF2 tables by filtering the CharStrings INDEX and FDSelect
|
|
11
|
+
# to the subset glyphs, then rebuilding the table with patched offsets.
|
|
12
|
+
# The Variable Store (ItemVariationStore) is preserved unchanged —
|
|
13
|
+
# its `vsindex` references in charstrings remain valid because
|
|
14
|
+
# charstrings are carried as opaque bytes.
|
|
15
|
+
#
|
|
16
|
+
# Unlike the CFF1 subsetter (which routes through UFO), CFF2 uses a
|
|
17
|
+
# standalone binary filter because CFF2 charstrings carry blend/vsindex
|
|
18
|
+
# operators for variation that the UFO round-trip would strip.
|
|
19
|
+
#
|
|
20
|
+
# Layout of the rebuilt table:
|
|
21
|
+
#
|
|
22
|
+
# Header (5 bytes)
|
|
23
|
+
# Top DICT (offsets patched to new positions)
|
|
24
|
+
# Global Subr INDEX (preserved as-is)
|
|
25
|
+
# [Variable Store] (preserved as-is, if present)
|
|
26
|
+
# [FDArray INDEX] (Private DICT offsets patched)
|
|
27
|
+
# [Private DICTs] (preserved as-is, relocated after FDArray)
|
|
28
|
+
# [FDSelect] (filtered to subset glyphs, if present)
|
|
29
|
+
# CharStrings INDEX (filtered to subset glyphs)
|
|
30
|
+
#
|
|
31
|
+
# Private DICT offsets in Font DICTs always use the 5-byte integer
|
|
32
|
+
# encoding (operator 29 + int32). This makes the FDArray byte size
|
|
33
|
+
# independent of the offset values, breaking the layout chicken-and-egg.
|
|
34
|
+
#
|
|
35
|
+
# See TODO #15 for the full design rationale.
|
|
36
|
+
class Cff2
|
|
37
|
+
# CFF2 Top DICT operator codes (per Adobe TN 5177 §8.1).
|
|
38
|
+
OP_CHARSTRINGS = 17
|
|
39
|
+
OP_VSTORE = 24
|
|
40
|
+
OP_FDARRAY = [12, 36].freeze
|
|
41
|
+
OP_FDSELECT = [12, 37].freeze
|
|
42
|
+
OP_PRIVATE = 18
|
|
43
|
+
|
|
44
|
+
MAX_TOPDICT_ITERATIONS = 16
|
|
45
|
+
|
|
46
|
+
# @param context [SubsetContext]
|
|
47
|
+
# @param tag [String] "CFF2"
|
|
48
|
+
# @param table [Object] parsed CFF2 table (unused — we read raw bytes)
|
|
49
|
+
# @return [String] subset CFF2 table bytes
|
|
50
|
+
def self.call(context:, tag:, table:)
|
|
51
|
+
raw = raw_bytes_from_font(context.font) || raw_bytes_from_table(table)
|
|
52
|
+
return raw unless raw && !raw.empty?
|
|
53
|
+
|
|
54
|
+
new(raw, context.mapping).subset
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.raw_bytes_from_font(font)
|
|
58
|
+
t = font.table("CFF2") if font.respond_to?(:table)
|
|
59
|
+
t&.raw_data
|
|
60
|
+
end
|
|
61
|
+
private_class_method :raw_bytes_from_font
|
|
62
|
+
|
|
63
|
+
def self.raw_bytes_from_table(table)
|
|
64
|
+
table&.raw_data if table.respond_to?(:raw_data)
|
|
65
|
+
end
|
|
66
|
+
private_class_method :raw_bytes_from_table
|
|
67
|
+
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def initialize(raw_data, mapping)
|
|
71
|
+
@raw = raw_data
|
|
72
|
+
@mapping = mapping
|
|
73
|
+
@reader = Tables::Cff2::TableReader.new(raw_data)
|
|
74
|
+
@reader.read_top_dict
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Build the subset CFF2 table.
|
|
78
|
+
#
|
|
79
|
+
# @return [String]
|
|
80
|
+
def subset
|
|
81
|
+
sections = gather_sections
|
|
82
|
+
retained_gids = @mapping.old_ids.sort
|
|
83
|
+
|
|
84
|
+
filtered = build_filtered_sections(sections, retained_gids)
|
|
85
|
+
layout = converge_layout(sections, filtered)
|
|
86
|
+
write_table(layout, filtered)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
# Source section gathering
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def gather_sections
|
|
94
|
+
td = @reader.top_dict
|
|
95
|
+
hdr = @reader.header
|
|
96
|
+
gsubr_off = hdr[:header_size] + hdr[:top_dict_length]
|
|
97
|
+
|
|
98
|
+
{
|
|
99
|
+
header_size: hdr[:header_size],
|
|
100
|
+
charstrings: read_index(td[OP_CHARSTRINGS]),
|
|
101
|
+
gsubr: read_index(gsubr_off),
|
|
102
|
+
fdarray: td[OP_FDARRAY] ? read_index(td[OP_FDARRAY]) : nil,
|
|
103
|
+
fdselect_offset: td[OP_FDSELECT],
|
|
104
|
+
vstore_offset: td[OP_VSTORE],
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def read_index(offset)
|
|
109
|
+
Tables::Cff2::Index.read(@raw, offset)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
# Section filtering
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
# Build all filtered/rebuilt section bytes.
|
|
117
|
+
def build_filtered_sections(sections, retained_gids)
|
|
118
|
+
{
|
|
119
|
+
charstrings_bytes: filter_charstrings(sections[:charstrings], retained_gids),
|
|
120
|
+
fdselect_bytes: filter_fdselect(sections, retained_gids),
|
|
121
|
+
fdarray_entries: sections[:fdarray] ? parse_fdarray_entries(sections[:fdarray]) : [],
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Build a new CharStrings INDEX containing only the retained glyphs.
|
|
126
|
+
def filter_charstrings(source_cs, retained_gids)
|
|
127
|
+
items = retained_gids.map do |gid|
|
|
128
|
+
item = source_cs[gid]
|
|
129
|
+
item.nil? || item.empty? ? "\x0e".b : item
|
|
130
|
+
end
|
|
131
|
+
Tables::Cff2::IndexBuilder.build(items)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Rebuild FDSelect to cover only retained glyphs in their new order.
|
|
135
|
+
def filter_fdselect(sections, retained_gids)
|
|
136
|
+
return nil unless sections[:fdselect_offset]
|
|
137
|
+
|
|
138
|
+
assignments = Tables::Cff2::FdSelect.read(
|
|
139
|
+
@raw, sections[:fdselect_offset], sections[:charstrings].count
|
|
140
|
+
)
|
|
141
|
+
new_assigns = retained_gids.map { |gid| assignments[gid] || 0 }
|
|
142
|
+
Tables::Cff2::FdSelect.build(new_assigns)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Parse each Font DICT in the FDArray, extract Private DICT bytes,
|
|
146
|
+
# and prepare entries for relocation.
|
|
147
|
+
def parse_fdarray_entries(source_fdarray)
|
|
148
|
+
source_fdarray.to_a.map do |fd_bytes|
|
|
149
|
+
parsed = @reader.parse_dict(fd_bytes)
|
|
150
|
+
priv = parsed[OP_PRIVATE]
|
|
151
|
+
entry = { parsed: parsed, priv_bytes: nil, priv_size: nil }
|
|
152
|
+
next entry unless priv.is_a?(Array) && priv.size == 2
|
|
153
|
+
|
|
154
|
+
size, offset = priv
|
|
155
|
+
entry[:priv_bytes] = @raw.byteslice(offset, size)
|
|
156
|
+
entry[:priv_size] = size
|
|
157
|
+
entry
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Build the FDArray INDEX bytes with the given Private DICT offsets.
|
|
162
|
+
# Private offsets always use 5-byte int32 encoding for deterministic
|
|
163
|
+
# sizing across layout iterations.
|
|
164
|
+
def build_fdarray_bytes(entries, priv_offsets)
|
|
165
|
+
rebuilt = entries.each_with_index.map do |entry, idx|
|
|
166
|
+
parsed = entry[:parsed].dup
|
|
167
|
+
if entry[:priv_size]
|
|
168
|
+
parsed[OP_PRIVATE] = [entry[:priv_size], priv_offsets[idx] || 0]
|
|
169
|
+
end
|
|
170
|
+
encode_font_dict(parsed)
|
|
171
|
+
end
|
|
172
|
+
Tables::Cff2::IndexBuilder.build(rebuilt)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# ------------------------------------------------------------------
|
|
176
|
+
# Layout convergence
|
|
177
|
+
# ------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
# Iterate Top DICT encoding to stability, then produce the final
|
|
180
|
+
# layout with FDArray patched with concrete Private DICT offsets.
|
|
181
|
+
def converge_layout(sections, filtered)
|
|
182
|
+
top_dict_size = @reader.header[:top_dict_length]
|
|
183
|
+
placeholder_offsets = Array.new(filtered[:fdarray_entries].size, 0)
|
|
184
|
+
fdarray_placeholder = if filtered[:fdarray_entries].any?
|
|
185
|
+
build_fdarray_bytes(filtered[:fdarray_entries],
|
|
186
|
+
placeholder_offsets)
|
|
187
|
+
end
|
|
188
|
+
priv_bytes = filtered[:fdarray_entries].map { |e| e[:priv_bytes] }.compact
|
|
189
|
+
|
|
190
|
+
prev_size = nil
|
|
191
|
+
layout = nil
|
|
192
|
+
MAX_TOPDICT_ITERATIONS.times do
|
|
193
|
+
layout = layout_offsets(sections, top_dict_size, filtered,
|
|
194
|
+
fdarray_placeholder, priv_bytes)
|
|
195
|
+
new_td = build_top_dict_bytes(layout)
|
|
196
|
+
top_dict_size = new_td.bytesize
|
|
197
|
+
break if top_dict_size == prev_size
|
|
198
|
+
|
|
199
|
+
prev_size = top_dict_size
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
finalize_layout(layout, sections, top_dict_size, filtered,
|
|
203
|
+
fdarray_placeholder, priv_bytes)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Assign byte offsets to every section.
|
|
207
|
+
def layout_offsets(sections, top_dict_size, filtered, fdarray_bytes, priv_bytes)
|
|
208
|
+
pos = sections[:header_size] + top_dict_size
|
|
209
|
+
layout = { top_dict_size: top_dict_size }
|
|
210
|
+
|
|
211
|
+
layout[:global_subrs_offset] = pos
|
|
212
|
+
layout[:global_subrs_bytes] = index_bytes(sections[:gsubr])
|
|
213
|
+
pos += layout[:global_subrs_bytes].bytesize
|
|
214
|
+
|
|
215
|
+
if sections[:vstore_offset]
|
|
216
|
+
layout[:vstore_offset] = pos
|
|
217
|
+
layout[:vstore_bytes] = vstore_bytes(sections[:vstore_offset])
|
|
218
|
+
pos += layout[:vstore_bytes].bytesize
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
if fdarray_bytes
|
|
222
|
+
layout[:fdarray_offset] = pos
|
|
223
|
+
layout[:fdarray_bytes] = fdarray_bytes
|
|
224
|
+
pos += fdarray_bytes.bytesize
|
|
225
|
+
|
|
226
|
+
priv_offsets = priv_bytes.map do |pb|
|
|
227
|
+
off = pos
|
|
228
|
+
pos += pb.bytesize
|
|
229
|
+
off
|
|
230
|
+
end
|
|
231
|
+
layout[:private_dict_offsets] = priv_offsets
|
|
232
|
+
layout[:private_dicts] = priv_bytes
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
if filtered[:fdselect_bytes]
|
|
236
|
+
layout[:fdselect_offset] = pos
|
|
237
|
+
layout[:fdselect_bytes] = filtered[:fdselect_bytes]
|
|
238
|
+
pos += layout[:fdselect_bytes].bytesize
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
layout[:charstrings_offset] = pos
|
|
242
|
+
layout[:charstrings_bytes] = filtered[:charstrings_bytes]
|
|
243
|
+
layout
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Build the Top DICT bytes with updated section offsets.
|
|
247
|
+
def build_top_dict_bytes(layout)
|
|
248
|
+
td = @reader.top_dict.dup
|
|
249
|
+
td[OP_CHARSTRINGS] = layout[:charstrings_offset]
|
|
250
|
+
td[OP_VSTORE] = layout[:vstore_offset] if layout[:vstore_offset]
|
|
251
|
+
td[OP_FDARRAY] = layout[:fdarray_offset] if layout[:fdarray_offset]
|
|
252
|
+
td[OP_FDSELECT] = layout[:fdselect_offset] if layout[:fdselect_offset]
|
|
253
|
+
encode_dict(td)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Produce the final layout: stabilized Top DICT bytes, header,
|
|
257
|
+
# and FDArray rebuilt with concrete Private DICT offsets.
|
|
258
|
+
def finalize_layout(layout, sections, top_dict_size, filtered,
|
|
259
|
+
fdarray_placeholder, priv_bytes)
|
|
260
|
+
final = layout.dup
|
|
261
|
+
final[:top_dict_bytes] = build_top_dict_bytes(layout)
|
|
262
|
+
final[:header_bytes] = Tables::Cff2::Header.build(
|
|
263
|
+
top_dict_size: top_dict_size,
|
|
264
|
+
)
|
|
265
|
+
if filtered[:fdarray_entries].any?
|
|
266
|
+
final[:fdarray_bytes] = build_fdarray_bytes(
|
|
267
|
+
filtered[:fdarray_entries], final[:private_dict_offsets] || []
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
final
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# ------------------------------------------------------------------
|
|
274
|
+
# Table serialization
|
|
275
|
+
# ------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
def write_table(layout, filtered)
|
|
278
|
+
io = StringIO.new(+"")
|
|
279
|
+
io << layout[:header_bytes]
|
|
280
|
+
io << layout[:top_dict_bytes]
|
|
281
|
+
io << layout[:global_subrs_bytes]
|
|
282
|
+
io << layout[:vstore_bytes] if layout[:vstore_bytes]
|
|
283
|
+
io << layout[:fdarray_bytes] if layout[:fdarray_bytes]
|
|
284
|
+
(layout[:private_dicts] || []).each { |pd| io << pd }
|
|
285
|
+
io << layout[:fdselect_bytes] if layout[:fdselect_bytes]
|
|
286
|
+
io << layout[:charstrings_bytes]
|
|
287
|
+
io.string
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# ------------------------------------------------------------------
|
|
291
|
+
# Binary helpers
|
|
292
|
+
# ------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
# Extract the raw bytes of a parsed INDEX from the source table.
|
|
295
|
+
def index_bytes(index)
|
|
296
|
+
@raw.byteslice(index.start_offset, index.total_size)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Extract VStore bytes by computing its byte extent.
|
|
300
|
+
def vstore_bytes(vstore_offset)
|
|
301
|
+
size = vstore_extent(vstore_offset)
|
|
302
|
+
@raw.byteslice(vstore_offset, size)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Compute the byte size of an ItemVariationStore.
|
|
306
|
+
def vstore_extent(vs_off)
|
|
307
|
+
io = StringIO.new(@raw)
|
|
308
|
+
io.seek(vs_off)
|
|
309
|
+
_format = read_u16(io)
|
|
310
|
+
region_list_off = read_u32(io)
|
|
311
|
+
data_count = read_u16(io)
|
|
312
|
+
data_offsets = Array.new(data_count) { read_u32(io) }
|
|
313
|
+
|
|
314
|
+
extents = []
|
|
315
|
+
extents << region_list_end(vs_off + region_list_off) if region_list_off.positive?
|
|
316
|
+
data_offsets.each do |rel_off|
|
|
317
|
+
extents << item_variation_data_end(vs_off + rel_off) if rel_off.positive?
|
|
318
|
+
end
|
|
319
|
+
end_off = extents.max || (8 + (data_count * 4))
|
|
320
|
+
end_off - vs_off
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def region_list_end(abs_off)
|
|
324
|
+
io = StringIO.new(@raw)
|
|
325
|
+
io.seek(abs_off)
|
|
326
|
+
axis_count = read_u16(io)
|
|
327
|
+
region_count = read_u16(io)
|
|
328
|
+
abs_off + 4 + (region_count * axis_count * 6)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def item_variation_data_end(abs_off)
|
|
332
|
+
io = StringIO.new(@raw)
|
|
333
|
+
io.seek(abs_off)
|
|
334
|
+
item_count = read_u16(io)
|
|
335
|
+
short_delta_count = read_u16(io)
|
|
336
|
+
region_index_count = read_u16(io)
|
|
337
|
+
long_count = region_index_count - short_delta_count
|
|
338
|
+
delta_set_size = (short_delta_count * 2) + long_count
|
|
339
|
+
abs_off + 6 + (region_index_count * 2) + (item_count * delta_set_size)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def read_u16(io)
|
|
343
|
+
io.read(2).unpack1("n")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def read_u32(io)
|
|
347
|
+
io.read(4).unpack1("N")
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# ------------------------------------------------------------------
|
|
351
|
+
# DICT encoding
|
|
352
|
+
# ------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
# Encode a DICT hash to binary using the CFF2 DictEncoder.
|
|
355
|
+
def encode_dict(dict)
|
|
356
|
+
io = +""
|
|
357
|
+
dict.each do |operator, operands|
|
|
358
|
+
vals = Array(operands)
|
|
359
|
+
vals.each do |v|
|
|
360
|
+
io << Tables::Cff2::DictEncoder.encode_integer(v.to_i)
|
|
361
|
+
end
|
|
362
|
+
io << Tables::Cff2::DictEncoder.encode_operator(operator)
|
|
363
|
+
end
|
|
364
|
+
io
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Encode a Font DICT. The Private operator (18) always uses 5-byte
|
|
368
|
+
# integer encoding for BOTH operands so the Font DICT byte size
|
|
369
|
+
# is deterministic across layout iterations.
|
|
370
|
+
def encode_font_dict(parsed)
|
|
371
|
+
io = +""
|
|
372
|
+
parsed.each do |operator, operands|
|
|
373
|
+
vals = Array(operands)
|
|
374
|
+
if operator == OP_PRIVATE && vals.size == 2
|
|
375
|
+
io << encode_int32(vals[0].to_i)
|
|
376
|
+
io << encode_int32(vals[1].to_i)
|
|
377
|
+
else
|
|
378
|
+
vals.each { |v| io << Tables::Cff2::DictEncoder.encode_integer(v.to_i) }
|
|
379
|
+
end
|
|
380
|
+
io << Tables::Cff2::DictEncoder.encode_operator(operator)
|
|
381
|
+
end
|
|
382
|
+
io
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# 5-byte integer encoding: byte 29 (marker) + 4-byte big-endian.
|
|
386
|
+
def encode_int32(value)
|
|
387
|
+
[29, value].pack("CN")
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
@@ -17,6 +17,7 @@ module Fontisan
|
|
|
17
17
|
autoload :Cblc, "fontisan/subset/table_strategy/cblc"
|
|
18
18
|
autoload :Cbdt, "fontisan/subset/table_strategy/cbdt"
|
|
19
19
|
autoload :Cff, "fontisan/subset/table_strategy/cff"
|
|
20
|
+
autoload :Cff2, "fontisan/subset/table_strategy/cff2"
|
|
20
21
|
autoload :Cmap, "fontisan/subset/table_strategy/cmap"
|
|
21
22
|
autoload :ColorBitmapPlacement,
|
|
22
23
|
"fontisan/subset/table_strategy/color_bitmap_placement"
|
|
@@ -57,6 +58,7 @@ module Fontisan
|
|
|
57
58
|
"head" => :Head,
|
|
58
59
|
"OS/2" => :Os2,
|
|
59
60
|
"CFF " => :Cff,
|
|
61
|
+
"CFF2" => :Cff2,
|
|
60
62
|
"CBDT" => :Cbdt,
|
|
61
63
|
"CBLC" => :Cblc,
|
|
62
64
|
}.freeze
|
|
@@ -73,8 +73,11 @@ width: nil)
|
|
|
73
73
|
@num_regions = num_regions
|
|
74
74
|
@has_blend = false
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
commands = outline.to_cff_commands
|
|
77
|
+
master_commands = master_outlines.map(&:to_cff_commands)
|
|
78
|
+
|
|
79
|
+
commands.each_with_index do |cmd, i|
|
|
80
|
+
master_cmds = master_commands.map { |mc| mc[i] }
|
|
78
81
|
encode_variable_command(cmd, master_cmds)
|
|
79
82
|
end
|
|
80
83
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
class Cff
|
|
6
|
+
# CFF Standard Strings (SID 0-390).
|
|
7
|
+
#
|
|
8
|
+
# Source: Adobe CFF Specification 1.0 (TN 5176), Appendix A.
|
|
9
|
+
# Identical to fontTools cffLib StandardStrings.
|
|
10
|
+
#
|
|
11
|
+
# Extracted to its own file so the data is separated from
|
|
12
|
+
# the Cff class logic.
|
|
13
|
+
module StandardStrings
|
|
14
|
+
# The 391 CFF standard strings are a fixed spec-defined data
|
|
15
|
+
# table (Adobe TN 5176 Appendix A).
|
|
16
|
+
# rubocop:disable Metrics/CollectionLiteralLength
|
|
17
|
+
LIST = %w[
|
|
18
|
+
.notdef space exclam quotedbl numbersign
|
|
19
|
+
dollar percent ampersand quoteright
|
|
20
|
+
parenleft parenright asterisk plus
|
|
21
|
+
comma hyphen period slash
|
|
22
|
+
zero one two three four five six seven eight nine
|
|
23
|
+
colon semicolon less equal greater question at
|
|
24
|
+
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
|
|
25
|
+
bracketleft backslash bracketright asciicircum underscore quoteleft
|
|
26
|
+
a b c d e f g h i j k l m n o p q r s t u v w x y z
|
|
27
|
+
braceleft bar braceright asciitilde
|
|
28
|
+
exclamdown cent sterling fraction yen florin section currency
|
|
29
|
+
quotesingle quotedblleft guillemotleft guilsinglleft guilsinglright
|
|
30
|
+
fi fl endash dagger daggerdbl periodcentered paragraph bullet
|
|
31
|
+
quotesinglbase quotedblbase quotedblright guillemotright ellipsis
|
|
32
|
+
perthousand questiondown grave acute circumflex tilde macron breve
|
|
33
|
+
dotaccent dieresis ring cedilla hungarumlaut ogonek caron emdash
|
|
34
|
+
AE ordfeminine Lslash Oslash OE ordmasculine
|
|
35
|
+
ae dotlessi lslash oslash oe germandbls
|
|
36
|
+
onesuperior twosuperior threesuperior minus multiply
|
|
37
|
+
oneshalf onequarter threequarters
|
|
38
|
+
questiondownsmall exclamdownsmall capitalshadow asciicircumsmall
|
|
39
|
+
centinferior sterlinginferior fractionsmall zerosuperior yeninferior
|
|
40
|
+
florinsmall dieresissmall caronsmall commaaccent dotlessj ae hoi
|
|
41
|
+
circumflexsmall tildesmall macronsmall brevesmall dotaccentsmall
|
|
42
|
+
slash ringsmall cedillasmall hungarumlautsmall ogoneksmall ringsmall
|
|
43
|
+
fismall flsmall Acutesmall Hungarumlautsmall Caronsmall Cedillasmall
|
|
44
|
+
Brevesmall Macronsmall Dieresissmall Ogoneksmall Circumflexsmall
|
|
45
|
+
Tildesmall Ringsmall Gravesmall Lslashsmall OEsmall florin y z
|
|
46
|
+
commasuperior figuredash hookleft afii00208
|
|
47
|
+
afii10017 afii10018 afii10019 afii10020 afii10021 afii10022 afii10023
|
|
48
|
+
afii10024 afii10025 afii10026 afii10027 afii10028 afii10029 afii10030
|
|
49
|
+
afii10031 afii10032 afii10033 afii10034 afii10035 afii10036 afii10037
|
|
50
|
+
afii10038 afii10039 afii10040 afii10041 afii10042 afii10043 afii10044
|
|
51
|
+
afii10045 afii10046 afii10047 afii10048 afii10049
|
|
52
|
+
afii10065 afii10066 afii10067 afii10068 afii10069 afii10070 afii10071
|
|
53
|
+
afii10072 afii10073 afii10074 afii10075 afii10076 afii10077 afii10078
|
|
54
|
+
afii10079 afii10080 afii10081 afii10082 afii10083 afii10084 afii10085
|
|
55
|
+
afii10086 afii10087 afii10088 afii10089 afii10090 afii10091 afii10092
|
|
56
|
+
afii10093 afii10094 afii10095 afii10096 afii10097
|
|
57
|
+
afii10071.alt afii10061.alt afii10101 afii10102 afii10103 afii10104
|
|
58
|
+
afii10105 afii10106 afii10107 afii10108 afii10109 section.alt afii10110
|
|
59
|
+
afii10193 afii10194 afii10195 afii10196 afii10050 afii10098
|
|
60
|
+
afii10024.alt afii10050.alt Wgrave Wacute Wdieresis Ygrave
|
|
61
|
+
afii10051 afii10052 afii10053 afii10054 afii10055 afii10056 afii10057
|
|
62
|
+
afii10058 afii10059 afii10060 afii10061
|
|
63
|
+
afii10146.alt afii10031.alt afii10147.alt
|
|
64
|
+
afii10146 afii10147 afii10148 afii10192 afii10846
|
|
65
|
+
afii57799 afii57801 afii57800 afii57802 afii57803 afii57804 afii57805
|
|
66
|
+
afii57806 afii57807 afii57808 afii57809 afii57810 afii57811 afii57812
|
|
67
|
+
afii57813 afii57814 afii57815 afii57816 afii57817 afii57818
|
|
68
|
+
afii57388 afii57403 afii57407 afii57409 afii57410 afii57411 afii57412
|
|
69
|
+
afii57413 afii57414 afii57415 afii57416 afii57417 afii57418 afii57419
|
|
70
|
+
afii57420 afii57421 afii57422 afii57423 afii57424 afii57425 afii57426
|
|
71
|
+
afii57427 afii57428 afii57429 afii57430 afii57431 afii57432 afii57433
|
|
72
|
+
afii57434
|
|
73
|
+
Afii61264 Afii61265 Afii61266 afii63167
|
|
74
|
+
afii57511 afii57512 afii57513 afii57514 afii57515 afii57516 afii57517
|
|
75
|
+
afii57518 afii57519 afii57520 afii57521 afii64237 afii64238
|
|
76
|
+
exclamdouble uni204A uni2080 uni2081 uni2082 uni2083 uni2084
|
|
77
|
+
].freeze
|
|
78
|
+
# rubocop:enable Metrics/CollectionLiteralLength
|
|
79
|
+
|
|
80
|
+
COUNT = 391
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/fontisan/tables/cff.rb
CHANGED
|
@@ -65,6 +65,7 @@ module Fontisan
|
|
|
65
65
|
autoload :PrivateDictWriter, "fontisan/tables/cff/private_dict_writer"
|
|
66
66
|
autoload :TableBuilder, "fontisan/tables/cff/table_builder"
|
|
67
67
|
autoload :TopDict, "fontisan/tables/cff/top_dict"
|
|
68
|
+
autoload :StandardStrings, "fontisan/tables/cff/standard_strings_data"
|
|
68
69
|
|
|
69
70
|
# OpenType table tag for CFF
|
|
70
71
|
TAG = "CFF "
|
|
@@ -408,134 +409,17 @@ module Fontisan
|
|
|
408
409
|
true
|
|
409
410
|
end
|
|
410
411
|
|
|
411
|
-
# Adobe Standard Encoding glyph names that map 1:1 to CFF SIDs
|
|
412
|
-
# 0..95. Source: Adobe CFF Specification 1.0 (TN 5176), Appendix A.
|
|
413
|
-
# These cover the printable ASCII range plus .notdef — the most
|
|
414
|
-
# common glyph names encountered in CFF fonts.
|
|
415
|
-
STANDARD_STRINGS_ASCII = [
|
|
416
|
-
".notdef", # SID 0
|
|
417
|
-
"space", # SID 1 (code 32)
|
|
418
|
-
"exclam", # SID 2 (code 33)
|
|
419
|
-
"quotedbl", # SID 3 (code 34)
|
|
420
|
-
"numbersign", # SID 4 (code 35)
|
|
421
|
-
"dollar", # SID 5 (code 36)
|
|
422
|
-
"percent", # SID 6 (code 37)
|
|
423
|
-
"ampersand", # SID 7 (code 38)
|
|
424
|
-
"quoteright", # SID 8 (code 39)
|
|
425
|
-
"parenleft", # SID 9 (code 40)
|
|
426
|
-
"parenright", # SID 10 (code 41)
|
|
427
|
-
"asterisk", # SID 11 (code 42)
|
|
428
|
-
"plus", # SID 12 (code 43)
|
|
429
|
-
"comma", # SID 13 (code 44)
|
|
430
|
-
"hyphen", # SID 14 (code 45)
|
|
431
|
-
"period", # SID 15 (code 46)
|
|
432
|
-
"slash", # SID 16 (code 47)
|
|
433
|
-
"zero", # SID 17 (code 48)
|
|
434
|
-
"one", # SID 18 (code 49)
|
|
435
|
-
"two", # SID 19 (code 50)
|
|
436
|
-
"three", # SID 20 (code 51)
|
|
437
|
-
"four", # SID 21 (code 52)
|
|
438
|
-
"five", # SID 22 (code 53)
|
|
439
|
-
"six", # SID 23 (code 54)
|
|
440
|
-
"seven", # SID 24 (code 55)
|
|
441
|
-
"eight", # SID 25 (code 56)
|
|
442
|
-
"nine", # SID 26 (code 57)
|
|
443
|
-
"colon", # SID 27 (code 58)
|
|
444
|
-
"semicolon", # SID 28 (code 59)
|
|
445
|
-
"less", # SID 29 (code 60)
|
|
446
|
-
"equal", # SID 30 (code 61)
|
|
447
|
-
"greater", # SID 31 (code 62)
|
|
448
|
-
"question", # SID 32 (code 63)
|
|
449
|
-
"at", # SID 33 (code 64)
|
|
450
|
-
"A", # SID 34 (code 65)
|
|
451
|
-
"B", # SID 35 (code 66)
|
|
452
|
-
"C", # SID 36 (code 67)
|
|
453
|
-
"D", # SID 37 (code 68)
|
|
454
|
-
"E", # SID 38 (code 69)
|
|
455
|
-
"F", # SID 39 (code 70)
|
|
456
|
-
"G", # SID 40 (code 71)
|
|
457
|
-
"H", # SID 41 (code 72)
|
|
458
|
-
"I", # SID 42 (code 73)
|
|
459
|
-
"J", # SID 43 (code 74)
|
|
460
|
-
"K", # SID 44 (code 75)
|
|
461
|
-
"L", # SID 45 (code 76)
|
|
462
|
-
"M", # SID 46 (code 77)
|
|
463
|
-
"N", # SID 47 (code 78)
|
|
464
|
-
"O", # SID 48 (code 79)
|
|
465
|
-
"P", # SID 49 (code 80)
|
|
466
|
-
"Q", # SID 50 (code 81)
|
|
467
|
-
"R", # SID 51 (code 82)
|
|
468
|
-
"S", # SID 52 (code 83)
|
|
469
|
-
"T", # SID 53 (code 84)
|
|
470
|
-
"U", # SID 54 (code 85)
|
|
471
|
-
"V", # SID 55 (code 86)
|
|
472
|
-
"W", # SID 56 (code 87)
|
|
473
|
-
"X", # SID 57 (code 88)
|
|
474
|
-
"Y", # SID 58 (code 89)
|
|
475
|
-
"Z", # SID 59 (code 90)
|
|
476
|
-
"bracketleft", # SID 60 (code 91)
|
|
477
|
-
"backslash", # SID 61 (code 92)
|
|
478
|
-
"bracketright", # SID 62 (code 93)
|
|
479
|
-
"asciicircum", # SID 63 (code 94)
|
|
480
|
-
"underscore", # SID 64 (code 95)
|
|
481
|
-
"quoteleft", # SID 65 (code 96)
|
|
482
|
-
"a", # SID 66 (code 97)
|
|
483
|
-
"b", # SID 67 (code 98)
|
|
484
|
-
"c", # SID 68 (code 99)
|
|
485
|
-
"d", # SID 69 (code 100)
|
|
486
|
-
"e", # SID 70 (code 101)
|
|
487
|
-
"f", # SID 71 (code 102)
|
|
488
|
-
"g", # SID 72 (code 103)
|
|
489
|
-
"h", # SID 73 (code 104)
|
|
490
|
-
"i", # SID 74 (code 105)
|
|
491
|
-
"j", # SID 75 (code 106)
|
|
492
|
-
"k", # SID 76 (code 107)
|
|
493
|
-
"l", # SID 77 (code 108)
|
|
494
|
-
"m", # SID 78 (code 109)
|
|
495
|
-
"n", # SID 79 (code 110)
|
|
496
|
-
"o", # SID 80 (code 111)
|
|
497
|
-
"p", # SID 81 (code 112)
|
|
498
|
-
"q", # SID 82 (code 113)
|
|
499
|
-
"r", # SID 83 (code 114)
|
|
500
|
-
"s", # SID 84 (code 115)
|
|
501
|
-
"t", # SID 85 (code 116)
|
|
502
|
-
"u", # SID 86 (code 117)
|
|
503
|
-
"v", # SID 87 (code 118)
|
|
504
|
-
"w", # SID 88 (code 119)
|
|
505
|
-
"x", # SID 89 (code 120)
|
|
506
|
-
"y", # SID 90 (code 121)
|
|
507
|
-
"z", # SID 91 (code 122)
|
|
508
|
-
"braceleft", # SID 92 (code 123)
|
|
509
|
-
"bar", # SID 93 (code 124)
|
|
510
|
-
"braceright", # SID 94 (code 125)
|
|
511
|
-
"asciitilde", # SID 95 (code 126)
|
|
512
|
-
].freeze
|
|
513
|
-
|
|
514
|
-
# First SID outside the ASCII subset. SIDs in
|
|
515
|
-
# EXTENDED_STRINGS_START..390 cover ligatures, currency symbols,
|
|
516
|
-
# Cyrillic (afii*), etc. Resolving those requires the full
|
|
517
|
-
# Adobe CFF spec Appendix A table; tracked as a follow-up to
|
|
518
|
-
# avoid embedding unverified data.
|
|
519
|
-
EXTENDED_STRINGS_START = STANDARD_STRINGS_ASCII.length
|
|
520
|
-
|
|
521
|
-
private
|
|
522
|
-
|
|
523
412
|
# Get a standard CFF string by SID (0-390).
|
|
524
413
|
#
|
|
525
|
-
#
|
|
526
|
-
#
|
|
527
|
-
# require the full Adobe CFF spec Appendix A table to fill in
|
|
528
|
-
# correctly. Until that follow-up lands, extended SIDs return
|
|
529
|
-
# nil so callers can detect the gap and fall back to the
|
|
530
|
-
# custom string index.
|
|
414
|
+
# Full 391-entry standard string table per Adobe CFF spec
|
|
415
|
+
# Appendix A. Returns nil for SID < 0 or SID > 390.
|
|
531
416
|
#
|
|
532
417
|
# @param sid [Integer] String ID (0-390)
|
|
533
|
-
# @return [String, nil] standard string, or nil for
|
|
418
|
+
# @return [String, nil] standard string, or nil for out-of-range SIDs
|
|
534
419
|
def standard_string(sid)
|
|
535
420
|
return nil if sid.negative?
|
|
536
|
-
return STANDARD_STRINGS_ASCII[sid] if sid < EXTENDED_STRINGS_START
|
|
537
421
|
|
|
538
|
-
|
|
422
|
+
StandardStrings::LIST[sid]
|
|
539
423
|
end
|
|
540
424
|
|
|
541
425
|
# Get the Charset for a specific font
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
3
5
|
module Fontisan
|
|
4
6
|
module Tables
|
|
5
7
|
class Cff2
|
|
@@ -17,10 +19,81 @@ module Fontisan
|
|
|
17
19
|
#
|
|
18
20
|
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/cff2
|
|
19
21
|
class FdSelect
|
|
22
|
+
# Read an FDSelect subtable and return the flat FD-index array
|
|
23
|
+
# (one entry per glyph).
|
|
24
|
+
#
|
|
25
|
+
# @param raw_data [String] full CFF2 table bytes
|
|
26
|
+
# @param offset [Integer] byte offset of the FDSelect
|
|
27
|
+
# @param num_glyphs [Integer] glyph count (from maxp)
|
|
28
|
+
# @return [Array<Integer>] FD index per glyph
|
|
29
|
+
def self.read(raw_data, offset, num_glyphs)
|
|
30
|
+
io = StringIO.new(raw_data)
|
|
31
|
+
io.seek(offset)
|
|
32
|
+
format = io.read(1).unpack1("C")
|
|
33
|
+
|
|
34
|
+
case format
|
|
35
|
+
when 0 then read_format0(io, num_glyphs)
|
|
36
|
+
when 3 then read_format3(io, num_glyphs)
|
|
37
|
+
when 4 then read_format4(io, num_glyphs)
|
|
38
|
+
else
|
|
39
|
+
raise CorruptedTableError, "unsupported FDSelect format: #{format}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Format 0: uint8 fd_index[numGlyphs]
|
|
44
|
+
def self.read_format0(io, num_glyphs)
|
|
45
|
+
io.read(num_glyphs).unpack("C*")
|
|
46
|
+
end
|
|
47
|
+
private_class_method :read_format0
|
|
48
|
+
|
|
49
|
+
# Format 3: uint16 numRanges, Range3[], uint16 sentinel
|
|
50
|
+
def self.read_format3(io, num_glyphs)
|
|
51
|
+
num_ranges = io.read(2).unpack1("n")
|
|
52
|
+
assignments = Array.new(num_glyphs, 0)
|
|
53
|
+
ranges = Array.new(num_ranges) do
|
|
54
|
+
first = io.read(2).unpack1("n")
|
|
55
|
+
fd = io.read(1).unpack1("C")
|
|
56
|
+
[first, fd]
|
|
57
|
+
end
|
|
58
|
+
sentinel = io.read(2).unpack1("n")
|
|
59
|
+
|
|
60
|
+
ranges.each_cons(2) do |(first, fd), (next_first, _)|
|
|
61
|
+
assignments.fill(fd, first, next_first - first)
|
|
62
|
+
end
|
|
63
|
+
if ranges.any?
|
|
64
|
+
last_first, last_fd = ranges.last
|
|
65
|
+
assignments.fill(last_fd, last_first, sentinel - last_first)
|
|
66
|
+
end
|
|
67
|
+
assignments
|
|
68
|
+
end
|
|
69
|
+
private_class_method :read_format3
|
|
70
|
+
|
|
71
|
+
# Format 4: uint32 numRanges, Range4[], uint32 sentinel
|
|
72
|
+
def self.read_format4(io, num_glyphs)
|
|
73
|
+
num_ranges = io.read(4).unpack1("N")
|
|
74
|
+
assignments = Array.new(num_glyphs, 0)
|
|
75
|
+
ranges = Array.new(num_ranges) do
|
|
76
|
+
first = io.read(4).unpack1("N")
|
|
77
|
+
fd = io.read(2).unpack1("n")
|
|
78
|
+
[first, fd]
|
|
79
|
+
end
|
|
80
|
+
sentinel = io.read(4).unpack1("N")
|
|
81
|
+
|
|
82
|
+
ranges.each_cons(2) do |(first, fd), (next_first, _)|
|
|
83
|
+
assignments.fill(fd, first, next_first - first)
|
|
84
|
+
end
|
|
85
|
+
if ranges.any?
|
|
86
|
+
last_first, last_fd = ranges.last
|
|
87
|
+
assignments.fill(last_fd, last_first, sentinel - last_first)
|
|
88
|
+
end
|
|
89
|
+
assignments
|
|
90
|
+
end
|
|
91
|
+
private_class_method :read_format4
|
|
92
|
+
|
|
20
93
|
# @param assignments [Array<Integer>] FD index per glyph (length = numGlyphs)
|
|
21
94
|
# @return [String] FDSelect bytes in the most compact format
|
|
22
95
|
def self.build(assignments)
|
|
23
|
-
return 0.to_s if assignments.empty?
|
|
96
|
+
return [0].pack("C").to_s + "".b if assignments.empty?
|
|
24
97
|
|
|
25
98
|
format0_bytes(assignments)
|
|
26
99
|
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff2
|
|
8
|
+
# Reads a CFF2 INDEX structure.
|
|
9
|
+
#
|
|
10
|
+
# A CFF2 INDEX uses a 4-byte count (Card32) instead of the
|
|
11
|
+
# 2-byte count (Card16) used by CFF1. Otherwise the layout is
|
|
12
|
+
# the same: count, offSize, offset array (1-based), then data.
|
|
13
|
+
#
|
|
14
|
+
# An empty INDEX is just the 4-byte count field (= 0).
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# idx = Cff2::Index.read(raw_cff2_bytes, charstrings_offset)
|
|
18
|
+
# idx[0] # first item
|
|
19
|
+
# idx.count
|
|
20
|
+
# idx.total_size
|
|
21
|
+
class Index
|
|
22
|
+
include Enumerable
|
|
23
|
+
|
|
24
|
+
attr_reader :count, :off_size, :offsets, :data, :start_offset
|
|
25
|
+
|
|
26
|
+
# Read a CFF2 INDEX at +offset+ within +raw_data+.
|
|
27
|
+
#
|
|
28
|
+
# @param raw_data [String] full CFF2 table bytes
|
|
29
|
+
# @param offset [Integer] byte offset of the INDEX
|
|
30
|
+
def self.read(raw_data, offset)
|
|
31
|
+
io = StringIO.new(raw_data)
|
|
32
|
+
io.seek(offset)
|
|
33
|
+
new(io, offset)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Build an INDEX from a list of data items.
|
|
37
|
+
# Delegates to {IndexBuilder}.
|
|
38
|
+
#
|
|
39
|
+
# @param items [Array<String>]
|
|
40
|
+
# @return [String] binary INDEX bytes
|
|
41
|
+
def self.build(items)
|
|
42
|
+
IndexBuilder.build(items)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param io [IO, StringIO] positioned at the INDEX start
|
|
46
|
+
# @param start_offset [Integer] byte offset (for diagnostics)
|
|
47
|
+
def initialize(io, start_offset = 0)
|
|
48
|
+
@io = io
|
|
49
|
+
@start_offset = start_offset
|
|
50
|
+
parse!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Fetch the item at +index+ (0-based).
|
|
54
|
+
#
|
|
55
|
+
# @return [String, nil] binary data, or nil if out of range
|
|
56
|
+
def [](index)
|
|
57
|
+
return nil if index.negative? || index >= count
|
|
58
|
+
return "".b if count.zero?
|
|
59
|
+
|
|
60
|
+
start_pos = offsets[index] - 1
|
|
61
|
+
end_pos = offsets[index + 1] - 1
|
|
62
|
+
data[start_pos, end_pos - start_pos]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Iterate over all items.
|
|
66
|
+
def each
|
|
67
|
+
return enum_for(:each) unless block_given?
|
|
68
|
+
|
|
69
|
+
count.times { |i| yield self[i] }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# All items as an array.
|
|
73
|
+
def to_a
|
|
74
|
+
Array.new(count) { |i| self[i] }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def empty?
|
|
78
|
+
count.zero?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Total size of the INDEX in bytes (count + offSize + offsets + data).
|
|
82
|
+
def total_size
|
|
83
|
+
return 4 if count.zero?
|
|
84
|
+
|
|
85
|
+
4 + 1 + ((count + 1) * off_size) + data.bytesize
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def parse!
|
|
91
|
+
@count = read_uint32
|
|
92
|
+
|
|
93
|
+
if @count.zero?
|
|
94
|
+
@off_size = 0
|
|
95
|
+
@offsets = []
|
|
96
|
+
@data = "".b
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@off_size = read_uint8
|
|
101
|
+
raise CorruptedTableError, "invalid CFF2 offSize: #{@off_size}" unless (1..4).cover?(@off_size)
|
|
102
|
+
|
|
103
|
+
@offsets = Array.new(@count + 1) { read_offset(@off_size) }
|
|
104
|
+
validate_offsets!
|
|
105
|
+
|
|
106
|
+
data_size = @offsets.last - 1
|
|
107
|
+
@data = read_bytes(data_size)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def read_uint32
|
|
111
|
+
read_bytes(4).unpack1("N")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def read_uint8
|
|
115
|
+
read_bytes(1).unpack1("C")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def read_offset(size)
|
|
119
|
+
bytes = read_bytes(size)
|
|
120
|
+
case size
|
|
121
|
+
when 1 then bytes.unpack1("C")
|
|
122
|
+
when 2 then bytes.unpack1("n")
|
|
123
|
+
when 3 then bytes.unpack("C3").inject(0) { |s, b| (s << 8) | b }
|
|
124
|
+
when 4 then bytes.unpack1("N")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def read_bytes(n)
|
|
129
|
+
return "".b if n.zero?
|
|
130
|
+
|
|
131
|
+
bytes = @io.read(n)
|
|
132
|
+
if bytes.nil? || bytes.bytesize < n
|
|
133
|
+
raise CorruptedTableError,
|
|
134
|
+
"unexpected EOF in CFF2 INDEX at offset #{@start_offset}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
bytes
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def validate_offsets!
|
|
141
|
+
unless @offsets.first == 1
|
|
142
|
+
raise CorruptedTableError,
|
|
143
|
+
"CFF2 INDEX first offset must be 1, got #{@offsets.first}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
@offsets.each_cons(2) do |prev, curr|
|
|
147
|
+
next unless curr < prev
|
|
148
|
+
|
|
149
|
+
raise CorruptedTableError, "CFF2 INDEX offsets not ascending"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -235,25 +235,7 @@ module Fontisan
|
|
|
235
235
|
Cff::Index.new(@io, start_offset: offset)
|
|
236
236
|
end
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
# Read bytes safely with EOF checking
|
|
241
|
-
#
|
|
242
|
-
# @param bytes [Integer] Number of bytes to read
|
|
243
|
-
# @param description [String] Description for error messages
|
|
244
|
-
# @return [String] Binary data
|
|
245
|
-
# @raise [EOFError] If not enough bytes available
|
|
246
|
-
def read_safely(bytes, description)
|
|
247
|
-
data = @io.read(bytes)
|
|
248
|
-
if data.nil? || data.bytesize < bytes
|
|
249
|
-
raise EOFError,
|
|
250
|
-
"Unexpected EOF while reading #{description}"
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
data
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# Parse DICT structure
|
|
238
|
+
# Parse a DICT structure (public: reused by the CFF2 subsetter).
|
|
257
239
|
#
|
|
258
240
|
# @param data [String] DICT binary data
|
|
259
241
|
# @return [Hash] Parsed operators and values
|
|
@@ -281,20 +263,37 @@ module Fontisan
|
|
|
281
263
|
dict
|
|
282
264
|
end
|
|
283
265
|
|
|
266
|
+
private
|
|
267
|
+
|
|
268
|
+
# Read bytes safely with EOF checking
|
|
269
|
+
#
|
|
270
|
+
# @param bytes [Integer] Number of bytes to read
|
|
271
|
+
# @param description [String] Description for error messages
|
|
272
|
+
# @return [String] Binary data
|
|
273
|
+
# @raise [EOFError] If not enough bytes available
|
|
274
|
+
def read_safely(bytes, description)
|
|
275
|
+
data = @io.read(bytes)
|
|
276
|
+
if data.nil? || data.bytesize < bytes
|
|
277
|
+
raise EOFError,
|
|
278
|
+
"Unexpected EOF while reading #{description}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
data
|
|
282
|
+
end
|
|
283
|
+
|
|
284
284
|
# Check if byte is an operator
|
|
285
285
|
#
|
|
286
|
-
# CFF2
|
|
286
|
+
# CFF/CFF2 DICT operator bytes are 0-21 (with 12 as the two-byte
|
|
287
|
+
# escape prefix) and 24 (vstore, CFF2-specific). Number encoding
|
|
288
|
+
# prefixes (28, 29, 30) and ranges (32-254) are NOT operators.
|
|
287
289
|
#
|
|
288
290
|
# @param byte [Integer] Byte value
|
|
289
291
|
# @return [Boolean] True if operator
|
|
290
292
|
def operator_byte?(byte)
|
|
291
|
-
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
# CFF2-specific operators
|
|
295
|
-
return true if byte == VSTORE_OPERATOR
|
|
293
|
+
return false if (28..30).cover?(byte) # number prefixes
|
|
294
|
+
return false if (32..254).cover?(byte) # number ranges
|
|
296
295
|
|
|
297
|
-
|
|
296
|
+
true # 0-27 (incl. 12 escape), 31, 255 — treat as operator
|
|
298
297
|
end
|
|
299
298
|
|
|
300
299
|
# Read DICT operator
|
data/lib/fontisan/tables/cff2.rb
CHANGED
|
@@ -26,6 +26,7 @@ module Fontisan
|
|
|
26
26
|
autoload :CharstringParser, "fontisan/tables/cff2/charstring_parser"
|
|
27
27
|
autoload :FdSelect, "fontisan/tables/cff2/fd_select"
|
|
28
28
|
autoload :Header, "fontisan/tables/cff2/header"
|
|
29
|
+
autoload :Index, "fontisan/tables/cff2/index"
|
|
29
30
|
autoload :IndexBuilder, "fontisan/tables/cff2/index_builder"
|
|
30
31
|
autoload :DictEncoder, "fontisan/tables/cff2/dict_encoder"
|
|
31
32
|
autoload :OperandStack, "fontisan/tables/cff2/operand_stack"
|
|
@@ -33,42 +33,143 @@ module Fontisan
|
|
|
33
33
|
|
|
34
34
|
# @param font [Fontisan::Ufo::Font]
|
|
35
35
|
# @param glyphs [Array<Fontisan::Ufo::Glyph>] in gid order
|
|
36
|
+
# @param masters [Array<Hash>, nil] variation masters for variable
|
|
37
|
+
# CFF2. Each hash: { font:, axes: }. When present, charstrings
|
|
38
|
+
# emit blend operators and a VariationStore is embedded.
|
|
39
|
+
# @param axis_count [Integer, nil] number of variation axes
|
|
36
40
|
# @param variation_store [String, nil] ItemVariationStore bytes
|
|
37
41
|
# for variable CFF2 fonts. When present, embedded between the
|
|
38
42
|
# GlobalSubr INDEX and CharStrings INDEX, and referenced from
|
|
39
43
|
# the Top DICT via operator 24.
|
|
40
44
|
# @return [String] CFF2 table bytes
|
|
41
|
-
def self.build(
|
|
45
|
+
def self.build(font, glyphs:, masters: nil, axis_count: nil,
|
|
46
|
+
variation_store: nil)
|
|
47
|
+
if masters&.any?
|
|
48
|
+
build_variable(font, glyphs, masters, axis_count)
|
|
49
|
+
else
|
|
50
|
+
build_static(font, glyphs, variation_store:)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Build a static CFF2 table (no variation data).
|
|
55
|
+
def self.build_static(font, glyphs, variation_store:)
|
|
42
56
|
charstrings = glyphs.map { |g| charstring_for(g) }
|
|
43
57
|
global_subr_index = empty_global_subr_index
|
|
44
58
|
font_dict = build_font_dict(private_size: 0, private_offset: 0)
|
|
45
59
|
font_dict_index = Tables::Cff2::IndexBuilder.build([font_dict])
|
|
46
60
|
vs_bytes = variation_store&.b
|
|
47
61
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
variation_store_offset: vs_bytes ? 0 : nil
|
|
62
|
+
layout = converge_layout(
|
|
63
|
+
charstrings:, global_subr_index:, font_dict_index:,
|
|
64
|
+
variation_store: vs_bytes
|
|
52
65
|
)
|
|
53
|
-
layout
|
|
54
|
-
|
|
66
|
+
assemble(layout:)
|
|
67
|
+
end
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
# Build a variable CFF2 table with blend operators and a VStore.
|
|
70
|
+
#
|
|
71
|
+
# Each glyph gets a variable charstring with blend operators when
|
|
72
|
+
# master outlines are available for it. Glyphs without masters fall
|
|
73
|
+
# back to static charstrings. The VStore contains one region per
|
|
74
|
+
# master, each peaking at the master's axis location.
|
|
75
|
+
def self.build_variable(font, glyphs, masters, axis_count)
|
|
76
|
+
num_regions = masters.size
|
|
77
|
+
vstore = build_variation_store(masters, axis_count)
|
|
65
78
|
|
|
66
|
-
|
|
79
|
+
master_glyph_sets = masters.map do |m|
|
|
80
|
+
{ font: m[:font], by_name: m[:font].glyphs }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
charstrings = glyphs.map.with_index do |glyph, _idx|
|
|
84
|
+
master_outlines = master_glyph_sets.filter_map do |mg|
|
|
85
|
+
mg[:by_name][glyph.name]&.to_outline
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if master_outlines.size == num_regions && glyph.contours.any?
|
|
89
|
+
Tables::Cff::Cff2CharStringBuilder.build_variable(
|
|
90
|
+
glyph.to_outline,
|
|
91
|
+
master_outlines: master_outlines,
|
|
92
|
+
num_regions: num_regions,
|
|
93
|
+
width: glyph.width.to_i,
|
|
94
|
+
)
|
|
95
|
+
else
|
|
96
|
+
charstring_for(glyph)
|
|
97
|
+
end
|
|
67
98
|
end
|
|
68
99
|
|
|
100
|
+
global_subr_index = empty_global_subr_index
|
|
101
|
+
font_dict = build_font_dict(private_size: 0, private_offset: 0)
|
|
102
|
+
font_dict_index = Tables::Cff2::IndexBuilder.build([font_dict])
|
|
103
|
+
|
|
104
|
+
layout = converge_layout(
|
|
105
|
+
charstrings:, global_subr_index:, font_dict_index:,
|
|
106
|
+
variation_store: vstore
|
|
107
|
+
)
|
|
69
108
|
assemble(layout:)
|
|
70
109
|
end
|
|
71
110
|
|
|
111
|
+
# Build the ItemVariationStore for CFF2: one region per master,
|
|
112
|
+
# each peaking at the master's axis location.
|
|
113
|
+
def self.build_variation_store(masters, axis_count)
|
|
114
|
+
ac = axis_count || masters.first&.dig(:axes)&.size || 1
|
|
115
|
+
regions = masters.each_with_index.map do |_master, midx|
|
|
116
|
+
Array.new(ac) do |aidx|
|
|
117
|
+
if aidx == midx
|
|
118
|
+
{ start: -1.0, peak: 1.0, end: 1.0 }
|
|
119
|
+
else
|
|
120
|
+
{ start: -1.0, peak: 0.0, end: 1.0 }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
region_list = serialize_region_list(regions, ac)
|
|
126
|
+
# One ItemVariationData with all regions referenced but zero items.
|
|
127
|
+
# The actual deltas live in charstrings (pushed via blend operands).
|
|
128
|
+
var_data = serialize_empty_variation_data(regions.size)
|
|
129
|
+
|
|
130
|
+
header_size = 8
|
|
131
|
+
offsets_array_size = 4
|
|
132
|
+
region_list_offset = header_size + offsets_array_size
|
|
133
|
+
var_data_offset = region_list_offset + region_list.bytesize
|
|
134
|
+
|
|
135
|
+
store = +""
|
|
136
|
+
store << [1].pack("n")
|
|
137
|
+
store << [region_list_offset].pack("N")
|
|
138
|
+
store << [1].pack("n")
|
|
139
|
+
store << [var_data_offset].pack("N")
|
|
140
|
+
store << region_list
|
|
141
|
+
store << var_data
|
|
142
|
+
store
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.serialize_region_list(regions, axis_count)
|
|
146
|
+
io = +""
|
|
147
|
+
io << [axis_count].pack("n")
|
|
148
|
+
io << [regions.size].pack("n")
|
|
149
|
+
regions.each do |region|
|
|
150
|
+
region.each do |coords|
|
|
151
|
+
io << [f2dot14(coords[:start]), f2dot14(coords[:peak]),
|
|
152
|
+
f2dot14(coords[:end])].pack("nnn")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
io
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Serialize an ItemVariationData with all regions referenced but
|
|
159
|
+
# zero items. CFF2 charstrings push their own deltas via blend.
|
|
160
|
+
def self.serialize_empty_variation_data(region_count)
|
|
161
|
+
io = +""
|
|
162
|
+
io << [0].pack("n") # itemCount = 0
|
|
163
|
+
io << [region_count].pack("n") # shortDeltaCount
|
|
164
|
+
io << [region_count].pack("n") # regionIndexCount
|
|
165
|
+
region_count.times { |i| io << [i].pack("n") }
|
|
166
|
+
io
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.f2dot14(value)
|
|
170
|
+
(value.to_f * 16384).to_i.clamp(-16384, 16384)
|
|
171
|
+
end
|
|
172
|
+
|
|
72
173
|
# ---------- charstring per-glyph ----------
|
|
73
174
|
|
|
74
175
|
def self.charstring_for(glyph)
|
|
@@ -148,6 +249,43 @@ module Fontisan
|
|
|
148
249
|
}
|
|
149
250
|
end
|
|
150
251
|
|
|
252
|
+
# Iteratively encode the Top DICT and recompute offsets until the
|
|
253
|
+
# layout stabilizes. After convergence, performs one final
|
|
254
|
+
# encode + compute pass so +layout[:top_dict]+ carries the
|
|
255
|
+
# converged offsets (not the stale initial placeholder).
|
|
256
|
+
def self.converge_layout(charstrings:, global_subr_index:,
|
|
257
|
+
font_dict_index:, variation_store: nil)
|
|
258
|
+
has_vs = !variation_store.nil?
|
|
259
|
+
top_dict = encode_top_dict(
|
|
260
|
+
charstrings_offset: 0, font_dict_index_offset: 0,
|
|
261
|
+
variation_store_offset: has_vs ? 0 : nil
|
|
262
|
+
)
|
|
263
|
+
layout = compute_layout(top_dict:, charstrings:, global_subr_index:,
|
|
264
|
+
font_dict_index:, variation_store:)
|
|
265
|
+
|
|
266
|
+
10.times do
|
|
267
|
+
top_dict = encode_top_dict(
|
|
268
|
+
charstrings_offset: layout[:charstrings_offset],
|
|
269
|
+
font_dict_index_offset: layout[:font_dict_index_offset],
|
|
270
|
+
variation_store_offset: layout[:variation_store_offset],
|
|
271
|
+
)
|
|
272
|
+
next_layout = compute_layout(top_dict:, charstrings:, global_subr_index:,
|
|
273
|
+
font_dict_index:, variation_store:)
|
|
274
|
+
break if same_offsets?(layout, next_layout)
|
|
275
|
+
|
|
276
|
+
layout = next_layout
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Final encode ensures layout[:top_dict] matches converged offsets
|
|
280
|
+
final_td = encode_top_dict(
|
|
281
|
+
charstrings_offset: layout[:charstrings_offset],
|
|
282
|
+
font_dict_index_offset: layout[:font_dict_index_offset],
|
|
283
|
+
variation_store_offset: layout[:variation_store_offset],
|
|
284
|
+
)
|
|
285
|
+
compute_layout(top_dict: final_td, charstrings:, global_subr_index:,
|
|
286
|
+
font_dict_index:, variation_store:)
|
|
287
|
+
end
|
|
288
|
+
|
|
151
289
|
def self.same_offsets?(a, b)
|
|
152
290
|
a[:charstrings_offset] == b[:charstrings_offset] &&
|
|
153
291
|
a[:font_dict_index_offset] == b[:font_dict_index_offset] &&
|
|
@@ -173,8 +311,12 @@ module Fontisan
|
|
|
173
311
|
|
|
174
312
|
private_class_method :charstring_for, :empty_charstring,
|
|
175
313
|
:encode_top_dict, :build_font_dict,
|
|
176
|
-
:compute_layout, :
|
|
177
|
-
:
|
|
314
|
+
:compute_layout, :converge_layout,
|
|
315
|
+
:same_offsets?,
|
|
316
|
+
:empty_global_subr_index, :assemble,
|
|
317
|
+
:build_static, :build_variable,
|
|
318
|
+
:build_variation_store, :serialize_region_list,
|
|
319
|
+
:serialize_empty_variation_data, :f2dot14
|
|
178
320
|
end
|
|
179
321
|
end
|
|
180
322
|
end
|
|
@@ -9,16 +9,13 @@ module Fontisan
|
|
|
9
9
|
#
|
|
10
10
|
# The orchestrator assembles a default-master UFO plus variation
|
|
11
11
|
# masters into a variable OpenType font with:
|
|
12
|
-
# - CFF2 outlines
|
|
12
|
+
# - CFF2 outlines with blend operators for each varying coordinate
|
|
13
|
+
# - ItemVariationStore embedded in the CFF2 table (regions only;
|
|
14
|
+
# deltas live in charstring blend operands)
|
|
13
15
|
# - fvar (axes + instances)
|
|
14
16
|
# - STAT (style attributes)
|
|
15
17
|
# - avar (axis remapping, when maps are provided)
|
|
16
18
|
#
|
|
17
|
-
# NOTE: the CFF2 table currently contains static outlines (no
|
|
18
|
-
# VariationStore, no blend operators). Full variable CFF2 requires
|
|
19
|
-
# TODO 07 (blend/vsindex) and TODO 18 (blend integration) to be
|
|
20
|
-
# wired into the charstring builder.
|
|
21
|
-
#
|
|
22
19
|
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/cff2
|
|
23
20
|
class VariableOtf
|
|
24
21
|
# @param default_font [Fontisan::Ufo::Font] the default master
|
|
@@ -59,7 +56,7 @@ module Fontisan
|
|
|
59
56
|
"post" => Post.build(@default),
|
|
60
57
|
"hmtx" => Hmtx.build(@default, glyphs: glyphs),
|
|
61
58
|
"cmap" => Cmap.build(@default, glyphs: glyphs),
|
|
62
|
-
"CFF2" =>
|
|
59
|
+
"CFF2" => build_cff2(glyphs),
|
|
63
60
|
"fvar" => Fvar.build(@default, axes: @axes, instances: @instances),
|
|
64
61
|
"STAT" => Stat.build(axes: @axes),
|
|
65
62
|
}
|
|
@@ -69,6 +66,18 @@ module Fontisan
|
|
|
69
66
|
|
|
70
67
|
tables
|
|
71
68
|
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Build the CFF2 table. When masters are supplied, variable
|
|
73
|
+
# charstrings with blend operators are emitted; otherwise a static
|
|
74
|
+
# CFF2 table is produced.
|
|
75
|
+
def build_cff2(glyphs)
|
|
76
|
+
masters = @masters.map { |mf| { font: mf, axes: @axes } }
|
|
77
|
+
Cff2.build(@default, glyphs: glyphs,
|
|
78
|
+
masters: masters.any? ? masters : nil,
|
|
79
|
+
axis_count: @axes.size)
|
|
80
|
+
end
|
|
72
81
|
end
|
|
73
82
|
end
|
|
74
83
|
end
|
data/lib/fontisan/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.33
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -486,6 +486,7 @@ files:
|
|
|
486
486
|
- lib/fontisan/subset/table_strategy/cbdt.rb
|
|
487
487
|
- lib/fontisan/subset/table_strategy/cblc.rb
|
|
488
488
|
- lib/fontisan/subset/table_strategy/cff.rb
|
|
489
|
+
- lib/fontisan/subset/table_strategy/cff2.rb
|
|
489
490
|
- lib/fontisan/subset/table_strategy/cmap.rb
|
|
490
491
|
- lib/fontisan/subset/table_strategy/color_bitmap_placement.rb
|
|
491
492
|
- lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb
|
|
@@ -552,6 +553,7 @@ files:
|
|
|
552
553
|
- lib/fontisan/tables/cff/offset_recalculator.rb
|
|
553
554
|
- lib/fontisan/tables/cff/private_dict.rb
|
|
554
555
|
- lib/fontisan/tables/cff/private_dict_writer.rb
|
|
556
|
+
- lib/fontisan/tables/cff/standard_strings_data.rb
|
|
555
557
|
- lib/fontisan/tables/cff/table_builder.rb
|
|
556
558
|
- lib/fontisan/tables/cff/top_dict.rb
|
|
557
559
|
- lib/fontisan/tables/cff2.rb
|
|
@@ -560,6 +562,7 @@ files:
|
|
|
560
562
|
- lib/fontisan/tables/cff2/dict_encoder.rb
|
|
561
563
|
- lib/fontisan/tables/cff2/fd_select.rb
|
|
562
564
|
- lib/fontisan/tables/cff2/header.rb
|
|
565
|
+
- lib/fontisan/tables/cff2/index.rb
|
|
563
566
|
- lib/fontisan/tables/cff2/index_builder.rb
|
|
564
567
|
- lib/fontisan/tables/cff2/operand_stack.rb
|
|
565
568
|
- lib/fontisan/tables/cff2/private_dict_blend_handler.rb
|