Skip to content

Round-trip guarantee ​

pptx-ts/read keeps the bytes of every part it loads. save() writes those bytes back for each part nothing changed, so loading and saving a deck leaves its untouched parts byte-identical. An edit reserializes only the parts it touched.

This page states what save() writes for each part, which constructs survive without a decoder, and the package layer the object model sits on. Read object model describes the typed view. Read and edit a deck covers opening and editing one.

What save writes ​

Part stateHow a part gets thereWhat save() writes
UntouchedLoaded and never marked dirty. Reading part.dom parses the part without marking it.The loaded bytes, byte-identical.
Dirtypart.markDirty(), called by you or by a typed setter.The part's DOM, serialized. The XML is equivalent but not byte-identical: attribute quoting and whitespace can change. The part keeps its XML declaration, or gets a default one when it had none. A part marked dirty with no change is still reserialized.
Binary (image, font, media)It cannot become dirty: markDirty() throws part/not-xml.The loaded bytes. Picture.setImage() adds a new part and leaves the old one in place.
Addedopc.addPart(), called by you or by a method that adds media, notes or copied parts.The given bytes, after the loaded parts, in the order they were added. [Content_Types].xml gains an Override unless an existing entry already resolves the part to its type.
Removedopc.removePart(), called by you or by removeSlide().Nothing. The part's .rels part and its Override entry go with it. A Default entry stays.
The .rels part of a changed relationship setadd(), addWithId() or remove() on the set.The set, serialized. An existing .rels part keeps its place in the package. A new one is appended.
[Content_Types].xml, unchangedNo registration or removal touched it.The loaded bytes.
[Content_Types].xml, changedensureRegistered() or ensureDefault() added an entry, or removePart() dropped an Override.A regenerated part: every Default entry, then every Override entry.
Under /[trash]/PowerPoint leaves deleted parts there, unregistered and unreferenced.Nothing. load() drops them.
  • The guarantee covers part bodies, the set of part names and part order. Loaded parts keep their zip order.
  • It does not cover the zip container. Compression and entry metadata can differ, so the file as a whole is not byte-identical.
  • part.originalBytes keeps the loaded bytes for the part's whole life. part.serialize() returns the current body, edits included. save(), slide copies and imports all read serialize().
  • A typed setter marks its part dirty. An edit through element_ or part.dom does not. Call markDirty() after such an edit, or save() writes the loaded bytes and the edit is lost.
  • test/read/roundtrip.test.js checks these rules on every deck in the read fixture corpus. The testing guide describes that suite.

load() rejects a package it cannot hold to these rules. A malformed .rels part throws the first time its relationships are read.

CodeThrown when
package/not-an-opc-packageThe zip has no [Content_Types].xml.
package/part-content-type-missingNo Override or Default entry resolves a part's content type.
package/content-types-invalid-root, package/content-types-entry-incomplete[Content_Types].xml has the wrong root, or an entry lacks an attribute.
package/relationships-invalid-root, package/relationship-incomplete, package/relationship-invalid-target-mode, package/duplicate-relationship-idA .rels part has the wrong root, a relationship lacks Id, Type or Target, has an unknown TargetMode, or repeats an id.

Kept but not decoded ​

These constructs load and save byte-identical, and no getter decodes them. Their parts stay reachable through presentation.opc, and through the shape that holds them where there is one.

