Appearance
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 state | How a part gets there | What save() writes |
|---|---|---|
| Untouched | Loaded and never marked dirty. Reading part.dom parses the part without marking it. | The loaded bytes, byte-identical. |
| Dirty | part.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. |
| Added | opc.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. |
| Removed | opc.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 set | add(), 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, unchanged | No registration or removal touched it. | The loaded bytes. |
[Content_Types].xml, changed | ensureRegistered() 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.originalByteskeeps the loaded bytes for the part's whole life.part.serialize()returns the current body, edits included.save(), slide copies and imports all readserialize().- A typed setter marks its part dirty. An edit through
element_orpart.domdoes not. CallmarkDirty()after such an edit, orsave()writes the loaded bytes and the edit is lost. test/read/roundtrip.test.jschecks 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.
| Code | Thrown when |
|---|---|
package/not-an-opc-package | The zip has no [Content_Types].xml. |
package/part-content-type-missing | No 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-id | A .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.
| Construct | Parts | Why it is not decoded | How to reach it |
|---|---|---|---|
| SmartArt layout, quick style and colours | The 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 objects | p: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 models | am3d: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. |
| Ink | p:contentPart, and an InkML part | Digitizer 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 video | a: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 3D | a:scene3d and a:sp3d in a shape's p:spPr | No getter reads them. | shape.element_. A table cell's bevel and light rig are decoded, by TableCell.cell3D. |
| Animation timing | p:timing in a slide part | The 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 data | The 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 data | The .fntdata parts under ppt/fonts/ | Glyph data. | presentation.embeddedFonts names each face's part, and opc.part() returns it. See Embedded fonts. |
| Document statistics | Slides, Words, Paragraphs, HeadingPairs and the other counts in docProps/app.xml | The 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 sidecars | The workbook embedded behind a chart, and a chartEx chart's style, colours and geography parts | Chart 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())| Export | What it is for | Members |
|---|---|---|
OpcPackage | The 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() |
Part | One part: its loaded bytes, its DOM on demand, and whether it is dirty. | partName, contentType, originalBytes, isXmlPart, dom, isParsed, markDirty(), isDirty, serialize() |
ContentTypes | The overlay over [Content_Types].xml that resolves and registers part types. | contentTypeFor(), ensureRegistered(), ensureDefault(), removeOverride(), isDirty, serialize() |
Relationships | The 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 aUint8Array,ArrayBuffer,Blobornumber[]. Under Node it also takes a file path: a string is always a path, never zip content. SeeOpcInput.partsleaves out[Content_Types].xml. Read it throughcontentTypes.contentTypeFor()looks for anOverridewith the exact partname first, then aDefaultfor the lowercased extension.domthrowspart/not-xmlfor a binary part. ItsDocumenttype 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.relspart. Pass'/', or nothing, for the package relationships in/_rels/.rels.- Relationship ids are opaque and need not be contiguous.
add()allocatesrId<n>, where n is one more than the highest numeric id in the set. resolveTarget()throwsrelationship/not-foundfor an id the set lacks, andrelationship/external-has-no-partnamefor an external target.resolveRelativePartName()throwspackage/relationship-target-escapes-rootfor 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()andreservePartNameLike()return an unused partname one past the highest index in use, and create nothing.