Skip to content

fix(pptx): preserve masters/layouts/theme/fonts/bg + EMF fallback on save#45

Merged
karthikmudunuri merged 2 commits into
mainfrom
karthikmudunuri/latest-package-releases
May 13, 2026
Merged

fix(pptx): preserve masters/layouts/theme/fonts/bg + EMF fallback on save#45
karthikmudunuri merged 2 commits into
mainfrom
karthikmudunuri/latest-package-releases

Conversation

@karthikmudunuri
Copy link
Copy Markdown
Member

Summary

Three stacked regressions caused client decks to lose chunks of visual content after a single text edit. Verified end-to-end against Dickinson_Sample_Slides.pptx and eon-deck.pptx.

1. Slide masters, layouts, theme, embedded fonts dropped on save

pptxgenjs regenerates its own ppt/slideMasters/, ppt/slideLayouts/, ppt/theme/, and never emits ppt/fonts/. Anything that lived on the master / layout — backgrounds, brand bars, gradients, page numbers, footers, theme palettes, embedded brand fonts — disappeared.

preserveUnknowns now copies the source's chrome (also notesMasters, handoutMasters, tags) into the generated zip, splices the source's <p:sldMasterIdLst> / <p:notesMasterIdLst> / <p:embeddedFontLst> into presentation.xml, rewrites presentation.xml.rels and each slide's rels to point at the original layouts, updates [Content_Types].xml, and copies referenced media (renamed on collision with pptxgenjs's own media). Bails when source/output slide-size aspect ratios differ so 4:3 sources don't get their chrome stretched onto a 16:9 canvas.

2. Slide-level <p:bg> collapsed to flat hex

pptxgenjs's slide.background only emits a <a:solidFill> with a hex color, so theme refs / gradients / image-fill backgrounds were collapsing (e.g. <a:schemeClr val=\"tx1\"/> became <a:srgbClr val=\"151515\"/>).

A new per-slide pass copies the source slide's <p:bg> element verbatim, rewriting r:embed / r:link for image-fill backgrounds (with media copy) using the same machinery as injectIntoSlide. When the source slide has no explicit bg, the pptxgenjs flat-hex stand-in is stripped so layout / master inheritance can do its job.

3. EMF/WMF decode failures wiped the entire slide

parsePic returned null on metafile decode failure. Combined with upstream catches, a single un-decodable <p:pic> could wipe every other element on the same slide (Dickinson sample slides 2, 3, 9 — title + subtitle + logo all gone after one text edit on a different slide). The fallback now returns an UnknownElement so the source <p:pic> is re-injected verbatim and the EMF reference survives for PowerPoint to render natively.

Validation

Run on the two real attached decks (.context/attachments/):

Deck Before After
Dickinson_Sample_Slides.pptx (4:3) slides 2/3/9 had empty spTree; 5/9 broken 9/9 slides retain content; slide 2's <a:schemeClr val=\"tx1\"/> theme bg survives
eon-deck.pptx (16:9) 1 layout, 0 fonts, 1 theme on save 28 layouts, 5 embedded fonts, 3 themes preserved

Changeset

Minor bump on @textcortex/slidewise — see .changeset/preserve-chrome-and-emf-fallback.md.

Test plan

  • pnpm typecheck clean
  • pnpm --filter @textcortex/slidewise test — 37/37 passing (2 new fixture-driven tests in chrome-preservation.test.ts)
  • End-to-end import → save against both attached decks; saved files at /tmp/*.saved.pptx if you want to eyeball in PowerPoint before merge
  • Reviewer: open the two saved files in PowerPoint and confirm Dickinson slide 2's black theme bg + eon-deck's brand fonts/layouts render as expected

Known not-yet-fixed

  • 4:3 chrome preservation — Dickinson masters are still skipped due to the aspect-ratio guard. The clean fix is to drive output <p:sldSz> from the source and inverse-fit editor coords to source-canvas space — bigger refactor that touches the importer's fit + pxToInches. Tracking as follow-up.

…save

Three stacked regressions caused client decks to lose chunks of content
after a single text edit:

* pptxgenjs regenerates its own slideMasters/, slideLayouts/, theme/ and
  never emits fonts/. preserveUnknowns now copies the source's chrome
  (incl. notesMasters, handoutMasters, tags) into the generated zip,
  splices source's sldMasterIdLst / notesMasterIdLst / embeddedFontLst
  into presentation.xml, rewrites presentation.xml.rels + each slide's
  rels to point at the originals, updates [Content_Types].xml, and
  copies referenced media (renamed on collision with pptxgenjs's media).
  Bails when source/output aspect ratios differ so 4:3 sources don't
  get their chrome stretched onto a 16:9 canvas.
* pptxgenjs's slide.background only emits flat-hex solidFill, collapsing
  theme refs / gradients / image-fills. A new per-slide pass copies the
  source slide's <p:bg> verbatim (with r:id rewrite for image fills),
  or strips the flat-hex stand-in when the source inherits from the
  layout / master.
* EMF/WMF decode failures used to return null from parsePic; combined
  with upstream catches this could wipe every element on the same slide
  (Dickinson slides 2/3/9). The fallback now returns an UnknownElement
  so the source <p:pic> is re-injected verbatim and PowerPoint renders
  the metafile natively.

Validated end-to-end on Dickinson_Sample_Slides.pptx (9/9 slides retain
content, slide 2's schemeClr tx1 theme bg survives) and eon-deck.pptx
(28 layouts, 5 embedded fonts, 3 themes preserved).
@karthikmudunuri karthikmudunuri merged commit 26c1b2d into main May 13, 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