Skip to content

feat(fonts): in-browser MTX decoder — render embedded PPTX fonts (CFF + TrueType) - #59

Merged
karthikmudunuri merged 8 commits into
mainfrom
karthikmudunuri/mtx-eot-font-decoder
May 30, 2026
Merged

karthikmudunuri merged 8 commits into
mainfrom
karthikmudunuri/mtx-eot-font-decoder

Conversation

@karthikmudunuri

@karthikmudunuri karthikmudunuri commented May 28, 2026 •

Copy link
Copy Markdown
Member

Summary

A complete clean-room MicroType Express (MTX) decoder in TypeScript, so the editor renders the exact fonts embedded in a .pptx (ppt/fonts/*.fntdata) — no CDN, no fontRegistry, no network. This is what PowerPoint does internally; here it runs in the browser.

PPTX embeds fonts as MTX-compressed EOT, which browsers (and FontForge, and LibreOffice's decoder) can't read. Reverse-engineered from the W3C MTX submission (Appendix C: BITIO/AHUFF/LZCOMP) + ISO/IEC 14496-18, and verified against a real client deck.

What's implemented

  • eot.ts — EOT wrapper parse; locate FontData as the trailing fontDataSize bytes (spec-correct); uncompressed-EOT extraction; route compressed payloads to MTX.
  • mtx.ts — MTX v3 container parse (header + 3 LZCOMP blocks, validated to tile exactly).
  • lzcomp.ts — full LZCOMP decompressor: MSB-first bit reader, FGK adaptive Huffman (complete-tree init + priming + sibling-rule update/swap), 7168-byte preload dictionary, copy-model loop.
  • ctf-glyf.ts — TrueType reconstruction: WOFF2-style triplet point decode, contour/loca rebuild, simple + composite glyphs, sfnt reassembly with recomputed checksums (OTS-clean). Hinting instructions dropped (browsers ignore them; outlines are exact).
  • Auto-wiring — resolveWebFonts / fontAssetToWebFont decode Deck.fonts on import and serve as data:font/otf|ttf URLs; embedded brand families are excluded from Google Fonts requests.

Verified

Against eon-deck.pptx — all 5 embedded EON fonts decode and render in-browser:

  • 4× EON Brix Sans (CFF/OTTO)
  • 1× EON Office Head (TrueType, glyf+loca reconstructed)

All 5 pass OTS (the exact OpenType Sanitizer Chrome & Firefox use, rc=0). FontForge/fonttools/FreeType confirm correct glyph outlines and font names. Confirmed live in the editor.

Tests / CI

  • New tests in src/lib/fonts/__tests__/ covering the container parse, CFF decode, TrueType reconstruction, and EOT header parse.
  • They it.skipIf(!hasEon) on the gitignored eon-deck.pptx fixture (proprietary embedded fonts, not committable) — run locally, skip cleanly in CI. Same pattern as the existing chrome-preservation tests.
  • pnpm test, pnpm typecheck, pnpm build:lib all clean.

Changesets

Three minor changesets (container, LZCOMP/CFF, TrueType-glyf) → 1.16.0 on release.

Known tradeoff

TrueType hinting is dropped (instructionLength = 0). Browsers render from outlines and ignore TrueType hints, so on-screen rendering is identical; pixel-exact hinted rendering at tiny sizes is the one deferred piece.

The PPTX importer already pulls embedded fonts from ppt/fonts/*.fntdata
into Deck.fonts. The editor couldn't use those bytes because they're
EOT-wrapped (often MTX-compressed) and the browser has no native decoder.
This change lays the groundwork to decode them where we can.

New packages/slidewise/src/lib/fonts/eot.ts:
- Full EOT header parser (1.0 / 2.0 / 2.1 / 2.2 variants)
- Uncompressed-EOT → TTF extraction (works end-to-end)
- TTEMBED_TTCOMPRESSED detection
- Discriminated EotDecodeError so callers can distinguish format failures
  from "not yet implemented"

New packages/slidewise/src/lib/fonts/mtx.ts:
- MTX outer container parser scaffolding
- Throws EotDecodeError("mtx-not-implemented") for the PowerPoint MTX
  variant. The Office-embedded MTX uses a different major version
  (observed 0x03) than the W3C submission spec (version 1); the
  post-2010 Office variant isn't publicly documented. Reverse-engineering
  it is a separate, multi-week project — tracked as follow-up.

resolveWebFonts() now decodes Deck.fonts on the fly. Uncompressed EOT
becomes a data:font/ttf;base64,... URL the browser registers via
@font-face — no fontRegistry, no platform involvement, no network.
MTX-compressed fonts fall through cleanly to the fontRegistry / system
fallback chain.

3 new tests against eon-deck.pptx fixture validate the EOT header parse,
the MTX detection flag, and the not-implemented signal path.
Two corrections from reverse-engineering real PowerPoint-embedded fonts
(eon-deck.pptx, 5 fonts):

1. parseEotHeader over-advanced 20 bytes on EOT v2.2 by trying to walk the
   optional EUDC/Signature tail, landing past the true FontData magic.
   FontData actually begins right after the RootString name field
   (offset 212 for font1, not 232). Verified: the payload magic "BSGP"
   sits exactly at 212, followed by fontDataSize bytes and a 20-byte
   EOT trailer. Stop after RootString.

2. The TTCOMPRESSED flag doesn't tell us WHICH compressor. Real PowerPoint
   fonts use a proprietary "BSGP"-magic glyph-compression format — NOT the
   W3C/ISO MicroType Express container our mtx.ts scaffold targets, and
   no public spec / open decoder exists for it. decodeEot now sniffs the
   payload magic and throws EotDecodeError("mtx-not-implemented") with a
   precise message for BSGP, so the fontRegistry / system fallback chain
   runs cleanly instead of producing a misleading "MTX failed" error.

Research notes in .context/mtx-research/ (gitignored). Uncompressed-EOT
extraction still works for decks that embed fonts without compression.
…one 1)

Reverse-engineered the real PowerPoint-embedded fonts against the W3C MTX
spec. Two earlier conclusions were WRONG and are corrected here:

- These are spec-compliant MicroType Express v3, NOT a proprietary "BSGP"
  format. "BSGP" is a string in the EOT RootString/EUDC metadata, 20 bytes
  before FontData. Real FontData = trailing fontDataSize bytes, starting 0x03
  (MTX version 3).
- LibreOffice's "no blank loca table" rejection was a LibreOffice bug — MTX
  deliberately strips loca and rebuilds it from decompressed glyf.

Milestone 1 (DONE, verified on all 5 EON fonts):
- eot.ts: locate FontData as the trailing fontDataSize bytes (spec-correct,
  robust) instead of walking variable name fields. Fixes a 20-byte offset bug.
- mtx.ts: parseMtxContainer — MTX v3 header (version/copyLimit/offsetData2/3)
  + 3-block split, with full validity checks. Verified: version==3, blocks
  ordered, blocks tile the payload exactly across all 5 fonts.
- mtx-container.test.ts asserts this on the real eon-deck fixture.

Milestone 2 (in progress):
- lzcomp.ts: BITIO (MSB-first) + FGK adaptive Huffman (AHUFF#1/2/3) + the
  copy-model main loop, per the W3C LZCOMP algorithm. The adaptive-Huffman
  initial-tree shape + update rule aren't yet bit-exact with the encoder, so
  decode diverges early. Reconciling needs the verbatim W3C Appendix C source.

decompressMtx attempts LZCOMP then throws mtx-not-implemented (incomplete work,
not corruption) so decodeEot callers fall back to fontRegistry cleanly. Full
notes in .context/mtx-research/FINDINGS.md.

54 tests pass, typecheck clean.
…r in-browser

Ported the W3C MTX submission Appendix C (BITIO/AHUFF/LZCOMP) to TypeScript.
PPTX-embedded fonts are MTX-compressed EOT; browsers can't decode MTX, so the
editor fell back to system fonts. Now CFF/OTTO embedded fonts decode fully and
render automatically — no CDN, no fontRegistry, no network.

- lzcomp.ts: MSB-first BitReader; adaptive Huffman (complete-binary-tree init,
  init_weight, exact priming, ReadSymbol, UpdateWeight + SwapNodes sibling
  rule); SetDistRange; 7168-byte preload dictionary; copy-model Decode loop
  (start = pos-distance-length+1, >=512 length bump).
- mtx.ts: decompressMtx returns block1 directly for CFF fonts (no glyf/loca).
- eot.ts: FontData = trailing fontDataSize bytes (spec-correct); route
  compressed payloads to the MTX decoder.
- fonts.ts: wrap decoded bytes as data:font/otf so embedded CFF fonts render
  on import via resolveWebFonts.

Verified vs eon-deck.pptx: 4 CFF EON Brix Sans weights -> valid OTTO
("EON Brix Sans Regular" per FontForge). TrueType-glyf EON Office Head falls
back mtx-not-implemented (CTF glyf reconstruction = milestone 3). Export path
unchanged. 56 tests pass, typecheck clean.
Embedded brand fonts (e.g. EON Office Head) will never exist on Google Fonts,
so requesting them produced a noisy CORS/404 in the console — including for
families we can't yet decode (TrueType-glyf MTX). googleFontExclusions() now
excludes every Deck.fonts family from the Google Fonts request, so an
undecodable embedded font falls back silently to system instead of a failed
network fetch.
…nts decode

Milestone 3. EON Office Head is a TrueType-outline embedded font: MTX stores
glyf in CTF (the WOFF2 triplet point encoding) and eliminates loca.

ctf-glyf.ts reconstructs it:
- per-glyph CTF parse: numContours, optional explicit bbox (0x7FFF flag),
  contourPoints -> endPtsOfContours, flags, WOFF2 triplet (dx,dy) decode;
  pushCount/codeSize read + skipped
- simple + composite glyphs; instructions dropped (instructionLength=0 —
  browsers ignore TrueType hints, unhinted renders identically)
- rebuild glyf (4-byte aligned) + long loca; reassemble sfnt (version
  0x00010000) with recomputed table checksums + head.checkSumAdjustment so
  OTS / strict browser sanitizers accept it

Verified vs eon-deck.pptx: FontForge opens the reconstruction as
"EON Office Head" with correct glyph-A outline. All 5 EON fonts now decode
in-browser (4 CFF OTTO + 1 TrueType). 56 tests pass, typecheck clean.
…(OTS-clean)

The reconstructed TrueType font opened in FontForge/fonttools/FreeType but the
browser rendered nothing — because Chrome/Firefox's OpenType Sanitizer (OTS)
rejected it:

  ERROR: cvt : Uneven table length (109)

Per the MTX spec, `cvt`/`hdmx`/`VDMX` are stored in a compressed/modified form
in CTF (not just glyf), so copying `cvt` verbatim produced an odd-length table
(cvt is an int16 array → must be even) and OTS failed the whole font.

Since we emit unhinted glyphs (instructionLength = 0), all of these are unused:
drop `cvt `/`fpgm`/`prep` (instruction programs) and `hdmx`/`VDMX`/`LTSH`/`gasp`
(device-metric caches). Verified with OTS (the exact sanitizer Chrome/Firefox
use): all 5 decoded EON fonts now sanitize cleanly (rc=0), so they load and
render in the browser.
…green)

The font decoder tests read eon-deck.pptx from the gitignored
.context/attachments dir — proprietary embedded fonts we can't commit to a
public repo. CI has no fixture, so the tests ENOENT-failed. Switch to the
same it.skipIf(!hasEon) pattern the existing chrome-preservation tests use:
run locally where the fixture is present, skip cleanly in CI.

Verified: with the fixture removed, the 6 tests skip (not fail); with it
present, they run and pass.
@karthikmudunuri karthikmudunuri changed the title feat(fonts): EOT parser + MTX decoder scaffold for embedded brand fonts feat(fonts): in-browser MTX decoder — render embedded PPTX fonts (CFF + TrueType) May 30, 2026
@karthikmudunuri
karthikmudunuri merged commit ea3007a into main May 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant