opc
The OPC container: read the .pptx ZIP safely, name its parts, follow its relationships, and write it back.
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
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_TYPEconstconst 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; };RelationshipTypetypetype RelationshipType = (typeof REL_TYPE)[keyof typeof REL_TYPE];
CONTENT_TYPEconstconst 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"; };ContentTypetypetype ContentType = (typeof CONTENT_TYPE)[keyof typeof CONTENT_TYPE];
FONT_DATA_EXTENSIONconstTheDefaultextension for embedded fonts, and its content type.const FONT_DATA_EXTENSION = "fntdata";
FONT_DATA_CONTENT_TYPEconstconst 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"];
OpcErrorCodetypetype 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; }OpcErrorclassclass OpcError extends Error { readonly code: OpcErrorCode; readonly detail: OpcErrorDetail; constructor(code: OpcErrorCode, message: string, detail?: OpcErrorDetail); }isOpcErrorfunctionfunction isOpcError(value: unknown): value is OpcError;
guardfunctionRunfn, converting anything it throws that is not already anOpcErrorinto 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 ofdata, as an unsigned 32-bit number.function crc32(data: Uint8Array, seed?: number): number;
src/digest/sha256.ts
sha256functionThe 32-byte digest ofbytes.function sha256(bytes: Uint8Array): Uint8Array;
sha256HexfunctionThe digest ofbytesas 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_STOREconstconst COMPRESSION_STORE = 0;
COMPRESSION_DEFLATEconstconst 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; }ReadZipOptionstypeinterface 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 ofbytesand validate the archive's structure.function readZip(bytes: Uint8Array, options?: ReadZipOptions): ZipArchive;
src/parts/pack-uri.ts
PartNameRuletypeOPC's own rule identifiers, plusX1.*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';
PartNameSeveritytypefatalmeans the name is rejected.warningmeans 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';
PartNameViolationtypeinterface PartNameViolation { readonly rule: PartNameRule; readonly severity: PartNameSeverity; readonly message: string; }PartNametypeA string that has been throughtoPartName. There is no cast-free way to make one, so a function taking aPartNamecan 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 rulenamebreaks, in reporting order. An empty array means the name is a conformant OPC part name.function validatePartName(name: string, maxLength?: number): PartNameViolation[];
isValidPartNamefunctionTrue whennamebreaks no rule at all, warnings included.function isValidPartName(name: string): boolean;
toPartNamefunctionValidatenameas a part name, or throw. Warnings are tolerated - seePartNameSeverityfor 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 theM1.12equivalence rule.function normalizePartName(name: string): string;
PartNameCollisiontypeA package-level naming collision, reported bycheckPartNameCollisions.interface PartNameCollision { readonly rule: 'M1.11' | 'M1.12'; readonly names: readonly [string, string]; readonly message: string; }checkPartNameCollisionsfunctionM1.11andM1.12across 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 describespartName's outgoing relationships.function relsPartNameFor(partName: string): PartName;
isRelationshipPartNamefunctionTrue for/_rels/.relsand/<dir>/_rels/<name>.rels.function isRelationshipPartName(name: string): boolean;
isContentTypesStreamNamefunctionThe content-type stream is matched case-insensitively. It is not a part, soM1.12does 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 relationshipTargetagainst 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>; }FlatXmlLimitstypeinterface 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_LIMITSconstconst 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';
DefaultEntrytypeinterface DefaultEntry { /** The extension as written, without a dot. Office writes these lowercase. */ readonly extension: string; readonly contentType: string; }OverrideEntrytypeinterface OverrideEntry { /** An absolute part name, leading slash included. */ readonly partName: string; readonly contentType: string; }ResolvedContentTypetypeinterface ResolvedContentType { readonly contentType: string; readonly origin: ContentTypeOrigin; }isXmlContentTypefunctionWhether a part with this content type holds XML.function isXmlContentType(contentType: string | undefined): boolean;
ContentTypesclassclass 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';
Relationshiptypeinterface 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 whenidis a legalxsd:ID.function isValidRelationshipId(id: string): boolean;
sourcePartNameForRelsfunctionThe part whose relationships a.relspart describes.function sourcePartNameForRels(relsPartName: string): string;
relativeTargetForfunctionA relativeTargetfromsourcePartNametotargetPartName.function relativeTargetFor(sourcePartName: string, targetPartName: string): string;
Relationshipsclassclass 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; }WritePackageOptionstypeinterface 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; }PartStoreclassclass 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…