PPTX Studio
Packages

opc

The OPC container: read the .pptx ZIP safely, name its parts, follow its relationships, and write it back.

@pptx-studio/opc0.1.3runs inTabWorkerNodeLive demo →npm →

Reads and writes the .pptx ZIP - parts, content types and relationships - with hard limits against hostile files.

npm install @pptx-studio/opc

What it is

A .pptx is a ZIP archive with rules on top: every entry is a part with a content type, and parts point at each other through small relationship files rather than by name. @pptx-studio/opc is the reader and writer for exactly that, and nothing more - it knows nothing about slides.

It reads leniently, so a file PowerPoint opens is a file this opens, and writes strictly, so what comes out is what PowerPoint expects. Parts nobody touched are copied back out still compressed.

30-second example

open.ts
const store = PartStore.open(bytes); // hostile-input limits are on by default

const root = store.rootRelationships();
const main = root.firstOfType(REL_TYPE.officeDocument);
if (main === undefined) throw new Error('no officeDocument relationship');
const presentation = root.resolve(main); // '/ppt/presentation.xml'

for (const rel of store.relationships(presentation).byType(REL_TYPE.slide)) {
  const slide = store.relationships(presentation).resolve(rel);
  console.log(slide, store.contentTypeOf(slide), store.read(slide).byteLength);
}

Key concepts

Nothing finds anything by filename

A slide does not know which layout it uses; only the slide's own _rels/slideN.xml.rels file does. Every lookup goes through store.relationships(part), which is scoped to one part, and relationship ids are meaningful only inside that file.

Preservation is the default state

A part you never asked for is never inflated, and on write it is copied across from the source archive byte for byte, still compressed. That is not an optimisation: it is why charts, macros and animations survive a save from code that cannot draw them.

Untrusted archives

PartStore.open enforces DEFAULT_ZIP_LIMITS - archive size, total inflated size, entries, compression ratio - and checks CRCs and overlapping entries. A hostile file raises a typed OpcError rather than taking the tab down.

Two digests, not interchangeable

crc32 is what a ZIP entry carries; sha256 is what the round-trip comparison uses for binary parts. Both are exported; neither stands in for the other.

API reference

77 exports of @pptx-studio/opc 0.1.3, read from the package this site installed. Click a name for its declaration.

src/constants.ts

  • CONTENT_TYPES_PARTconstThe content-type stream, as a ZIP entry name.
    const CONTENT_TYPES_PART = "[Content_Types].xml";
  • ROOT_RELS_PARTconstThe package-level relationship part, as a part name: the only way in to a .pptx.
    const ROOT_RELS_PART = "/_rels/.rels";
  • OPC_NSconstNamespaces used by the two package-level parts above.
    const OPC_NS: {
      readonly contentTypes: "http://schemas.openxmlformats.org/package/2006/content-types";
      readonly relationships: "http://schemas.openxmlformats.org/package/2006/relationships";
    };
  • REL_TYPEconst
    const REL_TYPE: {
      readonly officeDocument: string;
      readonly slide: string;
      readonly slideLayout: string;
      readonly slideMaster: string;
      readonly notesSlide: string;
      readonly notesMaster: string;
      readonly handoutMaster: string;
      readonly theme: string;
      readonly image: string;
      readonly font: string;
      readonly tags: string;
      readonly presProps: string;
      readonly viewProps: string;
      readonly tableStyles: string;
      readonly chart: string;
      /** The embedded workbook behind a chart. */
      readonly package: string;
      readonly oleObject: string;
      readonly diagramData: string;
      readonly diagramLayout: string;
      readonly diagramColors: string;
      /** Note the name: the type is `diagramQuickStyle`, the part is `quickStyle`. */
      readonly diagramQuickStyle: string;
      readonly diagramDrawing: string;
      readonly chartStyle: string;
      readonly chartColorStyle: string;
      readonly hyperlink: string;
      readonly video: string;
      readonly audio: string;
      readonly media: string;
      readonly extendedProperties: string;
      readonly customProperties: string;
      /**
       * The two below are in the **package** namespace, not `officeDocument`.
       *
       * That is not a typo and it is easy to get wrong: `extended-properties`
       * above is `officeDocument/...` while `core-properties` here is
       * `package/.../metadata/...`. Both appear side by side in every `_rels/.rels`
       * we have measured.
       */
      readonly coreProperties: string;
      readonly thumbnail: string;
    };
  • RelationshipTypetype
    type RelationshipType = (typeof REL_TYPE)[keyof typeof REL_TYPE];
  • CONTENT_TYPEconst
    const CONTENT_TYPE: {
      readonly relationships: string;
      readonly coreProperties: string;
      readonly xml: "application/xml";
      /** `.pptx`. A `.potx` is `template.main+xml` and the two are not interchangeable. */
      readonly presentation: string;
      readonly template: string;
      readonly slideshow: string;
      readonly slide: string;
      readonly slideLayout: string;
      readonly slideMaster: string;
      readonly notesSlide: string;
      readonly notesMaster: string;
      readonly handoutMaster: string;
      readonly presProps: string;
      readonly viewProps: string;
      readonly tableStyles: string;
      readonly tags: string;
      readonly commentAuthors: string;
      readonly comments: string;
      readonly theme: string;
      readonly extendedProperties: string;
      readonly customProperties: string;
      readonly spreadsheet: string;
      readonly chart: string;
      readonly diagramData: string;
      readonly diagramLayout: string;
      readonly diagramStyle: string;
      readonly diagramColors: string;
      readonly png: "image/png";
      readonly jpeg: "image/jpeg";
      readonly gif: "image/gif";
      readonly svg: "image/svg+xml";
      readonly tiff: "image/tiff";
      readonly emf: "image/x-emf";
      readonly wmf: "image/x-wmf";
    };
  • ContentTypetype
    type ContentType = (typeof CONTENT_TYPE)[keyof typeof CONTENT_TYPE];
  • FONT_DATA_EXTENSIONconstThe Default extension for embedded fonts, and its content type.
    const FONT_DATA_EXTENSION = "fntdata";
  • FONT_DATA_CONTENT_TYPEconst
    const FONT_DATA_CONTENT_TYPE = "application/x-fontdata";

