ppt-codec 0.0.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/content.cjs +72 -0
  4. package/dist/content.d.cts +6 -0
  5. package/dist/content.d.ts +6 -0
  6. package/dist/content.js +71 -0
  7. package/dist/document/document-atom.cjs +27 -0
  8. package/dist/document/document-atom.d.cts +17 -0
  9. package/dist/document/document-atom.d.ts +17 -0
  10. package/dist/document/document-atom.js +26 -0
  11. package/dist/document/fonts.cjs +29 -0
  12. package/dist/document/fonts.d.cts +5 -0
  13. package/dist/document/fonts.d.ts +5 -0
  14. package/dist/document/fonts.js +28 -0
  15. package/dist/document/slide-list.cjs +40 -0
  16. package/dist/document/slide-list.d.cts +14 -0
  17. package/dist/document/slide-list.d.ts +14 -0
  18. package/dist/document/slide-list.js +39 -0
  19. package/dist/drawing/shapes.cjs +109 -0
  20. package/dist/drawing/shapes.d.cts +16 -0
  21. package/dist/drawing/shapes.d.ts +16 -0
  22. package/dist/drawing/shapes.js +108 -0
  23. package/dist/errors.cjs +17 -0
  24. package/dist/errors.d.cts +9 -0
  25. package/dist/errors.d.ts +9 -0
  26. package/dist/errors.js +15 -0
  27. package/dist/header-C5eAd16_.d.cts +13 -0
  28. package/dist/header-C5eAd16_.d.ts +13 -0
  29. package/dist/index.cjs +112 -0
  30. package/dist/index.d.cts +16 -0
  31. package/dist/index.d.ts +16 -0
  32. package/dist/index.js +16 -0
  33. package/dist/read.cjs +119 -0
  34. package/dist/read.d.cts +13 -0
  35. package/dist/read.d.ts +13 -0
  36. package/dist/read.js +114 -0
  37. package/dist/record/header.cjs +25 -0
  38. package/dist/record/header.d.cts +2 -0
  39. package/dist/record/header.d.ts +2 -0
  40. package/dist/record/header.js +21 -0
  41. package/dist/record/tree.cjs +55 -0
  42. package/dist/record/tree.d.cts +2 -0
  43. package/dist/record/tree.d.ts +2 -0
  44. package/dist/record/tree.js +49 -0
  45. package/dist/record/types.cjs +87 -0
  46. package/dist/record/types.d.cts +45 -0
  47. package/dist/record/types.d.ts +45 -0
  48. package/dist/record/types.js +45 -0
  49. package/dist/stream/current-user.cjs +49 -0
  50. package/dist/stream/current-user.d.cts +11 -0
  51. package/dist/stream/current-user.d.ts +11 -0
  52. package/dist/stream/current-user.js +46 -0
  53. package/dist/stream/persist.cjs +71 -0
  54. package/dist/stream/persist.d.cts +20 -0
  55. package/dist/stream/persist.d.ts +20 -0
  56. package/dist/stream/persist.js +67 -0
  57. package/dist/text/atoms.cjs +70 -0
  58. package/dist/text/atoms.d.cts +22 -0
  59. package/dist/text/atoms.d.ts +22 -0
  60. package/dist/text/atoms.js +56 -0
  61. package/dist/text/style.cjs +173 -0
  62. package/dist/text/style.d.cts +39 -0
  63. package/dist/text/style.d.ts +39 -0
  64. package/dist/text/style.js +165 -0
  65. package/dist/tree-Du_LXAF0.d.cts +17 -0
  66. package/dist/tree-PMcPNgd-.d.ts +17 -0
  67. package/dist/units.cjs +12 -0
  68. package/dist/units.d.cts +6 -0
  69. package/dist/units.d.ts +6 -0
  70. package/dist/units.js +9 -0
  71. package/package.json +93 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joseph Mearman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # ppt-codec
