Class: Fontisan::Subset::TableStrategy::Cff2
- Inherits:
-
Object
- Object
- Fontisan::Subset::TableStrategy::Cff2
- Defined in:
- lib/fontisan/subset/table_strategy/cff2.rb
Overview
CFF2 (Compact Font Format 2) subsetter strategy.
Subsets CFF2 tables by filtering the CharStrings INDEX and FDSelect
to the subset glyphs, then rebuilding the table with patched offsets.
The Variable Store (ItemVariationStore) is preserved unchanged —
its vsindex references in charstrings remain valid because
charstrings are carried as opaque bytes.
Unlike the CFF1 subsetter (which routes through UFO), CFF2 uses a standalone binary filter because CFF2 charstrings carry blend/vsindex operators for variation that the UFO round-trip would strip.
Layout of the rebuilt table:
Header (5 bytes)
Top DICT (offsets patched to new positions)
Global Subr INDEX (preserved as-is)
[Variable Store] (preserved as-is, if present)
[FDArray INDEX] (Private DICT offsets patched)
[Private DICTs] (preserved as-is, relocated after FDArray)
[FDSelect] (filtered to subset glyphs, if present)
CharStrings INDEX (filtered to subset glyphs)
Private DICT offsets in Font DICTs always use the 5-byte integer encoding (operator 29 + int32). This makes the FDArray byte size independent of the offset values, breaking the layout chicken-and-egg.
See TODO #15 for the full design rationale.
Constant Summary collapse
- OP_CHARSTRINGS =
CFF2 Top DICT operator codes (per Adobe TN 5177 §8.1).
17- OP_VSTORE =
24- OP_FDARRAY =
[12, 36].freeze
- OP_FDSELECT =
[12, 37].freeze
- OP_PRIVATE =
18- MAX_TOPDICT_ITERATIONS =
16
Class Method Summary collapse
-
.call(context:, tag:, table:) ⇒ String
Subset CFF2 table bytes.
Instance Method Summary collapse
-
#build_fdarray_bytes(entries, priv_offsets) ⇒ Object
Build the FDArray INDEX bytes with the given Private DICT offsets.
-
#build_filtered_sections(sections, retained_gids) ⇒ Object
Build all filtered/rebuilt section bytes.
-
#build_top_dict_bytes(layout) ⇒ Object
Build the Top DICT bytes with updated section offsets.
-
#converge_layout(sections, filtered) ⇒ Object
Iterate Top DICT encoding to stability, then produce the final layout with FDArray patched with concrete Private DICT offsets.
-
#encode_dict(dict) ⇒ Object
Encode a DICT hash to binary using the CFF2 DictEncoder.
-
#encode_font_dict(parsed) ⇒ Object
Encode a Font DICT.
-
#encode_int32(value) ⇒ Object
5-byte integer encoding: byte 29 (marker) + 4-byte big-endian.
-
#filter_charstrings(source_cs, retained_gids) ⇒ Object
Build a new CharStrings INDEX containing only the retained glyphs.
-
#filter_fdselect(sections, retained_gids) ⇒ Object
Rebuild FDSelect to cover only retained glyphs in their new order.
-
#finalize_layout(layout, sections, top_dict_size, filtered, fdarray_placeholder, priv_bytes) ⇒ Object
Produce the final layout: stabilized Top DICT bytes, header, and FDArray rebuilt with concrete Private DICT offsets.
-
#gather_sections ⇒ Object
------------------------------------------------------------------ Source section gathering ------------------------------------------------------------------.
-
#index_bytes(index) ⇒ Object
Extract the raw bytes of a parsed INDEX from the source table.
-
#initialize(raw_data, mapping) ⇒ Cff2
constructor
------------------------------------------------------------------.
- #item_variation_data_end(abs_off) ⇒ Object
-
#layout_offsets(sections, top_dict_size, filtered, fdarray_bytes, priv_bytes) ⇒ Object
Assign byte offsets to every section.
-
#parse_fdarray_entries(source_fdarray) ⇒ Object
Parse each Font DICT in the FDArray, extract Private DICT bytes, and prepare entries for relocation.
- #read_index(offset) ⇒ Object
- #read_u16(io) ⇒ Object
- #read_u32(io) ⇒ Object
- #region_list_end(abs_off) ⇒ Object
-
#subset ⇒ String
Build the subset CFF2 table.
-
#vstore_bytes(vstore_offset) ⇒ Object
Extract VStore bytes by computing its byte extent.
-
#vstore_extent(vs_off) ⇒ Object
Compute the byte size of an ItemVariationStore.
-
#write_table(layout, filtered) ⇒ Object
------------------------------------------------------------------ Table serialization ------------------------------------------------------------------.
Constructor Details
#initialize(raw_data, mapping) ⇒ Cff2
69 70 71 72 73 74 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 69 def initialize(raw_data, mapping) @raw = raw_data @mapping = mapping @reader = Tables::Cff2::TableReader.new(raw_data) @reader.read_top_dict end |
Class Method Details
.call(context:, tag:, table:) ⇒ String
Returns subset CFF2 table bytes.
50 51 52 53 54 55 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 50 def self.call(context:, tag:, table:) raw = raw_bytes_from_font(context.font) || raw_bytes_from_table(table) return raw unless raw && !raw.empty? new(raw, context.mapping).subset end |
Instance Method Details
#build_fdarray_bytes(entries, priv_offsets) ⇒ Object
Build the FDArray INDEX bytes with the given Private DICT offsets. Private offsets always use 5-byte int32 encoding for deterministic sizing across layout iterations.
163 164 165 166 167 168 169 170 171 172 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 163 def build_fdarray_bytes(entries, priv_offsets) rebuilt = entries.each_with_index.map do |entry, idx| parsed = entry[:parsed].dup if entry[:priv_size] parsed[OP_PRIVATE] = [entry[:priv_size], priv_offsets[idx] || 0] end encode_font_dict(parsed) end Tables::Cff2::IndexBuilder.build(rebuilt) end |
#build_filtered_sections(sections, retained_gids) ⇒ Object
Build all filtered/rebuilt section bytes.
116 117 118 119 120 121 122 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 116 def build_filtered_sections(sections, retained_gids) { charstrings_bytes: filter_charstrings(sections[:charstrings], retained_gids), fdselect_bytes: filter_fdselect(sections, retained_gids), fdarray_entries: sections[:fdarray] ? parse_fdarray_entries(sections[:fdarray]) : [], } end |
#build_top_dict_bytes(layout) ⇒ Object
Build the Top DICT bytes with updated section offsets.
246 247 248 249 250 251 252 253 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 246 def build_top_dict_bytes(layout) td = @reader.top_dict.dup td[OP_CHARSTRINGS] = layout[:charstrings_offset] td[OP_VSTORE] = layout[:vstore_offset] if layout[:vstore_offset] td[OP_FDARRAY] = layout[:fdarray_offset] if layout[:fdarray_offset] td[OP_FDSELECT] = layout[:fdselect_offset] if layout[:fdselect_offset] encode_dict(td) end |
#converge_layout(sections, filtered) ⇒ Object
Iterate Top DICT encoding to stability, then produce the final layout with FDArray patched with concrete Private DICT offsets.
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 180 def converge_layout(sections, filtered) top_dict_size = @reader.header[:top_dict_length] placeholder_offsets = Array.new(filtered[:fdarray_entries].size, 0) fdarray_placeholder = if filtered[:fdarray_entries].any? build_fdarray_bytes(filtered[:fdarray_entries], placeholder_offsets) end priv_bytes = filtered[:fdarray_entries].map { |e| e[:priv_bytes] }.compact prev_size = nil layout = nil MAX_TOPDICT_ITERATIONS.times do layout = layout_offsets(sections, top_dict_size, filtered, fdarray_placeholder, priv_bytes) new_td = build_top_dict_bytes(layout) top_dict_size = new_td.bytesize break if top_dict_size == prev_size prev_size = top_dict_size end finalize_layout(layout, sections, top_dict_size, filtered, fdarray_placeholder, priv_bytes) end |
#encode_dict(dict) ⇒ Object
Encode a DICT hash to binary using the CFF2 DictEncoder.
354 355 356 357 358 359 360 361 362 363 364 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 354 def encode_dict(dict) io = +"" dict.each do |operator, operands| vals = Array(operands) vals.each do |v| io << Tables::Cff2::DictEncoder.encode_integer(v.to_i) end io << Tables::Cff2::DictEncoder.encode_operator(operator) end io end |
#encode_font_dict(parsed) ⇒ Object
Encode a Font DICT. The Private operator (18) always uses 5-byte integer encoding for BOTH operands so the Font DICT byte size is deterministic across layout iterations.
369 370 371 372 373 374 375 376 377 378 379 380 381 382 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 369 def encode_font_dict(parsed) io = +"" parsed.each do |operator, operands| vals = Array(operands) if operator == OP_PRIVATE && vals.size == 2 io << encode_int32(vals[0].to_i) io << encode_int32(vals[1].to_i) else vals.each { |v| io << Tables::Cff2::DictEncoder.encode_integer(v.to_i) } end io << Tables::Cff2::DictEncoder.encode_operator(operator) end io end |
#encode_int32(value) ⇒ Object
5-byte integer encoding: byte 29 (marker) + 4-byte big-endian.
385 386 387 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 385 def encode_int32(value) [29, value].pack("CN") end |
#filter_charstrings(source_cs, retained_gids) ⇒ Object
Build a new CharStrings INDEX containing only the retained glyphs.
125 126 127 128 129 130 131 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 125 def filter_charstrings(source_cs, retained_gids) items = retained_gids.map do |gid| item = source_cs[gid] item.nil? || item.empty? ? "\x0e".b : item end Tables::Cff2::IndexBuilder.build(items) end |
#filter_fdselect(sections, retained_gids) ⇒ Object
Rebuild FDSelect to cover only retained glyphs in their new order.
134 135 136 137 138 139 140 141 142 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 134 def filter_fdselect(sections, retained_gids) return nil unless sections[:fdselect_offset] assignments = Tables::Cff2::FdSelect.read( @raw, sections[:fdselect_offset], sections[:charstrings].count ) new_assigns = retained_gids.map { |gid| assignments[gid] || 0 } Tables::Cff2::FdSelect.build(new_assigns) end |
#finalize_layout(layout, sections, top_dict_size, filtered, fdarray_placeholder, priv_bytes) ⇒ Object
Produce the final layout: stabilized Top DICT bytes, header, and FDArray rebuilt with concrete Private DICT offsets.
257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 257 def finalize_layout(layout, sections, top_dict_size, filtered, fdarray_placeholder, priv_bytes) final = layout.dup final[:top_dict_bytes] = build_top_dict_bytes(layout) final[:header_bytes] = Tables::Cff2::Header.build( top_dict_size: top_dict_size, ) if filtered[:fdarray_entries].any? final[:fdarray_bytes] = build_fdarray_bytes( filtered[:fdarray_entries], final[:private_dict_offsets] || [] ) end final end |
#gather_sections ⇒ Object
Source section gathering
92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 92 def gather_sections td = @reader.top_dict hdr = @reader.header gsubr_off = hdr[:header_size] + hdr[:top_dict_length] { header_size: hdr[:header_size], charstrings: read_index(td[OP_CHARSTRINGS]), gsubr: read_index(gsubr_off), fdarray: td[OP_FDARRAY] ? read_index(td[OP_FDARRAY]) : nil, fdselect_offset: td[OP_FDSELECT], vstore_offset: td[OP_VSTORE], } end |
#index_bytes(index) ⇒ Object
Extract the raw bytes of a parsed INDEX from the source table.
294 295 296 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 294 def index_bytes(index) @raw.byteslice(index.start_offset, index.total_size) end |
#item_variation_data_end(abs_off) ⇒ Object
330 331 332 333 334 335 336 337 338 339 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 330 def item_variation_data_end(abs_off) io = StringIO.new(@raw) io.seek(abs_off) item_count = read_u16(io) short_delta_count = read_u16(io) region_index_count = read_u16(io) long_count = region_index_count - short_delta_count delta_set_size = (short_delta_count * 2) + long_count abs_off + 6 + (region_index_count * 2) + (item_count * delta_set_size) end |
#layout_offsets(sections, top_dict_size, filtered, fdarray_bytes, priv_bytes) ⇒ Object
Assign byte offsets to every section.
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 206 def layout_offsets(sections, top_dict_size, filtered, fdarray_bytes, priv_bytes) pos = sections[:header_size] + top_dict_size layout = { top_dict_size: top_dict_size } layout[:global_subrs_offset] = pos layout[:global_subrs_bytes] = index_bytes(sections[:gsubr]) pos += layout[:global_subrs_bytes].bytesize if sections[:vstore_offset] layout[:vstore_offset] = pos layout[:vstore_bytes] = vstore_bytes(sections[:vstore_offset]) pos += layout[:vstore_bytes].bytesize end if fdarray_bytes layout[:fdarray_offset] = pos layout[:fdarray_bytes] = fdarray_bytes pos += fdarray_bytes.bytesize priv_offsets = priv_bytes.map do |pb| off = pos pos += pb.bytesize off end layout[:private_dict_offsets] = priv_offsets layout[:private_dicts] = priv_bytes end if filtered[:fdselect_bytes] layout[:fdselect_offset] = pos layout[:fdselect_bytes] = filtered[:fdselect_bytes] pos += layout[:fdselect_bytes].bytesize end layout[:charstrings_offset] = pos layout[:charstrings_bytes] = filtered[:charstrings_bytes] layout end |
#parse_fdarray_entries(source_fdarray) ⇒ Object
Parse each Font DICT in the FDArray, extract Private DICT bytes, and prepare entries for relocation.
146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 146 def parse_fdarray_entries(source_fdarray) source_fdarray.to_a.map do |fd_bytes| parsed = @reader.parse_dict(fd_bytes) priv = parsed[OP_PRIVATE] entry = { parsed: parsed, priv_bytes: nil, priv_size: nil } next entry unless priv.is_a?(Array) && priv.size == 2 size, offset = priv entry[:priv_bytes] = @raw.byteslice(offset, size) entry[:priv_size] = size entry end end |
#read_index(offset) ⇒ Object
107 108 109 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 107 def read_index(offset) Tables::Cff2::Index.read(@raw, offset) end |
#read_u16(io) ⇒ Object
341 342 343 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 341 def read_u16(io) io.read(2).unpack1("n") end |
#read_u32(io) ⇒ Object
345 346 347 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 345 def read_u32(io) io.read(4).unpack1("N") end |
#region_list_end(abs_off) ⇒ Object
322 323 324 325 326 327 328 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 322 def region_list_end(abs_off) io = StringIO.new(@raw) io.seek(abs_off) axis_count = read_u16(io) region_count = read_u16(io) abs_off + 4 + (region_count * axis_count * 6) end |
#subset ⇒ String
Build the subset CFF2 table.
79 80 81 82 83 84 85 86 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 79 def subset sections = gather_sections retained_gids = @mapping.old_ids.sort filtered = build_filtered_sections(sections, retained_gids) layout = converge_layout(sections, filtered) write_table(layout, filtered) end |
#vstore_bytes(vstore_offset) ⇒ Object
Extract VStore bytes by computing its byte extent.
299 300 301 302 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 299 def vstore_bytes(vstore_offset) size = vstore_extent(vstore_offset) @raw.byteslice(vstore_offset, size) end |
#vstore_extent(vs_off) ⇒ Object
Compute the byte size of an ItemVariationStore.
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 305 def vstore_extent(vs_off) io = StringIO.new(@raw) io.seek(vs_off) _format = read_u16(io) region_list_off = read_u32(io) data_count = read_u16(io) data_offsets = Array.new(data_count) { read_u32(io) } extents = [] extents << region_list_end(vs_off + region_list_off) if region_list_off.positive? data_offsets.each do |rel_off| extents << item_variation_data_end(vs_off + rel_off) if rel_off.positive? end end_off = extents.max || (8 + (data_count * 4)) end_off - vs_off end |
#write_table(layout, filtered) ⇒ Object
Table serialization
276 277 278 279 280 281 282 283 284 285 286 287 |
# File 'lib/fontisan/subset/table_strategy/cff2.rb', line 276 def write_table(layout, filtered) io = StringIO.new(+"") io << layout[:header_bytes] io << layout[:top_dict_bytes] io << layout[:global_subrs_bytes] io << layout[:vstore_bytes] if layout[:vstore_bytes] io << layout[:fdarray_bytes] if layout[:fdarray_bytes] (layout[:private_dicts] || []).each { |pd| io << pd } io << layout[:fdselect_bytes] if layout[:fdselect_bytes] io << layout[:charstrings_bytes] io.string end |