src/errors.ts

  • OPC_ERROR_CODESconstMachine-readable failure reasons.
    const OPC_ERROR_CODES: readonly ["ERR_NOT_A_ZIP", "ERR_ENCRYPTED_PACKAGE", "ERR_TRUNCATED", "ERR_ARCHIVE_TOO_LARGE", "ERR_TOO_MANY_ENTRIES", "ERR_BUDGET_EXCEEDED", "ERR_ENTRY_TOO_LARGE", "ERR_RATIO_EXCEEDED", "ERR_UNSUPPORTED_COMPRESSION", "ERR_ENTRY_ENCRYPTED", "ERR_HEADER_MISMATCH", "ERR_OVERLAPPING_ENTRY", "ERR_CRC_MISMATCH", "ERR_INFLATE_FAILED", "ERR_ZIP64_UNSUPPORTED", "ERR_DUPLICATE_ENTRY", "ERR_INVALID_PART_NAME", "ERR_TARGET_ESCAPES_PACKAGE", "ERR_MALFORMED_XML", "ERR_DOCTYPE_FORBIDDEN", "ERR_MISSING_PACKAGE_PART", "ERR_MISSING_CONTENT_TYPE", "ERR_CONTENT_TYPE_CONFLICT", "ERR_INVALID_CONTENT_TYPE", "ERR_INVALID_RELATIONSHIP_ID", "ERR_DUPLICATE_RELATIONSHIP_ID", "ERR_DANGLING_RELATIONSHIP", "ERR_INVALID_RELATIONSHIP_TARGET", "ERR_PART_NOT_FOUND", "ERR_PART_EXISTS", "ERR_ZIP32_OVERFLOW"];
  • OpcErrorCodetype
    type OpcErrorCode = (typeof OPC_ERROR_CODES)[number];
  • OpcErrorDetailtypeStructured context for a failure. Every field is optional and diagnostic.
    interface OpcErrorDetail {
      /** ZIP entry name or OPC part name, when the failure is attributable to one. */
      readonly entry?: string;
      /** Byte offset into the archive, when the failure is positional. */
      readonly offset?: number;
      /** The budget that was exceeded. */
      readonly limit?: number;
      /** The value that exceeded it. */
      readonly actual?: number;
      /** Whatever a dependency threw, preserved rather than swallowed. */
      readonly cause?: unknown;
    }
  • OpcErrorclass
    class OpcError extends Error {
      readonly code: OpcErrorCode;
      readonly detail: OpcErrorDetail;
      constructor(code: OpcErrorCode, message: string, detail?: OpcErrorDetail);
    }
  • isOpcErrorfunction
    function isOpcError(value: unknown): value is OpcError;
  • guardfunctionRun fn, converting anything it throws that is not already an OpcError into one. This is the only sanctioned way to call into a dependency that handles untrusted bytes.
    function guard<T>(code: OpcErrorCode, message: string, detail: OpcErrorDetail, fn: () => T): T;

