Skip to content

Font Glyph Pruning

Problem

Custom fonts uploaded to creatives can be very large (8+ MB for fonts with full Unicode coverage). Ad networks and publishers may block or remove creatives that exceed size thresholds. A font with 10,000+ glyphs where the creative only uses Norwegian text is massively wasteful.

Existing Infrastructure (unused)

Creative-Composer already has a complete font optimization pipeline that was built but never wired into the build job:

Files

  • Creative-Composer/src/font-optimizer/index.ts -- core functions
  • Creative-Composer/src/bin/commands/job/fontopt.ts -- job wrapper (never imported)
  • Creative-Composer/src/bin/commands/job.ts -- build pipeline (passes fontData: null)

What's already built

  1. optimizable(creativeJson) -- checks if creative text is predictable. Returns false if creative has:

    • Any Tag block (custom HTML with unpredictable text)
    • Any TextInput with non-number validation (user can type anything)
  2. extractCharset(creativeJson) -- walks all components, collects every character from:

    • Block text content
    • Ad label text
    • Creative name
    • Adds number literals if any number-only input exists
  3. createFontSubset(source, target, include) -- uses subset-font (npm, harfbuzz WASM) to create a WOFF2 containing only the needed glyphs

  4. convertToWoff2(source, target) -- converts WOFF to WOFF2 using fontverter for non-optimizable creatives

  5. Tag injection -- injectTag.ts already has a CUSTOMFONTDATAPLACEHOLDER replacement that accepts base64 font data. Currently always receives null.

Why it's not active

The fontopt function in fontopt.ts is exported but never imported in job.ts. The build pipeline skips font optimization entirely. Likely was built as a prototype and never completed/tested.

What's Needed to Activate

Minimum viable activation

  1. Import fontopt in job.ts
  2. Call it between remapping and building, pass the font URL from creative JSON
  3. Pass the resulting base64 font data to injectTag instead of null
  4. Verify the engine's FontHelper.ts can load the inline font data (or the tag stub injects it correctly)

Gaps in current implementation

  • extractCharset is incomplete: only reads payload.text, doesn't cover:

    • Button labels
    • Form placeholder text, validation messages, submit button text
    • Slider navigation labels
    • Conversation response options
    • Any text set via creativeProperties (e.g. close button label)
    • Dynamic content from feeds/DCO (if implemented in the future)
  • optimizable is conservative: bails out entirely for Tag blocks and free-text inputs. Could instead fall back to a broader "safe" character set (full Latin + common punctuation) instead of skipping optimization.

  • No safety net: the charset extraction doesn't include a baseline like U+0020-007E (ASCII) + U+00C0-00FF (Latin-1 Supplement for ÆØÅ etc.). If any character is missed, the font renders with fallback -- visible to end users.

  • Multiple fonts per creative: current code assumes one font. Creatives can have different fonts per block.

  • No flow awareness: charset extraction reads all components but doesn't account for flow branches where different text appears.

Always include regardless of extracted charset:

U+0020-007E  -- ASCII (A-Z, a-z, 0-9, punctuation)
U+00C0-00FF  -- Latin-1 Supplement (ÆØÅ, accented chars)
U+2013-2014  -- en-dash, em-dash
U+2018-201D  -- smart quotes
U+2026       -- ellipsis
U+20AC       -- euro sign

This covers Norwegian, Swedish, Danish, German, French, Spanish, and most Western European text. Adds ~200 glyphs at most -- negligible size impact vs. the 10,000+ in a full CJK font.

Expected Impact

  • Full CJK/Arabic font: 8+ MB to 30-80 KB (99%+ reduction)
  • Large Latin font: 500 KB to 20-40 KB (90%+ reduction)
  • Already-small Latin font: 30 KB to 15-20 KB (modest, but WOFF2 conversion alone helps)

Next Steps

  1. Audit extractCharset -- verify all text sources in a real creative JSON are covered
  2. Add safety net charset -- always include Latin base set
  3. Wire fontopt into job.ts -- the actual activation
  4. Test with real creatives -- compare file sizes before/after, verify no missing glyphs
  5. Handle multiple fonts -- iterate per font, not per creative
  6. Consider broader fallback for Tag/TextInput -- use full Latin set instead of skipping entirely

Internal documentation