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
109
110
111
# File 'lib/fontisan/stitcher/cbdt_propagator.rb', line 82

def add_placeholder_glyphs(source, target)
  @placeholder_names = {}

  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)
    @placeholder_names[gid] = name
  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 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 rebuilt with compiled-font GIDs, and rewrites the file.

The CBDT/CBLC rebuild uses Subset::TableStrategy::ColorBitmapSubsetter to remap every bitmap block from source GID to compiled GID. This ensures CBLC's IndexSubTableArray references the compiled font's actual GIDs, not the source's.

Falls back to raw-byte copy when the GID mapping can't be resolved (no placeholder names recorded, or compiled post table is v3.0 with no glyph names).

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

Parameters:

  • source (Stitcher::Source, nil)

    the CBDT source

  • path (String)

    compiled font file to rewrite



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/fontisan/stitcher/cbdt_propagator.rb', line 134

def propagate_tables_into(source, path)
  return unless source

  compiled = FontLoader.load(path)

  tables = {}
  compiled.table_names.each do |tag|
    raw = compiled.table_data[tag]
    tables[tag] = raw if raw
  end

  rebuilt = rebuild_color_tables(source, compiled)
  tables["CBDT"] = rebuilt[:cbdt] if rebuilt[:cbdt]
  tables["CBLC"] = rebuilt[:cblc] if rebuilt[:cblc]

  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