src/digest/crc32.ts

  • crc32functionCRC-32 of data, as an unsigned 32-bit number.
    function crc32(data: Uint8Array, seed?: number): number;

src/digest/sha256.ts

  • sha256functionThe 32-byte digest of bytes.
    function sha256(bytes: Uint8Array): Uint8Array;
  • sha256HexfunctionThe digest of bytes as 64 lowercase hex digits - the spelling the corpus manifests use.
    function sha256Hex(bytes: Uint8Array): string;
  • sha256HexOfTextfunctionThe digest of a string, encoded UTF-8 first.
    function sha256HexOfText(text: string): string;

src/limits.ts

  • ZipLimitstypeDecompression budgets.
    interface ZipLimits {
      /** Hard ceiling on the archive as read from disk or the network. */
      readonly maxArchiveBytes: number;
      /** Running total of inflated bytes across every entry read from one archive. */
      readonly maxTotalInflatedBytes: number;
      /** Ceiling for any single entry. No legitimate part of a deck approaches this. */
      readonly maxEntryInflatedBytes: number;
      /** Ceiling on central-directory entry count. */
      readonly maxEntries: number;
      /** Ceiling on inflated/compressed for one entry, above `ratioFloorBytes`. */
      readonly maxCompressionRatio: number;
      /**
       * Below this inflated size the ratio test is not applied.
       *
       * Without a floor the test is useless: a 40-byte DEFLATE stream expanding to
       * a 12 KB `[Content_Types].xml` is a 300:1 ratio and completely ordinary. A
       * ratio only means something once the absolute size is large enough to hurt.
       */
      readonly ratioFloorBytes: number;
      /** Ceiling on a ZIP entry name, in UTF-16 code units after decoding. */
      readonly maxNameLength: number;
    }
  • DEFAULT_ZIP_LIMITSconstThe defaults, sized against the plan's stated envelope: a 200 MB, 300-slide deck must load, and nothing much larger is a real presentation.
    const DEFAULT_ZIP_LIMITS: ZipLimits;
  • InflationBudgetclassThe running counter.
    class InflationBudget {
      #private;
      readonly limit: number;
      constructor(limit: number);
      get used(): number;
      get remaining(): number;
      /** Would charging `bytes` exceed the limit? Charges nothing. */
      wouldExceed(bytes: number): boolean;
      /** Charge `bytes`, or throw `ERR_BUDGET_EXCEEDED` and charge nothing. */
      charge(bytes: number, entry: string): void;
    }

src/zip/zip-reader.ts

  • COMPRESSION_STOREconst
    const COMPRESSION_STORE = 0;
  • COMPRESSION_DEFLATEconst
    const COMPRESSION_DEFLATE = 8;
  • ZipEntrytypeOne central-directory entry, resolved against its local file header.
    interface ZipEntry {
      /** The raw ZIP entry name, e.g. `ppt/slides/slide1.xml`. No leading slash. */
      readonly name: string;
      /**
       * Position in the central directory.
       *
       * This is the export order. The writer replays it so an untouched package
       * comes back out with the same entry sequence, which is what makes a
       * round-trip diff meaningful.
       */
      readonly index: number;
      readonly method: number;
      readonly flags: number;
      readonly compressedSize: number;
      readonly uncompressedSize: number;
      /** The stored CRC-32, unsigned. Taken from the central directory, which is authoritative. */
      readonly crc32: number;
      /** Offset of the local file header. */
      readonly localHeaderOffset: number;
      /** Offset of the entry's compressed bytes, after the local header and its name and extra fields. */
      readonly dataOffset: number;
      readonly dosTime: number;
      readonly dosDate: number;
      readonly externalAttributes: number;
      /** A name ending in `/`. OPC packages should have none; real archives sometimes do. */
      readonly isDirectory: boolean;
      /** Bit 3 was set: the local header carried zeroed sizes and a trailing data descriptor. */
      readonly hasDataDescriptor: boolean;
    }
  • ReadZipOptionstype
    interface ReadZipOptions {
      /** Override any subset of the default budgets. */
      readonly limits?: Partial<ZipLimits>;
    }
  • ZipArchiveclassA parsed ZIP archive. Entries are enumerated eagerly; bytes are inflated only when asked for.
    class ZipArchive {
      #private;
      readonly entries: readonly ZipEntry[];
      readonly limits: ZipLimits;
      readonly budget: InflationBudget;
      constructor(bytes: Uint8Array, entries: readonly ZipEntry[], limits: ZipLimits);
      get(name: string): ZipEntry | undefined;
      has(name: string): boolean;
      /**
       * The entry's bytes exactly as stored - still compressed if it was
       * compressed. Charges no budget and inflates nothing.
       *
       * This is what the writer streams through for parts we never edited, so that
       * preservation costs neither a decompress nor a recompress.
       */
      raw(entry: ZipEntry): Uint8Array;
      /**
       * Inflate one entry, verify its CRC-32, and charge the running budget.
       *
       * The budget is charged once per entry rather than once per call: re-reading
       * a part must not consume the archive's allowance a second time, or opening
       * the same deck twice in one session would fail the second time for no
       * reason a user could understand.
       */
      read(entry: ZipEntry): Uint8Array;
      /** `read`, addressed by ZIP entry name. */
      readByName(name: string): Uint8Array;
    }
  • readZipfunctionParse the central directory of bytes and validate the archive's structure.
    function readZip(bytes: Uint8Array, options?: ReadZipOptions): ZipArchive;