2
+
3
+ [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/documents.js/tree/main/packages/ppt-codec) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/ppt-codec) [![npm version](https://img.shields.io/npm/v/ppt-codec)](https://www.npmjs.com/package/ppt-codec) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/documents.js/ci.yml?branch=main)](https://github.com/ExaDev/documents.js/actions)
4
+
5
+ > A hand-written reader for the PowerPoint 97-2003 binary file format (`.ppt`, [MS-PPT]), producing the same `document-schema.js` presentation content model `ooxml.js`'s pptx support and `odf.js`'s odp support both target. Worker-isomorphic: the same code runs under Node and inside a Cloudflare Workers isolate.
6
+
7
+ Created for [documents.js#817](https://github.com/ExaDev/documents.js/issues/817), part of the legacy-binary-formats epic [#85](https://github.com/ExaDev/documents.js/issues/85). Nothing in the ecosystem read a pre-2007 PowerPoint file: `ooxml.js` reads the XML-based pptx that replaced it, and the two formats share no structure at all beyond both being containers.
8
+
9
+ ## Status
10
+
11
+ **Under active development. The read path for slide text and geometry is built and tested; there is no write path.** What that means concretely is set out in [What it reads](#what-it-reads) and [What it does not read yet](#what-it-does-not-read-yet) below — both lists are exhaustive rather than illustrative, so a caller can tell from this page alone whether the format's own feature it cares about is covered.
12
+
13
+ ## Why the format is shaped the way it is
14
+
15
+ Unlike Word's and Excel's binary formats, whose content is a flat stream of records, a `.ppt` file's content is a **tree** of records, and the tree is not even the whole story:
16
+
17
+ - The file is an [MS-CFB] compound file, whose `PowerPoint Document` stream holds the records and whose `Current User` stream holds a single atom pointing into it.
18
+ - Every record — [MS-PPT]'s own and the [MS-ODRAW] drawing records nested inside it — carries the identical 8-byte header: a 16-bit word packing `recVer` (4 bits) and `recInstance` (12 bits), then `recType` and `recLen`. `recVer == 0xF` marks a container, whose data is more records; anything else marks an atom, whose data is fields. That one distinction is what makes the format a tree rather than a stream, and it is also what lets an unknown record be skipped by seeking `recLen` bytes past its header.
19
+ - The stream is **append-only across edits**. Saving a presentation can append a new _user edit_ rather than rewriting the file, so the same stream can hold several generations of the same slide. Which copy is live is decided by the `Current User` stream's `offsetToCurrentEdit`, the `UserEditAtom` chain it starts, and the persist directory those edits build up — a later edit's directory entry supersedes an earlier one's for the same persist identifier. A reader that simply scanned the stream for `RT_Slide` records would find superseded slides and have no way to tell them from live ones.
20
+ - Slide **placeholder** text is not stored on the slide. A title or body shape's `OfficeArtClientTextbox` holds an `OutlineTextRefAtom` — an index into the text records the _document's_ slide list carries for that slide. Only a plain text box stores its own text.
21
+
22
+ ## Getting started
23
+
24
+ Requires Node.js `>=20` and pnpm `11.6.0`.
25
+
26
+ ```sh
27
+ pnpm install
28
+ pnpm build # tsdown -> dist/ (ESM + CJS + .d.ts, one file set per src module)
29
+ pnpm typecheck # tsc -p tsconfig.json && tsc -p tsconfig.node.json, then attw --pack
30
+ pnpm lint # eslint . --fix --cache --max-warnings 0
31
+ pnpm test # vitest run --project unit
32
+ pnpm test:watch # vitest --project unit
33
+ pnpm test:workers # vitest run --config vitest.workers.config.ts, inside a real Cloudflare Workers (workerd) isolate
34
+ pnpm test:smoke # builds dist/, then loads the built ESM and CJS barrels and every advertised deep import
35
+ ```
36
+
37
+ To run a single test file, pass its path to vitest directly, e.g. `pnpm exec vitest run src/text/style.test.ts`.
38
+
39
+ ## Reading a document
40
+
41
+ ```ts
42
+ import { readPpt, readPptContent } from "ppt-codec";
43
+
44
+ // The tree form: a document-schema.js DocumentTree, the same artefact
45
+ // ooxml.js's readPptx and odf.js's readOdp produce for their own formats.
46
+ const tree = readPpt(pptBytes);
47
+
48
+ // The flat form: metadata plus ContentSlide[], matching the shape
49
+ // readPptxContent and readOdpContent return.
50
+ const { metadata, slides } = readPptContent(pptBytes);
51
+ for (const slide of slides) {
52
+ slide.size; // { widthPt, heightPt }
53
+ for (const shape of slide.shapes) {
54
+ shape.frame; // { xPt, yPt, widthPt, heightPt }
55
+ shape.blocks; // ContentParagraph[], each with its own ContentRun[]
56
+ }
57
+ }
58
+ ```
59
+
60
+ `readPptStreams(currentUserStream, powerPointDocumentStream)` is the same read one level down, for a caller that already holds the two streams — the compound file beneath them is `archive-codec`'s business, and separating the two is what lets every record-level behaviour be tested without a container around it.
61
+
62
+ ## What it reads
63
+
64
+ The whole path from a file's first byte to a slide's text, record by record:
65
+
66
+ | Layer | Records |
67
+ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | Container | The `Current User` and `PowerPoint Document` streams, read through `archive-codec`'s bounded [MS-CFB] reader. |
69
+ | Record framing | The generic 8-byte `RecordHeader`, the container/atom distinction, sibling sequences, child walks, and typed-descendant search — shared with [MS-ODRAW]'s records, which carry the identical header. |
70
+ | Edit resolution | `CurrentUserAtom` (including its encrypted/plaintext `headerToken`), the `UserEditAtom` chain, `PersistDirectoryAtom`/`PersistDirectoryEntry`'s packed 20-bit/12-bit run form, and the oldest-first directory construction whose later entries supersede earlier ones — [MS-PPT] 2.1.2's own "live record" process, Part 1. |
71
+ | Document | `DocumentContainer` → `DocumentAtom` (slide size, in master units), `DocumentTextInfoContainer`'s `FontCollectionContainer`/`FontEntityAtom` typeface names, and `SlideListWithTextContainer` (distinguished from the master and notes lists by `recInstance`, which does not run in the order the names suggest). |
72
+ | Slides | `SlidePersistAtom` → the persist directory → each `SlideContainer`, and the placeholder texts the slide list carries for it. |
73
+ | Drawing | `DrawingContainer` → `OfficeArtDgContainer` → the `OfficeArtSpgrContainer`/`OfficeArtSpContainer` tree, `OfficeArtFSP`'s group/patriarch/deleted flags, `OfficeArtClientAnchor` in both its 8-byte `SmallRectStruct` and 16-byte `RectStruct` spellings, and `OfficeArtChildAnchor` mapped through nested `OfficeArtFSPGR` group coordinate systems. |
74
+ | Text | `OfficeArtClientTextbox`, `TextHeaderAtom`, `TextCharsAtom` (UTF-16) and `TextBytesAtom` (one byte per character), `OutlineTextRefAtom` indirection into the slide list, and the paragraph split on the stored `\r`. |
75
+ | Formatting | `StyleTextPropAtom`: `TextPFRun`/`TextPFException` (indent level, alignment) and `TextCFRun`/`TextCFException` (bold, italic, underline, shadow, emboss, typeface reference, size in points, and a `ColorIndexStruct` colour when it is a literal sRGB value), each read in the spec's **declared field order** rather than its mask-bit order — the two differ, and following the mask-bit order desynchronises every field after the first divergence. |
76
+
77
+ Geometry is converted from master units (1/576 inch) to points on the way out, so a slide's `size` and every shape's `frame` are in the same unit the shared schema uses everywhere else.
78
+
79
+ ## What it does not read yet
80
+
81
+ Each of these is a real construct of the format that this package currently ignores or cannot represent — not a claim that it does not exist:
82
+
83
+ - **Writing.** There is no write path at all: this package reads `.ppt` and does not produce it.
84
+ - **Encrypted documents.** Recognised and refused by name (`PptEncryptedError`) rather than misparsed, but not decrypted.
85
+ - **Speaker notes.** Every slide's `notes` is `""`. Notes live in their own `NotesContainer` persist objects reached through the document's notes list, which is not yet walked.
86
+ - **Document metadata.** `metadata` is always `{}`. Document properties live in the compound file's own `SummaryInformation` stream ([MS-OSHARED]), not in any [MS-PPT] record.
87
+ - **Master and layout inheritance.** A run that states no size, typeface, or weight inherits it from the master's `TextMasterStyleAtom`; this reader reports such a property as absent rather than resolving the cascade, so a run's formatting is what the slide itself states and no more.
88
+ - **Scheme colours.** A `ColorIndexStruct` naming a colour-scheme slot (rather than a literal sRGB value) yields no colour, because resolving it needs the slide's `SlideSchemeColorSchemeAtom`.
89
+ - **Per-shape text insets.** Every shape reports PowerPoint's own defaults (0.1 inch left and right, 0.05 inch top and bottom); a per-shape override lives in the shape's `OfficeArtFOPT` property table, which is not read.
90
+ - **Images, tables, and OLE embeddings.** A picture shape, a table object, and an embedded or linked OLE object all read as a shape with geometry and no blocks. `ExObjListContainer` and the `ExOleObjStg` persist objects are not walked.
91
+ - **Shapes with no anchor.** A shape carrying neither an `OfficeArtClientAnchor` nor an `OfficeArtChildAnchor` is dropped, because `ContentShape` has no way to say "positioned, but unknown where".
92
+ - **Hyperlinks, bullets, spacing and margins.** `InteractiveInfo`/`TextInteractiveInfoAtom`, `TextPFException`'s bullet fields, and its `lineSpacing`/`spaceBefore`/`spaceAfter`/`leftMargin`/`indent` are parsed past correctly but not surfaced.
93
+ - **Animations, transitions, comments, headers and footers, and the metacharacter atoms** (slide number, date, header, footer).
94
+ - **Alignment values the shared schema has no name for.** `Tx_ALIGNDistributed`, `Tx_ALIGNThaiDistributed` and `Tx_ALIGNJustifyLow` map to no alignment rather than being rounded to `justify`.
95
+ - **The soft line break.** U+000B inside a paragraph is converted to a newline, an inference from the spec's own worked examples rather than a rule it states; the specification publishes no table of the special characters a text body may hold.
96
+
97
+ ## Architecture
98
+
99
+ Every module is importable by package-relative path as well as through the barrel — `tsdown` builds one dist file per src module (`root: 'src'`, the layout every sibling codec ships), and `package.json`'s `./*` exports wildcard maps each subpath onto it:
100
+
101
+ ```ts
102
+ import { readRecordAt } from "ppt-codec/record/tree";
103
+ import { readStyleTextPropAtom } from "ppt-codec/text/style";
104
+ ```
105
+
106
+ | Module | What it owns |
107
+ | ------------------------ | -------------------------------------------------------------------------------------------------------------------- |
108
+ | `record/header` | The generic 8-byte record header and the container/atom distinction. |
109
+ | `record/types` | The `RecordType` values this reader dispatches on, plus the [MS-ODRAW] types the drawing walk crosses into. |
110
+ | `record/tree` | Offset-addressed records, sibling sequences, child walks, typed-descendant search. |
111
+ | `stream/current-user` | `CurrentUserAtom`: where the live edit is, and whether the file is encrypted. |
112
+ | `stream/persist` | `UserEditAtom`, `PersistDirectoryAtom`, and the persist directory the edit chain builds. |
113
+ | `document/document-atom` | `DocumentAtom`: slide and notes sizes, master persist references. |
114
+ | `document/fonts` | The font collection, resolved to typeface names a `FontIndexRef` indexes. |
115
+ | `document/slide-list` | `SlideListWithTextContainer`: each slide's persist reference and its placeholder texts. |
116
+ | `drawing/shapes` | The OfficeArt shape tree, flattened, with every anchor resolved into slide coordinates through its enclosing groups. |
117
+ | `text/atoms` | The two text-body spellings, the text-type enumeration, and the paragraph split. |
118
+ | `text/style` | `StyleTextPropAtom`'s two run arrays and their mask-driven exception structures. |
119
+ | `content` | The mapping of PowerPoint's character-counted runs onto the schema's paragraph-owned runs. |
120
+ | `read` | The whole pipeline, and the `readPpt`/`readPptContent`/`readPptStreams` surface. |
121
+ | `units` | Master units to points. |
122
+ | `errors` | `PptFormatError` for malformed input, `PptEncryptedError` for well-formed input this package cannot decrypt. |
123
+
124
+ ### Every fixture is built from the specification, not captured
125
+
126
+ There is no `.ppt` file anywhere in this package's tests. Every fixture is assembled byte by byte from [MS-PPT]'s own field-layout tables, through the builders in `src/test-support/` — including a whole synthetic presentation and a minimal [MS-CFB] writer, so the end-to-end suite exercises the real offset arithmetic (the persist directory, the edit chain, every cross-stream reference) rather than a stubbed one. That is deliberate: a fixture built from the spec's field tables states what the parser is being held to, whereas a captured file would only state what one producer happened to emit, and could not be reduced to the single record under test.
127
+
128
+ ### What it deliberately does not depend on
129
+
130
+ The [MS-CFB] container beneath the format is the one piece not hand-written again: `archive-codec` already owns bounded compound-file reading for the family, and a second implementation here would be exactly the duplication that package's extraction exists to prevent. Everything above it — the record tree, the persist layer, the OfficeArt walk, the text and formatting model — is hand-written against the published specification, the same bet every sibling codec here makes against a heavyweight format library.
131
+
132
+ ## Conventions
133
+
134
+ - Worker-isomorphic (see the [family-wide convention](../../README.md#conventions)): runtime `src/` must not import `node:*`, a bare Node builtin, or use the `Buffer` global — enforced by a `no-restricted-imports`/`no-restricted-globals` ESLint rule and exercised in CI by running a suite inside an actual `workerd` isolate (`pnpm test:workers`). Test files under `src/**/*.test.ts` and `src/test-support/` are exempt and may use Node APIs for fixtures.
135
+ - Only `src/index.ts` may be named `index.*` — a custom ESLint rule (`local/no-non-barrel-index`) rejects any other module using an `index` basename, since that would be a hidden entry point the `exports` map in `package.json` doesn't advertise.
136
+ - Every structural failure throws `PptFormatError` rather than degrading: a malformed file fails whole, never returning a partial slide list that looks complete.
137
+
138
+ ## Specification references
139
+
140
+ Every field layout in this package is taken from a specification page, cited in the source at the point it is used. The load-bearing ones:
141
+
142
+ - [[MS-PPT]: PowerPoint (.ppt) Binary File Format](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/6be79dde-33c1-4c1b-8ccc-4b2301c08662)
143
+ - [[MS-PPT] 2.1.2: PowerPoint Document Stream](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/1fc22d56-28f9-4818-bd45-67c2bf721ccf) — the "live record" process this reader implements
144
+ - [[MS-PPT] 2.3.1: RecordHeader](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/df201194-0cd0-4dfb-bf10-eea353d8eabc)
145
+ - [[MS-PPT] 2.3.2: CurrentUserAtom](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/940d5700-e4d7-4fc0-ab48-fed5dbc48bc1)
146
+ - [[MS-PPT] 2.3.3: UserEditAtom](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/3ffb3fab-95de-4873-98aa-d508fbbac981) and [2.3.5: PersistDirectoryEntry](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/6214b5a6-7ca2-4a86-8a0e-5fd3d3eff1c9)
147
+ - [[MS-PPT] 2.4.1: DocumentContainer](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/6254c4d1-5217-4e16-b20d-c04ddcce31c9) and [2.4.2: DocumentAtom](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/121f2728-3497-4a0a-829e-6f416fee2ee6)
148
+ - [[MS-PPT] 2.4.14.3: SlideListWithTextContainer](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/307e6d12-7304-47a8-acbd-3e7b8041ad3c) and [2.4.14.5: SlidePersistAtom](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/48dce412-9692-4f93-aeb7-3d9fdd3a0a5a)
149
+ - [[MS-PPT] 2.5.1: SlideContainer](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/4cac0976-73d0-4ab3-a70b-e98b3cf1c312) and [2.5.13: DrawingContainer](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/0595b49f-da96-4402-b353-1f766e9d548f)
150
+ - [[MS-PPT] 2.7.1: OfficeArtClientAnchor](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/37ee18c7-3c7c-4adc-91fb-cb3b01789d72), [SmallRectStruct](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/e47cb973-8480-4995-90b2-008bcb2ffc65), [RectStruct](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/8a58e3ae-2682-42d0-82cd-a41c2999584e)
151
+ - [[MS-PPT] 2.9.76: OfficeArtClientTextbox](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/f50070dd-a4dc-4edd-a446-c4fcc5c80ace), [TextHeaderAtom](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/08d31a66-0750-4009-b416-49f2871cd178), [TextCharsAtom](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/a3c5c8d5-e530-4167-a242-7743bc99aeac), [TextBytesAtom](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/80aae34b-2699-43fa-9e6a-c560ae790cd7)
152
+ - [[MS-PPT]: StyleTextPropAtom](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/a9a5fa71-238d-491e-acc7-fa1fffd5f100), [TextPFException](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/c15a13b3-db2c-4b50-a7e6-08045581a663), [PFMasks](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/2a02831a-088b-44e7-84c9-c185ab314a71), [TextCFException](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/c75024a2-14cb-4d7d-9964-bdab2fcd9d93), [CFMasks](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/bbca8581-d011-4293-a375-b209523cf962), [CFStyle](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/3ea010b9-0ef9-4c05-9982-618130ca66cd), [ColorIndexStruct](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/5d6b0509-f3c7-435f-9bf4-6f1fc5f8293c)
153
+ - [[MS-ODRAW]: Office Drawing Binary File Format](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-odraw/8560795e-7759-4745-838f-f7f2ef2f1872) — [OfficeArtSpContainer](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-odraw/16194cb9-b4b0-476c-9678-a6ac1f06b034), [OfficeArtFSP](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-odraw/8a7e7be3-0582-4461-9400-29d7eda8497d), [OfficeArtFSPGR](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-odraw/82d2d6a1-3a7a-4d15-9803-33145a76545a), [OfficeArtChildAnchor](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-odraw/33a44593-02df-4684-ab35-5a7c4a9bcaac)
154
+ - [[MS-CFB]: Compound File Binary File Format](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b) — the container, read through `archive-codec`
155
+
156
+ ## Install
157
+
158
+ ```sh
159
+ pnpm add ppt-codec
160
+ # or
161
+ npm install ppt-codec
162
+ ```
163
+
164
+ ## Release and publishing
165
+
166
+ Release, CI, and commit-message conventions are all workspace-wide, not package-local — see the [monorepo root README](../../README.md#releases) for the mechanism.
167
+
168
+ ## Contributing
169
+
170
+ Conventional Commits, enforced workspace-wide by commitlint through a root `commit-msg` hook. Work inside `packages/ppt-codec/`; see [CONTRIBUTING.md](../../CONTRIBUTING.md) for the shared git hooks and history conventions.
171
+
172
+ ## License
173
+
174
+ MIT
@@ -0,0 +1,72 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_text_atoms = require("./text/atoms.cjs");
3
+ require("./text/style.cjs");
4
+ //#region src/content.ts
5
+ const BYTE_MAX = 255;
6
+ function mapAlignment(alignment) {
7
+ switch (alignment) {
8
+ case 0: return "left";
9
+ case 1: return "center";
10
+ case 2: return "right";
11
+ case 3: return "justify";
12
+ default: return;
13
+ }
14
+ }
15
+ function mapColor(color) {
16
+ if (color === void 0) return;
17
+ return {
18
+ r: color.red / BYTE_MAX,
19
+ g: color.green / BYTE_MAX,
20
+ b: color.blue / BYTE_MAX
21
+ };
22
+ }
23
+ function toExtents(runs) {
24
+ const extents = [];
25
+ let at = 0;
26
+ for (const run of runs) {
27
+ extents.push({
28
+ start: at,
29
+ end: at + run.count,
30
+ properties: run.properties
31
+ });
32
+ at += run.count;
33
+ }
34
+ return extents;
35
+ }
36
+ function runFrom(text, properties, fontNames) {
37
+ return {
38
+ text,
39
+ bold: properties.bold,
40
+ italic: properties.italic,
41
+ underline: properties.underline,
42
+ fontFamily: properties.fontRef === void 0 ? void 0 : fontNames[properties.fontRef],
43
+ sizePt: properties.sizePt,
44
+ color: mapColor(properties.color)
45
+ };
46
+ }
47
+ function buildParagraphs(text, style, fontNames) {
48
+ const paragraphExtents = toExtents(style.paragraphRuns);
49
+ const characterExtents = toExtents(style.characterRuns);
50
+ return require_text_atoms.splitParagraphs(text).map((paragraph) => {
51
+ const end = paragraph.start + paragraph.text.length;
52
+ const paragraphProperties = paragraphExtents.find((extent) => paragraph.start >= extent.start && paragraph.start < extent.end)?.properties;
53
+ const runs = [];
54
+ for (const extent of characterExtents) {
55
+ const from = Math.max(extent.start, paragraph.start);
56
+ const to = Math.min(extent.end, end);
57
+ if (from >= to) continue;
58
+ runs.push(runFrom(paragraph.text.slice(from - paragraph.start, to - paragraph.start), extent.properties, fontNames));
59
+ }
60
+ if (runs.length === 0 && paragraph.text.length > 0) runs.push({ text: paragraph.text });
61
+ const alignment = mapAlignment(paragraphProperties?.alignment);
62
+ const indentLevel = paragraphProperties?.indentLevel ?? 0;
63
+ return {
64
+ kind: "paragraph",
65
+ runs,
66
+ alignment,
67
+ list: indentLevel > 0 ? { level: indentLevel } : void 0
68
+ };
69
+ });
70
+ }
71
+ //#endregion
72
+ exports.buildParagraphs = buildParagraphs;
@@ -0,0 +1,6 @@
1
+ import { StyleTextProps } from "./text/style.cjs";
2
+ import { ContentParagraph } from "document-schema.js";
3
+ //#region src/content.d.ts
4
+ declare function buildParagraphs(text: string, style: StyleTextProps, fontNames: readonly string[]): ContentParagraph[];
5
+ //#endregion
6
+ export { buildParagraphs };
@@ -0,0 +1,6 @@
1
+ import { StyleTextProps } from "./text/style.js";
2
+ import { ContentParagraph } from "document-schema.js";
3
+ //#region src/content.d.ts
4
+ declare function buildParagraphs(text: string, style: StyleTextProps, fontNames: readonly string[]): ContentParagraph[];
5
+ //#endregion
6
+ export { buildParagraphs };
@@ -0,0 +1,71 @@
1
+ import { splitParagraphs } from "./text/atoms.js";
2
+ import "./text/style.js";
3
+ //#region src/content.ts
4
+ const BYTE_MAX = 255;
5
+ function mapAlignment(alignment) {
6
+ switch (alignment) {
7
+ case 0: return "left";
8
+ case 1: return "center";
9
+ case 2: return "right";
10
+ case 3: return "justify";
11
+ default: return;
12
+ }
13
+ }
14
+ function mapColor(color) {
15
+ if (color === void 0) return;
16
+ return {
17
+ r: color.red / BYTE_MAX,
18
+ g: color.green / BYTE_MAX,
19
+ b: color.blue / BYTE_MAX
20
+ };
21
+ }
22
+ function toExtents(runs) {
23
+ const extents = [];
24
+ let at = 0;
25
+ for (const run of runs) {
26
+ extents.push({
27
+ start: at,
28
+ end: at + run.count,
29
+ properties: run.properties
30
+ });
31
+ at += run.count;
32
+ }
33
+ return extents;
34
+ }
35
+ function runFrom(text, properties, fontNames) {
36
+ return {
37
+ text,
38
+ bold: properties.bold,
39
+ italic: properties.italic,
40
+ underline: properties.underline,
41
+ fontFamily: properties.fontRef === void 0 ? void 0 : fontNames[properties.fontRef],
42
+ sizePt: properties.sizePt,
43
+ color: mapColor(properties.color)
44
+ };
45
+ }
46
+ function buildParagraphs(text, style, fontNames) {
47
+ const paragraphExtents = toExtents(style.paragraphRuns);
48
+ const characterExtents = toExtents(style.characterRuns);
49
+ return splitParagraphs(text).map((paragraph) => {
50
+ const end = paragraph.start + paragraph.text.length;
51
+ const paragraphProperties = paragraphExtents.find((extent) => paragraph.start >= extent.start && paragraph.start < extent.end)?.properties;
52
+ const runs = [];
53
+ for (const extent of characterExtents) {
54
+ const from = Math.max(extent.start, paragraph.start);
55
+ const to = Math.min(extent.end, end);
56
+ if (from >= to) continue;
57
+ runs.push(runFrom(paragraph.text.slice(from - paragraph.start, to - paragraph.start), extent.properties, fontNames));
58
+ }
59
+ if (runs.length === 0 && paragraph.text.length > 0) runs.push({ text: paragraph.text });
60
+ const alignment = mapAlignment(paragraphProperties?.alignment);
61
+ const indentLevel = paragraphProperties?.indentLevel ?? 0;
62
+ return {
63
+ kind: "paragraph",
64
+ runs,
65
+ alignment,
66
+ list: indentLevel > 0 ? { level: indentLevel } : void 0
67
+ };
68
+ });
69
+ }
70
+ //#endregion
71
+ export { buildParagraphs };
@@ -0,0 +1,27 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_errors = require("../errors.cjs");
3
+ const require_record_types = require("../record/types.cjs");
4
+ //#region src/document/document-atom.ts
5
+ const DOCUMENT_ATOM_LEN = 40;
6
+ function readDocumentAtom(record) {
7
+ if (record.header.recType !== 1001) throw new require_errors.PptFormatError(`expected RT_DocumentAtom (0x${require_record_types.RT_DocumentAtom.toString(16)}), found record type 0x${record.header.recType.toString(16)}`);
8
+ if (record.data.length < DOCUMENT_ATOM_LEN) throw new require_errors.PptFormatError(`DocumentAtom carries ${record.data.length} bytes, fewer than the mandated 0x${DOCUMENT_ATOM_LEN.toString(16)}`);
9
+ const { data } = record;
10
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
11
+ return {
12
+ slideSize: {
13
+ x: view.getInt32(0, true),
14
+ y: view.getInt32(4, true)
15
+ },
16
+ notesSize: {
17
+ x: view.getInt32(8, true),
18
+ y: view.getInt32(12, true)
19
+ },
20
+ notesMasterPersistIdRef: view.getUint32(24, true),
21
+ handoutMasterPersistIdRef: view.getUint32(28, true),
22
+ firstSlideNumber: view.getUint16(32, true),
23
+ slideSizeType: view.getUint16(34, true)
24
+ };
25
+ }
26
+ //#endregion
27
+ exports.readDocumentAtom = readDocumentAtom;
@@ -0,0 +1,17 @@
1
+ import { t as PptRecord } from "../tree-Du_LXAF0.cjs";
2
+ //#region src/document/document-atom.d.ts
3
+ interface PointStruct {
4
+ readonly x: number;
5
+ readonly y: number;
6
+ }
7
+ interface DocumentAtom {
8
+ readonly slideSize: PointStruct;
9
+ readonly notesSize: PointStruct;
10
+ readonly notesMasterPersistIdRef: number;
11
+ readonly handoutMasterPersistIdRef: number;
12
+ readonly firstSlideNumber: number;
13
+ readonly slideSizeType: number;
14
+ }
15
+ declare function readDocumentAtom(record: PptRecord): DocumentAtom;
16
+ //#endregion
17
+ export { DocumentAtom, PointStruct, readDocumentAtom };
@@ -0,0 +1,17 @@
1
+ import { t as PptRecord } from "../tree-PMcPNgd-.js";
2
+ //#region src/document/document-atom.d.ts
3
+ interface PointStruct {
4
+ readonly x: number;
5
+ readonly y: number;
6
+ }
7
+ interface DocumentAtom {
8
+ readonly slideSize: PointStruct;
9
+ readonly notesSize: PointStruct;
10
+ readonly notesMasterPersistIdRef: number;
11
+ readonly handoutMasterPersistIdRef: number;
12
+ readonly firstSlideNumber: number;
13
+ readonly slideSizeType: number;
14
+ }
15
+ declare function readDocumentAtom(record: PptRecord): DocumentAtom;
16
+ //#endregion
17
+ export { DocumentAtom, PointStruct, readDocumentAtom };
@@ -0,0 +1,26 @@
1
+ import { PptFormatError } from "../errors.js";
2
+ import { RT_DocumentAtom } from "../record/types.js";
3
+ //#region src/document/document-atom.ts
4
+ const DOCUMENT_ATOM_LEN = 40;
5
+ function readDocumentAtom(record) {
6
+ if (record.header.recType !== 1001) throw new PptFormatError(`expected RT_DocumentAtom (0x${RT_DocumentAtom.toString(16)}), found record type 0x${record.header.recType.toString(16)}`);
7
+ if (record.data.length < DOCUMENT_ATOM_LEN) throw new PptFormatError(`DocumentAtom carries ${record.data.length} bytes, fewer than the mandated 0x${DOCUMENT_ATOM_LEN.toString(16)}`);
8
+ const { data } = record;
9
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
10
+ return {
11
+ slideSize: {
12
+ x: view.getInt32(0, true),
13
+ y: view.getInt32(4, true)
14
+ },
15
+ notesSize: {
16
+ x: view.getInt32(8, true),
17
+ y: view.getInt32(12, true)
18
+ },
19
+ notesMasterPersistIdRef: view.getUint32(24, true),
20
+ handoutMasterPersistIdRef: view.getUint32(28, true),
21
+ firstSlideNumber: view.getUint16(32, true),
22
+ slideSizeType: view.getUint16(34, true)
23
+ };
24
+ }
25
+ //#endregion
26
+ export { readDocumentAtom };
@@ -0,0 +1,29 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_record_tree = require("../record/tree.cjs");
3
+ const require_record_types = require("../record/types.cjs");
4
+ //#region src/document/fonts.ts
5
+ const FACE_NAME_BYTES = 64;
6
+ function readFaceName(record) {
7
+ if (record.data.length < FACE_NAME_BYTES) return;
8
+ const view = new DataView(record.data.buffer, record.data.byteOffset, record.data.byteLength);
9
+ let name = "";
10
+ for (let at = 0; at < FACE_NAME_BYTES; at += 2) {
11
+ const unit = view.getUint16(at, true);
12
+ if (unit === 0) break;
13
+ name += String.fromCharCode(unit);
14
+ }
15
+ return name;
16
+ }
17
+ function readFontNames(environment) {
18
+ const collection = require_record_tree.findChild(require_record_tree.childRecords(environment), require_record_types.RT_FontCollection);
19
+ if (collection === void 0) return [];
20
+ const names = [];
21
+ for (const entry of require_record_tree.childRecords(collection)) {
22
+ if (entry.header.recType !== 4023) continue;
23
+ const name = readFaceName(entry);
24
+ if (name !== void 0) names.push(name);
25
+ }
26
+ return names;
27
+ }
28
+ //#endregion
29
+ exports.readFontNames = readFontNames;
@@ -0,0 +1,5 @@
1
+ import { t as PptRecord } from "../tree-Du_LXAF0.cjs";
2
+ //#region src/document/fonts.d.ts
3
+ declare function readFontNames(environment: PptRecord): string[];
4
+ //#endregion
5
+ export { readFontNames };
@@ -0,0 +1,5 @@
1
+ import { t as PptRecord } from "../tree-PMcPNgd-.js";
2
+ //#region src/document/fonts.d.ts
3
+ declare function readFontNames(environment: PptRecord): string[];
4
+ //#endregion
5
+ export { readFontNames };
@@ -0,0 +1,28 @@
1
+ import { childRecords, findChild } from "../record/tree.js";
2
+ import { RT_FontCollection } from "../record/types.js";
3
+ //#region src/document/fonts.ts
4
+ const FACE_NAME_BYTES = 64;
5
+ function readFaceName(record) {
6
+ if (record.data.length < FACE_NAME_BYTES) return;
7
+ const view = new DataView(record.data.buffer, record.data.byteOffset, record.data.byteLength);
8
+ let name = "";
9
+ for (let at = 0; at < FACE_NAME_BYTES; at += 2) {
10
+ const unit = view.getUint16(at, true);
11
+ if (unit === 0) break;
12
+ name += String.fromCharCode(unit);
13
+ }
14
+ return name;
15
+ }
16
+ function readFontNames(environment) {
17
+ const collection = findChild(childRecords(environment), RT_FontCollection);
18
+ if (collection === void 0) return [];
19
+ const names = [];
20
+ for (const entry of childRecords(collection)) {
21
+ if (entry.header.recType !== 4023) continue;
22
+ const name = readFaceName(entry);
23
+ if (name !== void 0) names.push(name);
24
+ }
25
+ return names;
26
+ }
27
+ //#endregion
28
+ export { readFontNames };
@@ -0,0 +1,40 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_errors = require("../errors.cjs");
3
+ const require_record_tree = require("../record/tree.cjs");
4
+ const require_record_types = require("../record/types.cjs");
5
+ const require_text_atoms = require("../text/atoms.cjs");
6
+ //#region src/document/slide-list.ts
7
+ const SLIDE_PERSIST_ATOM_LEN = 20;
8
+ function readSlideListWithText(listContainer) {
9
+ if (listContainer.header.recType !== 4080) throw new require_errors.PptFormatError(`expected RT_SlideListWithText (0x${require_record_types.RT_SlideListWithText.toString(16)}), found record type 0x${listContainer.header.recType.toString(16)}`);
10
+ const slides = [];
11
+ let currentSlide;
12
+ let currentText;
13
+ for (const record of require_record_tree.childRecords(listContainer)) {
14
+ if (record.header.recType === 1011) {
15
+ if (record.data.length < SLIDE_PERSIST_ATOM_LEN) throw new require_errors.PptFormatError(`SlidePersistAtom at offset ${record.offset} carries ${record.data.length} bytes, fewer than the mandated 0x${SLIDE_PERSIST_ATOM_LEN.toString(16)}`);
16
+ const view = new DataView(record.data.buffer, record.data.byteOffset, record.data.byteLength);
17
+ currentSlide = {
18
+ persistIdRef: view.getUint32(0, true),
19
+ slideId: view.getUint32(12, true),
20
+ texts: []
21
+ };
22
+ currentText = void 0;
23
+ slides.push(currentSlide);
24
+ continue;
25
+ }
26
+ if (record.header.recType === 3999) {
27
+ if (currentSlide === void 0) throw new require_errors.PptFormatError(`TextHeaderAtom at offset ${record.offset} precedes any SlidePersistAtom, so it belongs to no slide`);
28
+ currentText = {
29
+ textType: require_text_atoms.readTextHeaderAtom(record),
30
+ records: []
31
+ };
32
+ currentSlide.texts.push(currentText);
33
+ continue;
34
+ }
35
+ currentText?.records.push(record);
36
+ }
37
+ return slides;
38
+ }
39
+ //#endregion
40
+ exports.readSlideListWithText = readSlideListWithText;
@@ -0,0 +1,14 @@
1
+ import { t as PptRecord } from "../tree-Du_LXAF0.cjs";
2
+ //#region src/document/slide-list.d.ts
3
+ interface OutlineText {
4
+ readonly textType: number;
5
+ readonly records: readonly PptRecord[];
6
+ }
7
+ interface SlidePersist {
8
+ readonly persistIdRef: number;
9
+ readonly slideId: number;
10
+ readonly texts: readonly OutlineText[];
11
+ }
12
+ declare function readSlideListWithText(listContainer: PptRecord): SlidePersist[];
13
+ //#endregion
14
+ export { OutlineText, SlidePersist, readSlideListWithText };
@@ -0,0 +1,14 @@
1
+ import { t as PptRecord } from "../tree-PMcPNgd-.js";
2
+ //#region src/document/slide-list.d.ts
3
+ interface OutlineText {
4
+ readonly textType: number;
5
+ readonly records: readonly PptRecord[];
6
+ }
7
+ interface SlidePersist {
8
+ readonly persistIdRef: number;
9
+ readonly slideId: number;
10
+ readonly texts: readonly OutlineText[];
11
+ }
12
+ declare function readSlideListWithText(listContainer: PptRecord): SlidePersist[];
13
+ //#endregion
14
+ export { OutlineText, SlidePersist, readSlideListWithText };