ConstructPartsWhy it is not decodedHow to reach it
SmartArt layout, quick style and coloursThe layout, quickStyle and colors parts under ppt/diagrams/They are the presets PowerPoint's layout engine runs to draw a diagram. Decoding them means implementing that engine.Diagram.layoutPart, quickStylePart and colorsPart. layoutTypeId names the preset. The data model and drawing cache are decoded: see SmartArt.
OLE objectsp:oleObj in a graphic frame, and the payload under ppt/embeddings/The payload is another application's file format.A GraphicFrame whose graphicDataUri is not a table, chart, chartEx or SmartArt namespace. Its parts through slide.relationships. See OLE embedded objects.
3D modelsam3d:model3d in a graphic frame, and a .glb part under ppt/media/A glTF binary plus camera and lighting markup, with no reader in the library.A GraphicFrame whose graphicDataUri is http://schemas.microsoft.com/office/drawing/2017/model3d. See 3D models.
Inkp:contentPart, and an InkML partDigitizer strokes, with no renderer and no writer.shapes skips a bare p:contentPart. Inside mc:AlternateContent, shapes reports the shape in the mc:Fallback branch. The ink part through slide.relationships.
Audio and videoa:audioFile or a:videoFile and p14:media on a picture, and the media part under ppt/media/The read model does not decode the media relationships.The Picture reports its poster image through imagePartName. The media parts through slide.relationships.
Shape 3Da:scene3d and a:sp3d in a shape's p:spPrNo getter reads them.shape.element_. A table cell's bevel and light rig are decoded, by TableCell.cell3D.
Animation timingp:timing in a slide partThe library keeps the recursive timing tree as DOM, and rewrites only the shape ids it references when ids change.slide.hasAnimations and flattenAnimations(). Transitions are decoded, by slide.transition. See Animations and transitions.
Custom XML dataThe item and itemProps parts under customXml/Application-defined XML with no schema to decode against.opc.parts. Programmatic tags are a different construct, and are decoded: see Tags.
Embedded font dataThe .fntdata parts under ppt/fonts/Glyph data.presentation.embeddedFonts names each face's part, and opc.part() returns it. See Embedded fonts.
Document statisticsSlides, Words, Paragraphs, HeadingPairs and the other counts in docProps/app.xmlThe producing application computed them for the file it wrote. An edited deck no longer matches them.opc.part('/docProps/app.xml'). appProperties reports four other fields: see Document properties.
Chart workbooks and chartEx sidecarsThe workbook embedded behind a chart, and a chartEx chart's style, colours and geography partsChart getters read the value caches stored in the chart part.presentation.opc.relationshipsFor(chart.partName).

Comments in PowerPoint's 2018 format are decoded, read-only: see Comments.

Package layer ​

Presentation wraps an OpcPackage, reachable as presentation.opc. Work at this layer for a part the object model has no class for.

ts
import { readFile, writeFile } from 'node:fs/promises'
import { OpcPackage } from 'pptx-ts/read'

const pkg = await OpcPackage.load(await readFile('deck.pptx'))
const slides = pkg.partsByContentType('application/vnd.openxmlformats-officedocument.presentationml.slide+xml')
console.log(slides.map((part) => part.partName)) // ['/ppt/slides/slide1.xml', ...]
await writeFile('deck-copy.pptx', await pkg.save())
ExportWhat it is forMembers
OpcPackageThe loaded package: parts by name or content type, each part's relationships, adding and removing parts, saving.load(), parts, part(), partsByContentType(), relationshipsFor(), contentTypes, addPart(), removePart(), reserveMediaPartName(), reservePartNameLike(), save()
PartOne part: its loaded bytes, its DOM on demand, and whether it is dirty.partName, contentType, originalBytes, isXmlPart, dom, isParsed, markDirty(), isDirty, serialize()
ContentTypesThe overlay over [Content_Types].xml that resolves and registers part types.contentTypeFor(), ensureRegistered(), ensureDefault(), removeOverride(), isDirty, serialize()
RelationshipsThe overlay over one .rels part. It is iterable.get(), byType(), resolveTarget(), add(), addWithId(), remove(), size, serialize()
resolveRelativePartName(), relsPartNameFor()Partname arithmetic: a relationship target to an absolute partname, and a part to the partname of its .rels part.
  • load() takes a Uint8Array, ArrayBuffer, Blob or number[]. Under Node it also takes a file path: a string is always a path, never zip content. See OpcInput.
  • parts leaves out [Content_Types].xml. Read it through contentTypes.
  • contentTypeFor() looks for an Override with the exact partname first, then a Default for the lowercased extension.
  • dom throws part/not-xml for a binary part. Its Document type is @xmldom/xmldom's, not the DOM library TypeScript ships, and the two are not assignable to each other.
  • relationshipsFor() returns one cached set per part, and an empty set for a part with no .rels part. Pass '/', or nothing, for the package relationships in /_rels/.rels.
  • Relationship ids are opaque and need not be contiguous. add() allocates rId<n>, where n is one more than the highest numeric id in the set.
  • resolveTarget() throws relationship/not-found for an id the set lacks, and relationship/external-has-no-partname for an external target. resolveRelativePartName() throws package/relationship-target-escapes-root for a target above the package root.
  • partsByContentType() returns a new array on every call.
  • removePart() leaves every relationship that points at the part. removeSlide() also unlinks the slide from the presentation, and removes each part the slide referenced that no remaining part references. It never removes a layout, master or theme.
  • reserveMediaPartName() and reservePartNameLike() return an unused partname one past the highest index in use, and create nothing.