src/parts/pack-uri.ts

  • PartNameRuletypeOPC's own rule identifiers, plus X1.* for hostility the spec never considered.
    type PartNameRule = 'M1.1' | 'M1.3' | 'M1.4' | 'M1.5' | 'M1.6' | 'M1.7' | 'M1.8' | 'M1.9' | 'M1.10' | 'M1.11' | 'M1.12' |
    /** Control character or NUL, literal or percent-encoded. */
    'X1.1' |
    /** Backslash - a Windows separator smuggled through a ZIP entry name. */
    'X1.2' |
    /** A leading drive letter, e.g. `/C:/...`, which is valid pchar and still hostile. */
    'X1.3' |
    /** A `.` or `..` segment: path traversal. */
    'X1.4' |
    /** Longer than the configured ceiling. */
    'X1.5';
  • PartNameSeveritytypefatal means the name is rejected. warning means it is out of spec but harmless, and rejecting it would fail a real deck over a hygiene nit - a literal space in a media part name is the common case.
    type PartNameSeverity = 'fatal' | 'warning';
  • PartNameViolationtype
    interface PartNameViolation {
      readonly rule: PartNameRule;
      readonly severity: PartNameSeverity;
      readonly message: string;
    }
  • PartNametypeA string that has been through toPartName. There is no cast-free way to make one, so a function taking a PartName can rely on the grammar rather than re-validating.
    type PartName = string & {
      readonly [partNameBrand]: true;
    };
  • MAX_PART_NAME_LENGTHconstDefault ceiling on part-name length. ZIP itself allows 65535.
    const MAX_PART_NAME_LENGTH = 1024;
  • validatePartNamefunctionEvery rule name breaks, in reporting order. An empty array means the name is a conformant OPC part name.
    function validatePartName(name: string, maxLength?: number): PartNameViolation[];
  • isValidPartNamefunctionTrue when name breaks no rule at all, warnings included.
    function isValidPartName(name: string): boolean;
  • toPartNamefunctionValidate name as a part name, or throw. Warnings are tolerated - see PartNameSeverity for why.
    function toPartName(name: string, maxLength?: number): PartName;
  • partNameFromZipEntryfunctionConvert a ZIP entry name to a part name and validate it.
    function partNameFromZipEntry(entryName: string, maxLength?: number): PartName;
  • zipEntryNameForfunctionThe ZIP entry name for a part: the part name without its leading slash.
    function zipEntryNameFor(partName: PartName): string;
  • normalizePartNamefunctionASCII-only lowercase, for the M1.12 equivalence rule.
    function normalizePartName(name: string): string;
  • PartNameCollisiontypeA package-level naming collision, reported by checkPartNameCollisions.
    interface PartNameCollision {
      readonly rule: 'M1.11' | 'M1.12';
      readonly names: readonly [string, string];
      readonly message: string;
    }
  • checkPartNameCollisionsfunctionM1.11 and M1.12 across a whole package - neither can be judged from one name alone.
    function checkPartNameCollisions(names: readonly string[]): PartNameCollision[];
  • partExtensionfunctionThe extension of a part name, without the dot, case preserved. Empty when there is none.
    function partExtension(name: string): string;
  • partDirectoryfunctionEverything up to and including the last slash: /ppt/slides/.
    function partDirectory(name: string): string;
  • relsPartNameForfunctionThe relationship part that describes partName's outgoing relationships.
    function relsPartNameFor(partName: string): PartName;
  • isRelationshipPartNamefunctionTrue for /_rels/.rels and /<dir>/_rels/<name>.rels.
    function isRelationshipPartName(name: string): boolean;
  • isContentTypesStreamNamefunctionThe content-type stream is matched case-insensitively. It is not a part, so M1.12 does not formally apply, but every producer writes it identically and no reader gains anything by being strict here.
    function isContentTypesStreamName(entryName: string): boolean;
  • resolveRelativeTargetfunctionResolve a relationship Target against the part that declared it.
    function resolveRelativeTarget(sourcePartName: string, target: string): PartName;

