Repository navigation
feat(fonts): in-browser MTX decoder — render embedded PPTX fonts (CFF + TrueType) - #59
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, nofontRegistry, 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 trailingfontDataSizebytes (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).resolveWebFonts/fontAssetToWebFontdecodeDeck.fontson import and serve asdata:font/otf|ttfURLs; embedded brand families are excluded from Google Fonts requests.Verified
Against
eon-deck.pptx— all 5 embedded EON fonts decode and render in-browser: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
src/lib/fonts/__tests__/covering the container parse, CFF decode, TrueType reconstruction, and EOT header parse.it.skipIf(!hasEon)on the gitignoredeon-deck.pptxfixture (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:liball clean.Changesets
Three
minorchangesets (container, LZCOMP/CFF, TrueType-glyf) →1.16.0on 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.