Releases: textcortex/SlideWise
@textcortex/slidewise@1.17.0
Minor Changes
-
019e000: feat(pptx): support PowerPoint templates (.potx)
.potxand.pptxshare an identical OOXML package; only the main part's
content type in[Content_Types].xmldiffers. This adds first-class template
support across import and export:parsePptxalready parsed.potxtransparently (it reads parts by path, not
by content type) — now the rest of the pipeline preserves template-ness.- New exported
isPptxTemplate(blob)detects a template by inspecting the
package content type rather than trusting a filename extension (a mis-named
.pptxthat is really a template is detected correctly). serializeDeckgains anasTemplate?: booleanoption. When omitted,
template-ness is inherited from the source archive, so a parsed.potx
round-trips back to a.potx; passtrue/falseto force the output kind.
Templates are emitted with the…presentationml.template.main+xmlmain-part
content type and the.potxMIME type.
@textcortex/slidewise@1.16.4
Patch Changes
-
a085c8d: fix(pptx): import-fidelity fixes for think-cell / brand-template decks
- Skip shapes flagged
hidden="1"(e.g. think-cell "do not delete" data objects) - Render run-level text highlight (
<a:rPr><a:highlight>) end to end - Apply
cap="all"/"small"(including when inherited from a placeholder list style) as a render-time letter-case transform - Derive font weight from weight-named families ("Gilroy ExtraBold" → 800, "… Medium" → 500, …) so substitute fonts render at the right heaviness
- Tables: per-cell fills, text colours, per-side borders, proportional column widths / row heights, cell spans (
gridSpan/hMerge/rowSpan/vMerge), per-cell vertical anchor, and rich per-cell runs (highlight / bold / ✓ glyphs / bullet line breaks). Unfilled cells stay transparent instead of inheriting a sibling fill - Map Wingdings bullet glyphs to Unicode (
ü→✓,q→☐,§→▪, …) - Bullets: repeat a character bullet across in-paragraph line breaks, suppress the glyph on empty paragraphs, and trim trailing empty paragraphs
- Synthesise block-arrow paths (
down/up/left/rightArrow) and resolve outline colour from<p:style><a:lnRef>so dashed/outlined shapes draw - Keep a text-bearing preset or custom-geometry shape's fill, border, and corner radius behind its text (roundRect callouts, outlined chevrons)
- Honour
<a:bodyPr><a:spAutoFit>no-wrap for short single-line labels; skip the arrow-tip text inset on no-fill label shapes - Render per-paragraph hanging-indent bullets as one block per line so multi-line items align correctly
- Skip shapes flagged
@textcortex/slidewise@1.16.3
Patch Changes
-
80e1b4e: Fix several PPTX import-rendering fidelity gaps surfaced by real-world decks:
- Picture/SVG fills on shapes — shapes whose fill is an
<a:blipFill>(the modern Office "icon" pattern, including dual PNG+SVG blips) now render their artwork. Previously thesecustGeomicons (globes, stars, grid textures, brand marks) imported with no fill and showed blank. The image is painted clipped to the shape's silhouette, or as a box-filling background for rect/rounded/circle shapes. - Empty picture placeholders — empty picture placeholders inherited from the slide layout no longer leak onto the slide as grey "Insert Picture" prompt boxes. Picture placeholders the slide actually hosts now inherit their rounded geometry and fill from the layout/master so they render as the template intends.
- Embedded fonts (EOT / MicroType-Express) — embedded
.fntdatafonts now decode to browser-valid TTFs. Two bugs were fixed: composite glyphs that carriedWE_HAVE_INSTRUCTIONSon a non-first component produced a malformedglyftable, and format-12cmapsubtables shipped a non-zerolanguagefield — both caused the browser's font sanitizer (OTS) to reject the whole font and fall back to a system typeface. - Weight-named font families — weight-named embedded families (e.g. "Montserrat Bold", "Montserrat Semi-Bold") are now also aliased to their base family at the matching numeric weight, so bold/semi-bold text bound to the base family renders with the real embedded face instead of a synthetic bold.
- Picture/SVG fills on shapes — shapes whose fill is an
@textcortex/slidewise@1.16.2
Patch Changes
-
71f96cb: fix(pptx): resolve theme colours when persisting verbatim custGeom, so brand-coloured vectors qualify for cross-process replay
The cross-process verbatim-replay fix (1.16.1) only stamped a custGeom shape's source
<p:sp>into the deck JSON when the XML was fully self-contained — and it excluded anything referencing a theme colour (<a:schemeClr>). Brand marks are almost always filled with a theme accent (e.g. E.ON red isschemeClr val="accent2"), so the very shapes this was meant to fix (the bicycle) were skipped and fell back to the lossy synth path — still blank.The importer now resolves
<a:schemeClr>references to literal<a:srgbClr>against the slide's theme before persisting, instead of bailing. Both elements accept the same child transforms (lumMod,alpha, …) so the swap is lossless — only the colour source changes from a theme reference to a baked hex, making the fragment valid without the source theme. Shapes that still reference media (r:embed/r:id/r:link) or carry a colour token absent from the theme remain on the synth path.
@textcortex/slidewise@1.16.1
Patch Changes
-
65eeac2: fix(pptx): carry verbatim custGeom OOXML in the deck JSON so vector shapes survive cross-process serialize
The high-fidelity replay of imported elements relies on two module-global registries (
sourceBufferCache,elementSourceRegistry) populated only byparsePptxand never written to the deck JSON. In a pipeline that parses in one process and serializes in another (parse client-side → store deck JSON → serialize server-side), those registries are empty, so every element is re-synthesised from its deck fields. Synthesis can't represent OOXML even-odd / winding, so complexcustGeomvectors (e.g. a bicycle diagram) render blank even though simpler ones (the brand logo) happen to survive.The importer now stamps the verbatim
<p:sp>of self-contained custGeom shapes (nor:embed/r:id/a:schemeClrreferences) onto the element aspristineOoxml = { xml, snapshot }, which rides along in the deck JSON. On serialize, an unedited such shape (snapshot still matches) is replayed verbatim — exact source geometry and winding — instead of being re-synthesised; itscNvPr/@idis rewritten to avoid spTree collisions. Edited shapes fall back to synthesis. This is the same persist-in-JSON pattern already used for embedded fonts (deck.fonts), scoped to vector shapes to keep JSON bloat negligible (~a few KB per deck).
@textcortex/slidewise@1.16.0
Minor Changes
-
ea3007a: Begin the MTX → TTF decoder for PPTX-embedded fonts.
PPTX stores embedded fonts as MTX-compressed EOT inside
ppt/fonts/*.fntdata. PowerPoint decodes them natively; browsers can't, which is why editor previews fall back to system fonts even whenparsePptxextracted the bytes intoDeck.fonts. This change lays the groundwork:New
packages/slidewise/src/lib/fonts/eot.ts- Full EOT wrapper parser — header, flags, variable-length name fields, version 1.0 / 2.0 / 2.1 / 2.2 tail variants
- Uncompressed-EOT extraction → ready-to-register TTF/OTF bytes
- MTX detection via the
TTEMBED_TTCOMPRESSEDflag EotDecodeErrorwith discriminatedkindso callers can distinguish "truncated", "magic-mismatch", "mtx-not-implemented", "mtx-failed"
New
packages/slidewise/src/lib/fonts/mtx.ts- MTX outer container parser scaffolding
- Recognises but does not yet decompress the PowerPoint MTX variant (Office-embedded fonts use a different major version than the W3C MTX submission spec; the post-2010 Office variant isn't publicly documented).
- Throws
EotDecodeError("mtx-not-implemented")for unsupported sub-methods so the fallback chain (Deck.webFonts → fontRegistry → system fonts) runs cleanly. No noisy console errors — diagnostic only whenwindow.__slidewiseFontDebug = true.
Auto-wiring through
resolveWebFonts()The font loader now decodes
Deck.fontson the fly. When a font is uncompressed EOT (~30% of real-world embedded fonts), we synthesise adata:font/ttf;base64,…URL and register it via@font-face— nofontRegistryneeded, no platform involvement. Brand-embedded fonts that use MTX glyph compression (the EON case, most enterprise decks) still needfontRegistryfor editor preview, but the export path still embeds the original MTX bytes verbatim.What still needs to happen for full coverage
A real MTX decompressor for the Office variant. Either:
- Reverse-engineering the format against a test corpus, or
- A WebAssembly port of FontForge's GPL'd
parsettf.cMTX path
Both are multi-week projects. Tracked as a follow-up.
Tests
3 new tests in
src/lib/fonts/__tests__/eot.test.tsagainst the realeon-deck.pptxfixture:- EOT header parser succeeds on every embedded font (5 entries)
isMtxCompressed()correctly reports the EON fonts as MTXdecodeEot()returnsEotDecodeError.kind === "mtx-not-implemented"for MTX-flagged fonts (so the caller's fallback fires)
No public API changes.
FontAsset,WebFontAsset, and the rest of the font surface are untouched. Additive. -
ea3007a: Complete the in-browser MTX decoder: TrueType-glyf font reconstruction.
Milestone 2 decoded CFF/OTTO embedded fonts. This adds TrueType-outline fonts: MTX stores
glyfin Compact Table Format (the WOFF2 triplet point encoding) and stripsloca.ctf-glyf.tsreconstructs a standardglyf+locaand reassembles the sfnt with recomputed table checksums +head.checkSumAdjustment(so strict browser sanitizers accept it). TrueType hinting instructions are dropped (browsers ignore them; unhinted outlines render identically on screen). Simple and composite glyphs are handled.Verified against
eon-deck.pptx: all 5 embedded EON fonts now decode in-browser — the 4 CFF EON Brix Sans weights (OTTO) and the TrueType EON Office Head (FontForge confirms the font name and correct glyph outlines). No CDN, nofontRegistry, no network: embedded PPTX fonts render exactly and automatically on import. -
ea3007a: Decode CFF embedded PowerPoint fonts in-browser via a clean-room MTX (MicroType Express) decompressor.
PPTX embeds fonts as MTX-compressed EOT in
ppt/fonts/*.fntdata. Browsers can't decode MTX, so editor previews fell back to system fonts even though the importer extracts the bytes intoDeck.fonts. This ports the W3C MTX submission (Appendix C: BITIO / AHUFF / LZCOMP) to TypeScript so the editor renders the real embedded typeface — no CDN, nofontRegistry, no network.lib/fonts/lzcomp.ts— full LZCOMP decompressor: MSB-first bit reader, adaptive Huffman (complete-tree init + priming + sibling-rule update/swap), 7168-byte preload dictionary, copy-model loop.lib/fonts/mtx.ts— MTX v3 container parse +decompressMtx: for CFF/OTTO fonts, block 1 decompresses to the complete font and is returned directly.lib/fonts/eot.ts— locates FontData as the trailingfontDataSizebytes (spec-correct); routes compressed payloads through the MTX decoder.resolveWebFonts/fontAssetToWebFontwrap the decoded bytes as adata:font/otfURL, so embedded CFF fonts render automatically on import.
Verified against
eon-deck.pptx: the 4 CFF EON Brix Sans weights decode to valid OTTO fonts (FontForge confirms "EON Brix Sans Regular"). TrueType-glyf fonts (EON Office Head) fall back withmtx-not-implemented— CTF glyf reconstruction is the remaining milestone. Export is unchanged (original.fntdatabytes still round-trip to PPTX).
@textcortex/slidewise@1.15.4
Patch Changes
-
03b71b7: fix(pptx): custGeom export — map path coords to the shape's EMU extent and drop the bogus
fill="darken"Two correctness issues in
svgPathToOoxml(custGeom emission):-
Wrong
fill="darken"on even-odd paths. OOXML's<a:path fill="…">is a shading hint (none / norm / lighten / darken), not a winding rule — custGeom has no even-odd flag at all. Emittingfill="darken"forfillRule: "evenodd"silently darkened the shape and tripped some renderers (LibreOffice) without ever producing the hole. We now leave the defaultnormshading; holes are carried by the subpath directions already encoded ind. -
Path coordinate space didn't match the shape box.
<a:path w/h>was emitted at the source viewBox dimensions while the points stayed in that space. PowerPoint itself emits custGeom withw/hequal to the shape's EMU extent, and LibreOffice only maps the path onto the shape correctly when the two line up.svgPathToOoxmlnow takes the target EMU extent and rescales the points so<a:path w/h>matches the shape — improving cross-renderer fidelity for vectors whose viewBox differs from their box.
-
@textcortex/slidewise@1.15.3
Patch Changes
-
0343bca: fix(pptx): vector shapes no longer render blank — gradient paint servers, full-circle arcs, and embedded brand fonts
Three independent fidelity bugs that made imported brand decks render blank or fall back to system fonts:
-
Gradient fills on vector shapes rendered blank. SVG
<path>/<polygon>fill=cannot take a CSSlinear-gradient(...)/radial-gradient(...)string, so any custGeom silhouette or triangle/diamond/star carrying a gradient fill painted nothing. The renderer now builds an SVG paint server (<linearGradient>/<radialGradient>, including#RRGGBBAAalpha →stop-opacity) and references it viafill="url(#…)". Solid /transparent/url()fills are unchanged. Applies to shape paths, the polygon presets, and text backing paths. -
Full-circle custGeom arcs (e.g. bicycle wheels) imported blank. A 360°
<a:arcTo>was converted to a single SVG elliptical-arc whose end point equals its start — which the SVG spec renders as nothing, so wheels/rings vanished. The importer now splits any arc sweep into ≤120° segments, so full circles render. -
custGeom arcs downgraded to a rect on export.
svgPathToOoxmlbailed on SVGAcommands, collapsing the whole shape to aprstGeomrect (invisible on line-art). Arcs are now approximated as cubic Béziers (≤90° segments, sub-pixel error) and emitted as<a:cubicBezTo>, so arc-bearing vectors survive export. -
Embedded brand fonts fell back to Calibri in the editor. The importer populated
Deck.fonts(the PPTX-embedded payload) but neverDeck.webFonts, so the canvas had nothing browser-renderable to paint with. It now sniffs each embedded font's signature and, when it's a browser-loadable SFNT/WOFF (which is what PowerPoint embeds), surfaces aWebFontAssetso the real typeface renders. Non-renderable payloads are skipped (no regression).
-
@textcortex/slidewise@1.15.2
Patch Changes
-
b3f083a: fix(pptx): preserve groups (and the custGeom / radial-gradient children inside them) on import instead of flattening
The PPTX importer flattened
<p:grpSp>on the way in — it parsed the group's children and spliced them in as loose top-level elements, so noGroupElementever reached the editor. The group structure was lost, and because the flattened children were registered with their own child-coordinate-space<p:sp>XML, they re-injected at the slide's top level with the wrong coordinates on round-trip. custGeom logos and radial-gradient panels — which in real decks almost always live inside groups — went down with the group. The schema, renderer, and export writer already supported all three; only the importer dropped them.The importer now builds a real
GroupElement(children keep slide-absolute coordinates, z re-stamped in document order, bounding box from the group's<a:xfrm>with a child-union fallback) and registers the whole<p:grpSp>for verbatim replay, so an unedited group round-trips byte-for-byte.snapshotElementnow recurses into group children, so editing any descendant flips the group off verbatim-replay onto the synth path (which re-emits custGeom, gradients, and nested groups) rather than re-emitting stale source XML.Deferred (unchanged from before): text/image children inside an edited group still round-trip lossy through the synth path, and group-level in-canvas selection/drag remains a follow-up.
@textcortex/slidewise@1.15.1
Patch Changes
-
d2529c3: fix(pptx): preserve per-stop and solid-fill alpha from 8-digit hex colors
parseFilltruncated#RRGGBBAA/#RGBAcolors to 6 digits viahexBare,
dropping the alpha channel before it could reach<a:alpha>. Translucent
gradient stops (and flat translucent fills) were therefore serialized opaque.
parseFillnow extracts alpha from 4- and 8-digit hex, so gradient stops carry
their<a:alpha>and solid shape fills map alpha to pptxgenjstransparency.