src/parts/flat-xml.ts

  • FlatElementtypeOne start tag, with its attributes as written.
    interface FlatElement {
      /** Local name with any prefix stripped: `Override`, not `ct:Override`. */
      readonly name: string;
      /** The name exactly as written, prefix included. */
      readonly qname: string;
      /** 0 for the root element. */
      readonly depth: number;
      /** Attributes keyed by their written name, values entity-decoded. */
      readonly attrs: ReadonlyMap<string, string>;
    }
  • FlatXmlLimitstype
    interface FlatXmlLimits {
      /** Ceiling on elements in one part. */
      readonly maxElements: number;
      /** Ceiling on attributes on one element. */
      readonly maxAttributes: number;
      /** Ceiling on nesting. These parts are two deep; anything more is not one of them. */
      readonly maxDepth: number;
    }
  • DEFAULT_FLAT_XML_LIMITSconst
    const DEFAULT_FLAT_XML_LIMITS: FlatXmlLimits;
  • localNamefunctionStrip a prefix: ct:Override -> Override.
    function localName(qname: string): string;
  • readFlatXmlfunctionRead every start tag in document order.
    function readFlatXml(bytes: Uint8Array, context: string, limits?: FlatXmlLimits): FlatElement[];
  • escapeAttributefunctionEscape a string for use in an attribute value.
    function escapeAttribute(value: string): string;
  • XML_DECLARATIONconstThe XML declaration Office writes, verbatim - including the CRLF.
    const XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";

src/parts/content-types.ts

  • ContentTypeOrigintypeResolution order, and where a part's type came from.
    type ContentTypeOrigin = 'override' | 'default';
  • DefaultEntrytype
    interface DefaultEntry {
      /** The extension as written, without a dot. Office writes these lowercase. */
      readonly extension: string;
      readonly contentType: string;
    }
  • OverrideEntrytype
    interface OverrideEntry {
      /** An absolute part name, leading slash included. */
      readonly partName: string;
      readonly contentType: string;
    }
  • ResolvedContentTypetype
    interface ResolvedContentType {
      readonly contentType: string;
      readonly origin: ContentTypeOrigin;
    }
  • isXmlContentTypefunctionWhether a part with this content type holds XML.
    function isXmlContentType(contentType: string | undefined): boolean;
  • ContentTypesclass
    class ContentTypes {
      #private;
      private constructor();
      /** An empty map, for a package being built from nothing. */
      static empty(): ContentTypes;
      /**
       * Read the stream.
       *
       * Order is preserved so that regenerating an unchanged map reproduces the
       * original layout, which is what keeps an export diff readable.
       */
      static parse(bytes: Uint8Array): ContentTypes;
      get defaults(): readonly DefaultEntry[];
      get overrides(): readonly OverrideEntry[];
      /** True once anything has changed, which is what makes the writer regenerate. */
      get dirty(): boolean;
      /**
       * The content type of `partName`, and where it came from.
       *
       * `Override` beats `Default`; both comparisons are ASCII case-insensitive,
       * matching the case-insensitive equivalence OPC already requires of part
       * names. Returns `undefined` when neither applies - that is a package error,
       * but reporting it is the caller's job, because a deck that PowerPoint opens
       * should open here even while we refuse to *write* one in that state.
       */
      resolve(partName: string): ResolvedContentType | undefined;
      /** The content type, or `undefined`. */
      for(partName: string): string | undefined;
      /** The content type, or throw. This is what the writer calls. */
      require(partName: string): string;
      setDefault(extension: string, contentType: string): void;
      setOverride(partName: string, contentType: string): void;
      /** Remove the `Override` for a part, if there is one. Returns whether there was. */
      removeOverride(partName: string): boolean;
      /**
       * Make sure `partName` resolves to `contentType`, doing as little as possible.
       *
       * An `Override` rather than a `Default`, because an `Override` is always
       * correct and a `Default` is a claim about every part sharing that extension
       * - one that a caller adding a single part is in no position to make. Callers
       * that *do* know the extension is uniform, such as the font embedder writing
       * `fntdata`, call `setDefault` and say so.
       */
      ensureFor(partName: string, contentType: string): void;
      /**
       * Serialise, in the layout Office uses: declaration, `Types`, every `Default`
       * in order, then every `Override` in order, no whitespace anywhere.
       */
      serialize(): Uint8Array;
    }

