Class: Fontisan::Stitcher::CbdtPropagator

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/stitcher/cbdt_propagator.rb

Overview

Owns every CBDT-specific concern in the stitch pipeline.

The Stitcher has two distinct CBDT responsibilities that benefit from being in one place:

1. Detection: exactly one CBDT source per stitch is supported.
 Multiple CBDT sources raise MultipleCbdtSourcesError.

2. Action: when a CBDT source exists, the stitcher
 - injects empty CBDT placeholder glyphs into the target UFO
   (the bitmap data lives in CBDT/CBLC tables, not in outlines),
 - propagates the raw CBDT/CBLC tables into the compiled font
   file so the bitmaps survive the round-trip.

Both responsibilities are model-driven from the CBDT Source itself (Source#bitmap_mode == :cbdt), so the propagator is stateless given a list of sources. Tests can construct one with synthetic sources directly.

Instance Method Summary collapse

Constructor Details

#initialize(sources) ⇒ CbdtPropagator

Returns a new instance of CbdtPropagator.

Parameters:

  • sources (Array<Stitcher::Source>)

    all sources the stitcher knows about, in label order (the propagator does not care about labels).



27
28
29
# File 'lib/fontisan/stitcher/cbdt_propagator.rb', line 27

def initialize(sources)
  @sources = sources
end

Instance Method Details

#add_placeholder_glyphs(source, target) ⇒ Object

Inject empty CBDT placeholder glyphs from source into the target UFO. One placeholder per gid in the source's maxp/cmap. Outline data is intentionally absent — the bitmap lives in the CBDT table that #propagate_tables_into will copy later.

Why placeholders are name-deconflicted

Placeholders are named after the CBDT source's gid ("gidN"). Outline donors compiled earlier into the same target use the SAME naming scheme (see Source#extract_truetype_glyph), so a CBDT placeholder at "gid110" would collide with an outline glyph at "gid110" from a different donor. Layer#add raises on conflict (it never silently overwrites — that path previously erased real outline glyphs and dropped their cmap entries, causing the CJK Ext G loss in Essenfont-Regular.ttc). We allocate a unique name via UniqueGlyphName so each placeholder lands at its own GID slot without touching the outlines.

Glyphs are NOT registered with the deduplicator: each CBDT glyph is unique to its source, and deduplication would incorrectly collapse distinct bitmaps.

Parameters:



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/fontisan/stitcher/cbdt_propagator.rb', line 82

def add_placeholder_glyphs(source, target)
  ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
  if ufo
    ufo.glyphs.each_value do |g|
      name = UniqueGlyphName.in(target, g.name)
      target.layers.default_layer.add(GlyphCloner.clone(g, name: name))
    end
    return
  end

  maxp = source.font.table("maxp")
  num_glyphs = maxp&.num_glyphs || 0
  cmap = source.font.table("cmap")
  mappings = cmap&.unicode_mappings || {}

  gid_cps = Hash.new { |h, k| h[k] = [] }
  mappings.each { |cp, gid| gid_cps[gid] << cp }

  num_glyphs.times do |gid|
    base_name = gid.zero? ? ".notdef" : "gid#{gid}"
    name = UniqueGlyphName.in(target, base_name)
    glyph = Ufo::Glyph.new(name: name)
    glyph.width = 0
    gid_cps[gid].each { |cp| glyph.add_unicode(cp) }
    target.layers.default_layer.add(glyph)
  end
end

#cbdt_sourceStitcher::Source?

Returns the single CBDT source, or nil if there is none.

Returns:

Raises:



35
36
37
38
39
40
41
42
43
44
45
# File 'lib/fontisan/stitcher/cbdt_propagator.rb', line 35

def cbdt_source
  cbdts = @sources.select { |s| s.bitmap_mode == :cbdt }
  return nil if cbdts.empty?

  if cbdts.size > 1
    raise MultipleCbdtSourcesError,
          "multiple CBDT sources not supported (found #{cbdts.size})"
  end

  cbdts.first
end

#propagate_tables_into(source, path) ⇒ Object

Propagate the raw CBDT/CBLC tables from the CBDT source into the compiled font at path, rewriting the file in place.

Reads every table from the compiled font as raw bytes (bypassing BinData #table because some tables — notably CFF2 — don't yet have round-trippable BinData models), splices in CBDT/CBLC, and rewrites the file.

No-op when called with a non-CBDT source or nil.

Limitation: GID stability

The propagation copies CBDT/CBLC bytes verbatim. CBLC indexes glyphs by SOURCE GID — the GIDs of the CBDT donor. The compiled font's GIDs may differ because:

1. +add_placeholder_glyphs+ renames CBDT placeholders via
 UniqueGlyphName when their default "gid{N}" name collides
 with outline glyphs sharing the same donor-gid scheme
 (see Layer's naming contract).
2. The compiler assigns GIDs in target-namespace order, which
 is not the same as source-GID order once renaming happens.

The bitmaps line up correctly only when the CBDT source and every outline source cover disjoint codepoint ranges (the Essenfont TTC case: emoji vs CJK Ext G). When ranges overlap, CBLC's source-GID-indexed bitmaps may point at the wrong compiled glyphs and the colour rendering for affected codepoints will fall back to outlines.

A proper fix requires a CBLC rebuild pass: walk the compiled font's cmap to find the new GID for every CBDT-covered source glyph, then rewrite CBLC's IndexSubTableArray + IndexSubTable offsets to match. Tracked as a follow-up.

Parameters:

  • source (Stitcher::Source, nil)

    the CBDT source

  • path (String)

    compiled font file to rewrite



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/fontisan/stitcher/cbdt_propagator.rb', line 147

def propagate_tables_into(source, path)
  return unless source

  compiled = FontLoader.load(path)

  # Read every table as raw bytes straight from the file's table
  # directory. We deliberately bypass #table (which parses via
  # BinData) because some tables — notably CFF2 — don't yet have
  # round-trippable BinData models; calling #table on them returns
  # nil and would silently drop them from the rewritten font.
  tables = {}
  compiled.table_names.each do |tag|
    raw = compiled.table_data[tag]
    tables[tag] = raw if raw
  end

  cbdt_bytes = source.raw_table_bytes("CBDT")
  cblc_bytes = source.raw_table_bytes("CBLC")
  tables["CBDT"] = cbdt_bytes if cbdt_bytes
  tables["CBLC"] = cblc_bytes if cblc_bytes

  sfnt = tables.key?("CFF ") || tables.key?("CFF2") ? 0x4F54544F : 0x00010000
  FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
end

#safe_cbdt_sourceStitcher::Source?

Safe variant: returns nil instead of raising when there are multiple CBDT sources. Used inside the per-subfont glyph-copy pass where raising mid-compile would leave partial state.

Returns:



52
53
54
55
56
# File 'lib/fontisan/stitcher/cbdt_propagator.rb', line 52

def safe_cbdt_source
  cbdt_source
rescue MultipleCbdtSourcesError
  nil
end