Class: Fontisan::Stitcher::CbdtPropagator
- Inherits:
-
Object
- Object
- Fontisan::Stitcher::CbdtPropagator
- 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
-
#add_placeholder_glyphs(source, target) ⇒ Object
Inject empty CBDT placeholder glyphs from
sourceinto the target UFO. -
#cbdt_source ⇒ Stitcher::Source?
The single CBDT source, or nil if there is none.
-
#initialize(sources) ⇒ CbdtPropagator
constructor
A new instance of CbdtPropagator.
-
#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. -
#safe_cbdt_source ⇒ Stitcher::Source?
Safe variant: returns nil instead of raising when there are multiple CBDT sources.
Constructor Details
#initialize(sources) ⇒ CbdtPropagator
Returns a new instance of CbdtPropagator.
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.
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_source ⇒ Stitcher::Source?
Returns the single CBDT source, or nil if there is none.
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.
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_source ⇒ Stitcher::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.
52 53 54 55 56 |
# File 'lib/fontisan/stitcher/cbdt_propagator.rb', line 52 def safe_cbdt_source cbdt_source rescue MultipleCbdtSourcesError nil end |