src/parts/relationships.ts

  • TargetModetypeRelationship parts - the edges of the package graph.
    type TargetMode = 'Internal' | 'External';
  • RelationshipOrigintypeWhere a relationship came from.
    type RelationshipOrigin = 'archive' | 'added';
  • Relationshiptype
    interface Relationship {
      readonly id: string;
      /** The relationship type URI, verbatim. */
      readonly type: string;
      /** The target exactly as written - relative, absolute, or an external URI. */
      readonly target: string;
      readonly targetMode: TargetMode;
      readonly origin: RelationshipOrigin;
    }
  • isValidRelationshipIdfunctionTrue when id is a legal xsd:ID.
    function isValidRelationshipId(id: string): boolean;
  • sourcePartNameForRelsfunctionThe part whose relationships a .rels part describes.
    function sourcePartNameForRels(relsPartName: string): string;
  • relativeTargetForfunctionA relative Target from sourcePartName to targetPartName.
    function relativeTargetFor(sourcePartName: string, targetPartName: string): string;
  • Relationshipsclass
    class Relationships {
      #private;
      /** The part these relationships belong to. `/` for the package root. */
      readonly sourcePartName: string;
      /** The `.rels` part itself. */
      readonly partName: PartName;
      private constructor();
      /** An empty collection for a part that has no `.rels` yet. */
      static empty(sourcePartName: string): Relationships;
      static parse(bytes: Uint8Array, sourcePartName: string): Relationships;
      get all(): readonly Relationship[];
      get dirty(): boolean;
      get size(): number;
      byId(id: string): Relationship | undefined;
      byType(type: string): Relationship[];
      firstOfType(type: string): Relationship | undefined;
      /**
       * The part a relationship points at.
       *
       * Internal only - an external target is a URI naming something outside the
       * package and has no part name, so asking for one is a caller bug rather
       * than a data problem.
       */
      resolve(rel: Relationship): PartName;
      /** The part `id` points at, or `undefined` if there is no such internal relationship. */
      targetOf(id: string): PartName | undefined;
      /**
       * The next free `rIdN`.
       *
       * One past the highest number already used, **not** one past the count. Those
       * differ more often than you would expect: of 1296 relationship parts in the
       * corpus we measured, 99 have gaps in their numbering and 175 list their
       * relationships out of numeric order. Allocating from the count would have
       * collided in every one of them.
       */
      nextId(): string;
      /** Add a relationship with a freshly allocated id. */
      add(type: string, target: string, targetMode?: TargetMode): Relationship;
      /** Add a relationship to a part, computing the relative target for you. */
      addTo(type: string, targetPartName: string): Relationship;
      /** Add with a caller-chosen id - for reproducing an existing package exactly. */
      addWithId(id: string, type: string, target: string, targetMode?: TargetMode): Relationship;
      remove(id: string): boolean;
      /**
       * Serialise in Office's layout: declaration, `Relationships`, then each
       * `Relationship` in document order with attributes ordered Id, Type, Target,
       * TargetMode. `TargetMode` is written only when External, because `Internal`
       * is the schema default and Office omits it.
       */
      serialize(): Uint8Array;
    }

src/zip/zip-writer.ts

  • ZipEntryInputtypeOne entry, with its data already in the form it will be stored in.
    interface ZipEntryInput {
      /** ZIP entry name: a part name without its leading slash. */
      readonly name: string;
      readonly method: number;
      /** Deflated bytes when `method` is 8, the bytes themselves when it is 0. */
      readonly data: Uint8Array;
      readonly crc32: number;
      readonly uncompressedSize: number;
    }
  • storedEntryfunctionAn entry stored uncompressed.
    function storedEntry(name: string, bytes: Uint8Array): ZipEntryInput;
  • deflatedEntryfunctionAn entry compressed with DEFLATE, or stored when that would not be smaller.
    function deflatedEntry(name: string, bytes: Uint8Array, level?: number): ZipEntryInput;
  • passthroughEntryfunctionAn entry copied out of a source archive without being decompressed.
    function passthroughEntry(archive: ZipArchive, entry: ZipEntry): ZipEntryInput;
  • writeZipfunctionEmit a ZIP32 archive.
    function writeZip(entries: readonly ZipEntryInput[]): Uint8Array;
  • entryOverheadfunctionThe byte cost of an entry, for a size estimate that does not build one.
    function entryOverhead(name: string): number;
  • EMPTY_ARCHIVE_SIZEconstThe fixed cost of an archive with no entries.
    const EMPTY_ARCHIVE_SIZE = 22;

src/parts/part-store.ts

  • PartInfotypeWhat the store knows about a part without reading it.
    interface PartInfo {
      readonly name: PartName;
      readonly contentType: string;
      /** Uncompressed size in bytes. */
      readonly size: number;
      /** False once the part's bytes have been replaced or it was added here. */
      readonly fromArchive: boolean;
    }
  • WritePackageOptionstype
    interface WritePackageOptions {
      /**
       * Move `[Content_Types].xml` to the front and `_rels/.rels` behind it,
       * the layout Office writes. Off by default - see `write`.
       */
      readonly normalizeEntryOrder?: boolean;
      /** DEFLATE level for parts we serialise ourselves. */
      readonly deflateLevel?: number;
    }
  • PartStoreclass
    class PartStore {
      #private;
      /** The content-type map. Mutating it is how a part's type changes. */
      readonly contentTypes: ContentTypes;
      private constructor();
      /** A package with nothing in it but an empty content-type map. */
      static create(): PartStore;
      /** Open an archive. Nothing is inflated but `[Content_Types].xml`. */
      static open(bytes: Uint8Array, options?: ReadZipOptions): PartStore;
      /** Part names in write order. Excludes `[Content_Types].xml`, which is not a part. */
      get partNames(): PartName[];
      get size(): number;
      has(partName: string): boolean;
      contentTypeOf(partName: string): string | undefined;
      info(partName: string): PartInfo | undefined;
      /**
       * The parts `write` will serialise afresh, rather than stream from the
       * archive, in write order.
       *
       * "Dirty-part-only export" is the claim this class exists to make, and
       * `PartInfo.fromArchive` alone cannot express it: a relationship collection
       * mutated in place through `relationships(...)` leaves its part's `fromArchive`
       * true while `write` re-emits it, because the parsed collection is the edit
       * and `replacePart` was never called. Somewhere has to hold both halves, and
       * this is the only place that can see them.
       */
      rewrittenParts(): PartName[];
      /**
       * The bytes of a part.
       *
       * An archive-backed part is inflated on every call and its CRC-32 checked on
       * every call; the decompression budget is charged once. Nothing is cached
       * here on purpose - what gets cached one layer up is the *parsed* form, and
       * holding both would double the memory of a 200 MB deck to keep a copy
       * nobody reads twice.
       */
      read(partName: string): Uint8Array;
      /**
       * The relationships of a part, parsed on first ask and cached after.
       *
       * Pass `'/'` for the package root, whose relationships live in
       * `_rels/.rels`. A part with no relationship part gets an empty collection
       * rather than `undefined`, so callers add to it without a special case; an
       * empty, untouched collection is never written.
       */
      relationships(sourcePartName: string): Relationships;
      /** The package-level relationships: the only way in to a `.pptx`. */
      rootRelationships(): Relationships;
      /**
       * Every internal relationship that points at a part this package does not
       * contain.
       *
       * `write` preserves the ones that arrived broken rather than refusing to
       * export the deck - see `#assertRelationshipsResolve` for why. That makes
       * this the only way to know they are there, and they are worth knowing
       * about: a stale image relationship is exactly the defect that shows a user
       * a red X and a message about a missing part, with nothing in the file to
       * explain it.
       *
       * Reads and parses every relationship part, so it is a deliberate act rather
       * than something `open` does for you. Never throws.
       */
      danglingRelationships(): {
        source: PartName;
        relationship: Relationship;
        target: string;
      }[];
      /** Add a part that is not already there. */
      addPart(partName: string, contentType: string, bytes: Uint8Array): PartName;
      /** Replace a part's bytes, keeping its name, content type and position. */
      replacePart(partName: string, bytes: Uint8Array): void;
      /**
       * Remove a part, its relationship part and its content-type `Override`.
       *
       * Nothing scans for relationships that pointed at it. That is not an
       * oversight: finding them means walking every `.rels` in the package, and
       * deciding what to do about each is a question about the document rather
       * than the container. `write` refuses to emit a dangling relationship, so
       * the mistake surfaces at export with the part named, rather than as a file
       * PowerPoint declines to open.
       */
      removePart(partName: string): boolean;
      /**
       * Serialise the package.
       *
       * **Entry order is preserved, not normalised.** The plan called for
       * `[Content_Types].xml` first and `_rels/.rels` second, which is what Office
       * writes - but it is a convention rather than a requirement, and we checked:
       * a real deck rebuilt with the content-type stream written *last* opens in
       * PowerPoint with every slide, shape and picture intact. Meanwhile roughly
       * four fifths of the third-party packages we measured do not put it first
       * either. Reordering them would buy nothing measurable and would break the
       * one property this sub-phase exists to establish, that a package we did not
       * edit comes back out exactly as it went in. Pass `normalizeEntryOrder` to
       * get Office's layout anyway; a package built with `create` gets it already,
       * since it has no original order to preserve.
       */
      write(options?: WritePackageOptions): Uint8Array;
      /**
       * Write every changed relationship collection back into the part table.
       *
       * A collection emptied of everything loses its part rather than being written
       * as an empty `<Relationships/>` - Office does not keep those around, and a
       * part that exists only to say nothing is one more thing to explain.
       *
       * ## Why this is public, and why it installs bytes rather than only returning them
       *
       * A relationship part has two representations here: its bytes, and the parsed
       * `Relationships` the store hands out and caches. `relationships(...)` returns
       * a live object, so adding an edge mutates the parsed form and leaves the
       * bytes alone - and until this runs, the two disagree.
       *
       * That was invisible while `write` was the only thing that cared, because it
       * asked the parsed form. It stopped being invisible the moment something else
       * read the package: `@pptx-studio/validate` reads relationship markup as raw
       * XML on purpose - `Relationships.parse` refuses the duplicate and malformed
       * ids that three of the rules exist to report - so it was validating the
       * `.rels` as it arrived rather than as it would be written. A relationship
       * removed in this session still looked present, one added still looked
       * absent, and a `.rels` created in this session was not in `partNames` at all,
       * so nothing checked it. Sub-phase 1.3 found this the way these things are
       * always found: a test asserted a collected image and got a dangling-
       * relationship finding for the edge that had just been removed.
       *
       * So the bytes are installed into the part table, and after this call every
       * byte-reading consumer - `read`, `info`, `partNames`, the validator, a
       * census - sees the package that is about to be written. It is idempotent:
       * calling it again re-serialises the same collections to the same bytes.
       */
      materializeRelationships(): Map<string, Uint8Array>;
    }

Errors

Throws OpcError, whose code is one of:

  • ERR_NOT_A_ZIP
  • ERR_ENCRYPTED_PACKAGE
  • ERR_TRUNCATED
  • ERR_ARCHIVE_TOO_LARGE
  • ERR_TOO_MANY_ENTRIES
  • ERR_BUDGET_EXCEEDED
  • ERR_ENTRY_TOO_LARGE
  • ERR_RATIO_EXCEEDED
  • ERR_UNSUPPORTED_COMPRESSION
  • ERR_ENTRY_ENCRYPTED
  • ERR_HEADER_MISMATCH
  • ERR_OVERLAPPING_ENTRY
  • ERR_CRC_MISMATCH
  • ERR_INFLATE_FAILED
  • ERR_ZIP64_UNSUPPORTED
  • ERR_DUPLICATE_ENTRY
  • ERR_INVALID_PART_NAME
  • ERR_TARGET_ESCAPES_PACKAGE
  • ERR_MALFORMED_XML
  • ERR_DOCTYPE_FORBIDDEN
  • ERR_MISSING_PACKAGE_PART
  • ERR_MISSING_CONTENT_TYPE
  • ERR_CONTENT_TYPE_CONFLICT
  • ERR_INVALID_CONTENT_TYPE
  • ERR_INVALID_RELATIONSHIP_ID
  • ERR_DUPLICATE_RELATIONSHIP_ID
  • ERR_DANGLING_RELATIONSHIP
  • ERR_INVALID_RELATIONSHIP_TARGET
  • ERR_PART_NOT_FOUND
  • ERR_PART_EXISTS
  • ERR_ZIP32_OVERFLOW

Try it live

Reading the deck…

Open the full demo →

Read more

On this page