ppt-codec 1.2.13 → 1.4.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.
package/README.md CHANGED
@@ -57,7 +57,51 @@ for (const slide of slides) {
57
57
  }
58
58
  ```
59
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.
60
+ `readPptStreams(currentUserStream, powerPointDocumentStream, password)` 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
+ ## Encryption
63
+
64
+ A `.ppt` protected with a password to open uses [MS-OFFCRYPTO] 2.3.5 "RC4 CryptoAPI Encryption" — genuinely different from the MD5-based scheme `xls-codec` and `doc-codec` share (ExaDev/documents.js#1108/#1113): SHA-1-based key derivation with no intermediate-hash iteration, and re-keying per persist object rather than at a fixed byte interval. `readPpt`/`readPptContent`/`readPptStreams` take an optional `password`, ignored for an unencrypted presentation; a missing or incorrect password against an encrypted one throws `PptEncryptedError` rather than returning a partial or garbled document, and so does an encryption shape this package does not implement (anything other than RC4 CryptoAPI — [MS-PPT] itself never specifies any other scheme for the binary format).
65
+
66
+ ```ts
67
+ import { readPptContent } from "ppt-codec";
68
+
69
+ const { metadata, slides } = readPptContent(
70
+ pptBytes,
71
+ "correct horse battery staple",
72
+ );
73
+ ```
74
+
75
+ `archive-codec`'s `crypto/office-rc4-cryptoapi` module (ExaDev/documents.js#1116) carries the key derivation and password verification; this package's own `src/encryption.ts` carries the container-specific pieces, which differ from both xls-codec's and doc-codec's shared scheme in three real ways:
76
+
77
+ - **Location.** There is no fixed-offset header at all. The `DocumentEncryptionAtom` (`RT_CryptSession10Container`, [MS-PPT]'s own name for record type 0x2F14) is just another persist object, reached only by walking the current `UserEditAtom.encryptSessionPersistIdRef` through the same persist directory every other record uses.
78
+ - **Re-keying granularity.** Each top-level persist object gets its own RC4 key, derived from its own persist ID as the "block number" — not a running byte offset within one continuous stream, the way both `xls-codec`'s FilePass scheme and `doc-codec`'s own EncryptionHeader scheme re-key.
79
+ - **Encrypted headers.** A persist object's own 8-byte record header is encrypted along with its data, unlike the never-encrypted headers the shared xls/doc scheme leaves alone — `decryptPptDocumentStream` decrypts a peek of those 8 bytes first, under the object's own key, to learn its real length before decrypting the object in full.
80
+
81
+ Pictures in a separate `Pictures` stream are also RC4 CryptoAPI-encrypted per [MS-PPT], but this package does not read the `Pictures` stream at all today (see [Images, tables, and OLE embeddings](#what-it-does-not-read-yet)), so decrypting it is out of scope until something needs to.
82
+
83
+ `writePptContent` never encrypts.
84
+
85
+ ## Master and colour inheritance
86
+
87
+ A real `.ppt` deck overwhelmingly relies on master-inherited formatting rather than direct, per-run formatting: a title placeholder typically states its own text and nothing else, leaving size, typeface, weight, and colour entirely to its slide's own master. `readPptStreams`/`readPptContent`/`readPpt` resolve this cascade for every run, not just report the run's own direct formatting as before.
88
+
89
+ ```ts
90
+ import { readPptContent } from "ppt-codec";
91
+
92
+ const { slides } = readPptContent(pptBytes);
93
+ // A run whose own TextCFException states neither bold nor colour still
94
+ // reports both here, resolved against its master and the slide's own
95
+ // colour scheme.
96
+ slides[0]?.shapes[0]?.blocks[0]?.runs[0]?.bold;
97
+ slides[0]?.shapes[0]?.blocks[0]?.runs[0]?.color;
98
+ ```
99
+
100
+ **Text-formatting cascade** (`document/master.ts`): a run's own `TextPFException`/`TextCFException` field wins outright when it states one. For everything it leaves unstated, resolution walks the applicable `TextMasterStyleAtom` from the run's own (clamped) outline level down to level 0, then -- if the run's own `TextTypeEnum` is a variant with no cascade of its own (`CENTER_BODY`/`HALF_BODY`/`QUARTER_BODY` retry as plain `BODY`; `CENTER_TITLE` retries as plain `TITLE`) -- repeats the same walk against the fallback type. `NOTES` and `OTHER` have no further fallback. A `TextTypeEnum` a slide's own master carries no atom for falls back to the single document-wide default `TextMasterStyleAtom` [MS-PPT] 2.9.35 states lives inside `DocumentTextInfoContainer` (a child of `Environment`), the fallback of last resort for every type.
101
+
102
+ **Multi-master resolution**: unlike this package's own writer, which only ever produces one `MainMasterContainer`, a real file can carry several (different design templates within one deck). Every slide's own `SlideAtom.masterIdRef` is read and resolved against the master list, not assumed to be the deck's only master.
103
+
104
+ **Colour-scheme resolution** (`document/color-scheme.ts`) is a separate, simpler step: a `ColorIndexStruct` naming a colour-scheme slot (`0x00`-`0x07` -- background, text, shadow, title text, fill, Accent 1, Accent 2, Accent 3) resolves against the slide's own `SlideSchemeColorSchemeAtom` when it carries one, or its master's otherwise. Unlike text formatting, this never walks a master-level cascade: [MS-PPT] mandates every slide-shaped container carry its own complete colour scheme (a slide that visually "follows the master's scheme" does so by a real producer duplicating the master's own values into it), so a slide's own scheme is the only place this reader looks first.
61
105
 
62
106
  ## Writing a document
63
107
 
@@ -79,17 +123,19 @@ const bytes = writePptContent({ metadata: {}, slides });
79
123
 
80
124
  The whole path from a file's first byte to a slide's text, record by record:
81
125
 
82
- | Layer | Records |
83
- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
84
- | Container | The `Current User` and `PowerPoint Document` streams, read through `archive-codec`'s bounded [MS-CFB] reader. |
85
- | 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. |
86
- | 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. |
87
- | 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). |
88
- | Slides | `SlidePersistAtom` → the persist directory each `SlideContainer`, and the placeholder texts the slide list carries for it. |
89
- | Speaker notes | `NotesListWithTextContainer` (the third of the three containers sharing `RT_SlideListWithText`) → `NotesPersistAtom` → the persist directory → each `NotesContainer`, and the `NotesAtom.slideIdRef` naming the presentation slide those notes belong to. The text comes from the notes slide's own drawing, since the notes list unlike the slide list — carries no texts for an `OutlineTextRefAtom` to reach into. |
90
- | 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. |
91
- | 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`. |
92
- | Formatting | `StyleTextPropAtom`: `TextPFRun`/`TextPFException` (indent level, alignment, line spacing, space before/after, left margin, and first-line indent) 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. |
126
+ | Layer | Records |
127
+ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
128
+ | Container | The `Current User` and `PowerPoint Document` streams, read through `archive-codec`'s bounded [MS-CFB] reader. |
129
+ | 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. |
130
+ | 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. |
131
+ | Encryption | `DocumentEncryptionAtom` (`UserEditAtom.encryptSessionPersistIdRef` the persist directory), [MS-OFFCRYPTO] 2.3.5's RC4 CryptoAPI scheme see [Encryption](#encryption). |
132
+ | Document | `DocumentContainer` → `DocumentAtom` (slide size, in master units), `DocumentTextInfoContainer`'s `FontCollectionContainer`/`FontEntityAtom` typeface names and its own document-default `TextMasterStyleAtom`, and `SlideListWithTextContainer` (distinguished from the master and notes lists by `recInstance`, which does not run in the order the names suggest). |
133
+ | Masters | `MasterListWithTextContainer` → `MasterPersistAtom` → the persist directory → each `MainMasterContainer`'s own `TextMasterStyleAtom` items and `SlideSchemeColorSchemeAtom` a real file can carry more than one master, and `SlideAtom.masterIdRef` decides which one a given slide actually follows. |
134
+ | Slides | `SlidePersistAtom` → the persist directory each `SlideContainer`'s own `SlideAtom` (`masterIdRef`, `notesIdRef`) and drawing, and the placeholder texts the slide list carries for it. |
135
+ | Speaker notes | `NotesListWithTextContainer` (the third of the three containers sharing `RT_SlideListWithText`) `NotesPersistAtom` the persist directory → each `NotesContainer`, and the `NotesAtom.slideIdRef` naming the presentation slide those notes belong to. The text comes from the notes slide's own drawing, since the notes list unlike the slide list — carries no texts for an `OutlineTextRefAtom` to reach into. |
136
+ | 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. |
137
+ | 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`. |
138
+ | Formatting | `StyleTextPropAtom`: `TextPFRun`/`TextPFException` (indent level, alignment, line spacing, space before/after, left margin, and first-line indent) and `TextCFRun`/`TextCFException` (bold, italic, underline, shadow, emboss, typeface reference, size in points, and a `ColorIndexStruct` colour — literal sRGB or a colour-scheme slot reference), 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. A field a run states neither directly nor at all resolves against the applicable master's own cascade and colour scheme — see [Master and colour inheritance](#master-and-colour-inheritance). |
93
139
 
94
140
  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.
95
141
 
@@ -97,10 +143,7 @@ Geometry is converted from master units (1/576 inch) to points on the way out, s
97
143
 
98
144
  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:
99
145
 
100
- - **Encrypted documents.** Recognised and refused by name (`PptEncryptedError`) rather than misparsed, but not decrypted.
101
146
  - **`DocumentSummaryInformation`'s extended and user-defined properties** (company, manager, custom properties) — a genuinely different stream from the one [Metadata](#metadata) covers, not attempted at all.
102
- - **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.
103
- - **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`.
104
147
  - **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.
105
148
  - **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.
106
149
  - **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".
@@ -198,6 +241,7 @@ import { readStyleTextPropAtom } from "ppt-codec/text/style";
198
241
  | `stream/current-user-write` | Writes a real `CurrentUserAtom` pointing at the single edit this writer always produces. |
199
242
  | `stream/persist` | `UserEditAtom`, `PersistDirectoryAtom`, and the persist directory the edit chain builds. |
200
243
  | `stream/persist-write` | Writes a single-edit `UserEditAtom`/`PersistDirectoryAtom` pair covering the document container and every slide container. |
244
+ | `encryption` | `readDocumentEncryptionAtom`, `decryptPptDocumentStream` — [MS-OFFCRYPTO] 2.3.5 RC4 CryptoAPI decryption, wired against `archive-codec`'s own key derivation (see [Encryption](#encryption)). |
201
245
  | `document/document-atom` | `DocumentAtom`: slide and notes sizes, master persist references. |
202
246
  | `document/document-atom-write` | Writes a `DocumentAtom` for the one slide size every slide must share. |
203
247
  | `document/fonts` | The font collection, resolved to typeface names a `FontIndexRef` indexes. |
@@ -208,7 +252,9 @@ import { readStyleTextPropAtom } from "ppt-codec/text/style";
208
252
  | `document/notes-list-write` | Writes that container from the same `NotesPersist` shape the reader produces. |
209
253
  | `document/notes` | A `NotesContainer`: its `NotesAtom` (which slide the notes belong to) and the notes text its drawing carries. |
210
254
  | `document/notes-write` | Writes a `NotesContainer` for one slide's notes, through the same drawing writer a slide's own shapes go through. |
211
- | `document/master-write` | The minimal `MainMasterContainer` and `MasterListWithTextContainer`, plus the `SlideAtom` every slide needs in order to name that master and its own notes slide. Write-only: nothing reads a `SlideAtom` yet. |
255
+ | `document/master` | `readSlideAtom` (`masterIdRef`/`notesIdRef`), `buildMasterStyleTable`, and `resolveCharacterProperties`/`resolveParagraphProperties` the master text-formatting cascade a run whose own fields are absent resolves against (see [Master and colour inheritance](#master-and-colour-inheritance)). |
256
+ | `document/master-write` | The minimal `MainMasterContainer` and `MasterListWithTextContainer`, plus the `SlideAtom` every slide needs in order to name that master and its own notes slide. |
257
+ | `document/color-scheme` | `readSlideSchemeColorSchemeAtom`, `resolveSchemeColor` — resolves a `ColorIndexStruct`'s scheme-slot reference against the slide's (or its master's) own colour scheme. |
212
258
  | `document/color-scheme-write` | The `SlideSchemeColorSchemeAtom` [MS-PPT] 2.9.51 gives four different containers, two of which this writer produces — the main master and every notes slide — so it belongs to neither of them. |
213
259
  | `drawing/shapes` | The OfficeArt shape tree, flattened, with every anchor resolved into slide coordinates through its enclosing groups. |
214
260
  | `drawing/shapes-write` | Writes the patriarch group and one plain, anchored `OfficeArtSpContainer` per shape. |
@@ -221,7 +267,7 @@ import { readStyleTextPropAtom } from "ppt-codec/text/style";
221
267
  | `read` | The whole read pipeline, and the `readPpt`/`readPptContent`/`readPptStreams` surface. |
222
268
  | `write` | The whole write pipeline, and the `writePpt`/`writePptContent`/`writePptStreams` surface. |
223
269
  | `units` | Master units to points, and points to master units. |
224
- | `errors` | `PptFormatError` for malformed input, `PptEncryptedError` for well-formed input this package cannot decrypt, `PptUnsupportedContentError` for well-formed content this package's writer cannot express. |
270
+ | `errors` | `PptFormatError` for malformed input, `PptEncryptedError` for encrypted input given no password, an incorrect one, or an encryption scheme this package does not implement (anything other than RC4 CryptoAPI), `PptUnsupportedContentError` for well-formed content this package's writer cannot express. |
225
271
 
226
272
  ### Every fixture is built from the specification, not captured
227
273
 
@@ -24,9 +24,12 @@ function pointsToParaSpacing(pt) {
24
24
  function mapColorToPpt(color) {
25
25
  if (color === void 0) return;
26
26
  return {
27
- red: Math.round(color.r * BYTE_MAX),
28
- green: Math.round(color.g * BYTE_MAX),
29
- blue: Math.round(color.b * BYTE_MAX)
27
+ kind: "rgb",
28
+ rgb: {
29
+ red: Math.round(color.r * BYTE_MAX),
30
+ green: Math.round(color.g * BYTE_MAX),
31
+ blue: Math.round(color.b * BYTE_MAX)
32
+ }
30
33
  };
31
34
  }
32
35
  function storedRunText(text) {
@@ -23,9 +23,12 @@ function pointsToParaSpacing(pt) {
23
23
  function mapColorToPpt(color) {
24
24
  if (color === void 0) return;
25
25
  return {
26
- red: Math.round(color.r * BYTE_MAX),
27
- green: Math.round(color.g * BYTE_MAX),
28
- blue: Math.round(color.b * BYTE_MAX)
26
+ kind: "rgb",
27
+ rgb: {
28
+ red: Math.round(color.r * BYTE_MAX),
29
+ green: Math.round(color.g * BYTE_MAX),
30
+ blue: Math.round(color.b * BYTE_MAX)
31
+ }
29
32
  };
30
33
  }
31
34
  function storedRunText(text) {
package/dist/content.cjs CHANGED
@@ -2,6 +2,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("./text/style.cjs");
3
3
  const require_text_atoms = require("./text/atoms.cjs");
4
4
  const require_units = require("./units.cjs");
5
+ const require_document_color_scheme = require("./document/color-scheme.cjs");
6
+ const require_document_master = require("./document/master.cjs");
5
7
  //#region src/content.ts
6
8
  const BYTE_MAX = 255;
7
9
  function mapAlignment(alignment) {
@@ -35,6 +37,10 @@ function mapColor(color) {
35
37
  b: color.blue / BYTE_MAX
36
38
  };
37
39
  }
40
+ function resolveRunColor(color, colorScheme) {
41
+ if (color === void 0) return;
42
+ return color.kind === "rgb" ? color.rgb : require_document_color_scheme.resolveSchemeColor(color.schemeIndex, colorScheme);
43
+ }
38
44
  function toExtents(runs) {
39
45
  const extents = [];
40
46
  let at = 0;
@@ -48,7 +54,7 @@ function toExtents(runs) {
48
54
  }
49
55
  return extents;
50
56
  }
51
- function runFrom(text, properties, fontNames) {
57
+ function runFrom(text, properties, fontNames, colorScheme) {
52
58
  return {
53
59
  text,
54
60
  bold: properties.bold,
@@ -56,35 +62,39 @@ function runFrom(text, properties, fontNames) {
56
62
  underline: properties.underline,
57
63
  fontFamily: properties.fontRef === void 0 ? void 0 : fontNames[properties.fontRef],
58
64
  sizePt: properties.sizePt,
59
- color: mapColor(properties.color)
65
+ color: mapColor(resolveRunColor(properties.color, colorScheme))
60
66
  };
61
67
  }
62
- function buildParagraphs(text, style, fontNames) {
68
+ function buildParagraphs(text, style, fontNames, masterStyles, textType, colorScheme) {
63
69
  const paragraphExtents = toExtents(style.paragraphRuns);
64
70
  const characterExtents = toExtents(style.characterRuns);
65
71
  return require_text_atoms.splitParagraphs(text).map((paragraph) => {
66
72
  const end = paragraph.start + paragraph.text.length;
67
- const paragraphProperties = paragraphExtents.find((extent) => paragraph.start >= extent.start && paragraph.start < extent.end)?.properties;
73
+ const rawParagraphProperties = paragraphExtents.find((extent) => paragraph.start >= extent.start && paragraph.start < extent.end)?.properties;
74
+ const indentLevel = rawParagraphProperties?.indentLevel ?? 0;
75
+ const paragraphProperties = require_document_master.resolveParagraphProperties(rawParagraphProperties, masterStyles, textType, indentLevel);
68
76
  const runs = [];
69
77
  for (const extent of characterExtents) {
70
78
  const from = Math.max(extent.start, paragraph.start);
71
79
  const to = Math.min(extent.end, end);
72
80
  if (from >= to) continue;
73
- runs.push(runFrom(paragraph.text.slice(from - paragraph.start, to - paragraph.start), extent.properties, fontNames));
81
+ const resolvedCharacterProperties = require_document_master.resolveCharacterProperties(extent.properties, masterStyles, textType, indentLevel);
82
+ runs.push(runFrom(paragraph.text.slice(from - paragraph.start, to - paragraph.start), resolvedCharacterProperties, fontNames, colorScheme));
83
+ }
84
+ if (runs.length === 0 && paragraph.text.length > 0) {
85
+ const resolvedCharacterProperties = require_document_master.resolveCharacterProperties(void 0, masterStyles, textType, indentLevel);
86
+ runs.push(runFrom(paragraph.text, resolvedCharacterProperties, fontNames, colorScheme));
74
87
  }
75
- if (runs.length === 0 && paragraph.text.length > 0) runs.push({ text: paragraph.text });
76
- const alignment = mapAlignment(paragraphProperties?.alignment);
77
- const indentLevel = paragraphProperties?.indentLevel ?? 0;
78
88
  return {
79
89
  kind: "paragraph",
80
90
  runs,
81
- alignment,
91
+ alignment: mapAlignment(paragraphProperties.alignment),
82
92
  list: indentLevel > 0 ? { level: indentLevel } : void 0,
83
- spacingBeforePt: paraSpacingToPoints(paragraphProperties?.spaceBefore),
84
- spacingAfterPt: paraSpacingToPoints(paragraphProperties?.spaceAfter),
85
- lineSpacing: paraSpacingToLineSpacing(paragraphProperties?.lineSpacing),
86
- indentLeftPt: marginOrIndentToPoints(paragraphProperties?.leftMargin),
87
- indentFirstLinePt: marginOrIndentToPoints(paragraphProperties?.indent)
93
+ spacingBeforePt: paraSpacingToPoints(paragraphProperties.spaceBefore),
94
+ spacingAfterPt: paraSpacingToPoints(paragraphProperties.spaceAfter),
95
+ lineSpacing: paraSpacingToLineSpacing(paragraphProperties.lineSpacing),
96
+ indentLeftPt: marginOrIndentToPoints(paragraphProperties.leftMargin),
97
+ indentFirstLinePt: marginOrIndentToPoints(paragraphProperties.indent)
88
98
  };
89
99
  });
90
100
  }
@@ -1,6 +1,7 @@
1
- import { StyleTextProps } from "./text/style.cjs";
1
+ import { RgbColor, StyleTextProps } from "./text/style.cjs";
2
+ import { n as MasterStyleTable } from "./master-zlMRXyPU.cjs";
2
3
  import { ContentParagraph } from "document-schema.js";
3
4
  //#region src/content.d.ts
4
- declare function buildParagraphs(text: string, style: StyleTextProps, fontNames: readonly string[]): ContentParagraph[];
5
+ declare function buildParagraphs(text: string, style: StyleTextProps, fontNames: readonly string[], masterStyles: MasterStyleTable, textType: number, colorScheme: readonly RgbColor[]): ContentParagraph[];
5
6
  //#endregion
6
7
  export { buildParagraphs };
package/dist/content.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { StyleTextProps } from "./text/style.js";
1
+ import { RgbColor, StyleTextProps } from "./text/style.js";
2
+ import { n as MasterStyleTable } from "./master-Cs_sDM1-.js";
2
3
  import { ContentParagraph } from "document-schema.js";
3
4
  //#region src/content.d.ts
4
- declare function buildParagraphs(text: string, style: StyleTextProps, fontNames: readonly string[]): ContentParagraph[];
5
+ declare function buildParagraphs(text: string, style: StyleTextProps, fontNames: readonly string[], masterStyles: MasterStyleTable, textType: number, colorScheme: readonly RgbColor[]): ContentParagraph[];
5
6
  //#endregion
6
7
  export { buildParagraphs };
package/dist/content.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import "./text/style.js";
2
2
  import { splitParagraphs } from "./text/atoms.js";
3
3
  import { masterUnitsToPoints } from "./units.js";
4
+ import { resolveSchemeColor } from "./document/color-scheme.js";
5
+ import { resolveCharacterProperties, resolveParagraphProperties } from "./document/master.js";
4
6
  //#region src/content.ts
5
7
  const BYTE_MAX = 255;
6
8
  function mapAlignment(alignment) {
@@ -34,6 +36,10 @@ function mapColor(color) {
34
36
  b: color.blue / BYTE_MAX
35
37
  };
36
38
  }
39
+ function resolveRunColor(color, colorScheme) {
40
+ if (color === void 0) return;
41
+ return color.kind === "rgb" ? color.rgb : resolveSchemeColor(color.schemeIndex, colorScheme);
42
+ }
37
43
  function toExtents(runs) {
38
44
  const extents = [];
39
45
  let at = 0;
@@ -47,7 +53,7 @@ function toExtents(runs) {
47
53
  }
48
54
  return extents;
49
55
  }
50
- function runFrom(text, properties, fontNames) {
56
+ function runFrom(text, properties, fontNames, colorScheme) {
51
57
  return {
52
58
  text,
53
59
  bold: properties.bold,
@@ -55,35 +61,39 @@ function runFrom(text, properties, fontNames) {
55
61
  underline: properties.underline,
56
62
  fontFamily: properties.fontRef === void 0 ? void 0 : fontNames[properties.fontRef],
57
63
  sizePt: properties.sizePt,
58
- color: mapColor(properties.color)
64
+ color: mapColor(resolveRunColor(properties.color, colorScheme))
59
65
  };
60
66
  }
61
- function buildParagraphs(text, style, fontNames) {
67
+ function buildParagraphs(text, style, fontNames, masterStyles, textType, colorScheme) {
62
68
  const paragraphExtents = toExtents(style.paragraphRuns);
63
69
  const characterExtents = toExtents(style.characterRuns);
64
70
  return splitParagraphs(text).map((paragraph) => {
65
71
  const end = paragraph.start + paragraph.text.length;
66
- const paragraphProperties = paragraphExtents.find((extent) => paragraph.start >= extent.start && paragraph.start < extent.end)?.properties;
72
+ const rawParagraphProperties = paragraphExtents.find((extent) => paragraph.start >= extent.start && paragraph.start < extent.end)?.properties;
73
+ const indentLevel = rawParagraphProperties?.indentLevel ?? 0;
74
+ const paragraphProperties = resolveParagraphProperties(rawParagraphProperties, masterStyles, textType, indentLevel);
67
75
  const runs = [];
68
76
  for (const extent of characterExtents) {
69
77
  const from = Math.max(extent.start, paragraph.start);
70
78
  const to = Math.min(extent.end, end);
71
79
  if (from >= to) continue;
72
- runs.push(runFrom(paragraph.text.slice(from - paragraph.start, to - paragraph.start), extent.properties, fontNames));
80
+ const resolvedCharacterProperties = resolveCharacterProperties(extent.properties, masterStyles, textType, indentLevel);
81
+ runs.push(runFrom(paragraph.text.slice(from - paragraph.start, to - paragraph.start), resolvedCharacterProperties, fontNames, colorScheme));
82
+ }
83
+ if (runs.length === 0 && paragraph.text.length > 0) {
84
+ const resolvedCharacterProperties = resolveCharacterProperties(void 0, masterStyles, textType, indentLevel);
85
+ runs.push(runFrom(paragraph.text, resolvedCharacterProperties, fontNames, colorScheme));
73
86
  }
74
- if (runs.length === 0 && paragraph.text.length > 0) runs.push({ text: paragraph.text });
75
- const alignment = mapAlignment(paragraphProperties?.alignment);
76
- const indentLevel = paragraphProperties?.indentLevel ?? 0;
77
87
  return {
78
88
  kind: "paragraph",
79
89
  runs,
80
- alignment,
90
+ alignment: mapAlignment(paragraphProperties.alignment),
81
91
  list: indentLevel > 0 ? { level: indentLevel } : void 0,
82
- spacingBeforePt: paraSpacingToPoints(paragraphProperties?.spaceBefore),
83
- spacingAfterPt: paraSpacingToPoints(paragraphProperties?.spaceAfter),
84
- lineSpacing: paraSpacingToLineSpacing(paragraphProperties?.lineSpacing),
85
- indentLeftPt: marginOrIndentToPoints(paragraphProperties?.leftMargin),
86
- indentFirstLinePt: marginOrIndentToPoints(paragraphProperties?.indent)
92
+ spacingBeforePt: paraSpacingToPoints(paragraphProperties.spaceBefore),
93
+ spacingAfterPt: paraSpacingToPoints(paragraphProperties.spaceAfter),
94
+ lineSpacing: paraSpacingToLineSpacing(paragraphProperties.lineSpacing),
95
+ indentLeftPt: marginOrIndentToPoints(paragraphProperties.leftMargin),
96
+ indentFirstLinePt: marginOrIndentToPoints(paragraphProperties.indent)
87
97
  };
88
98
  });
89
99
  }
@@ -0,0 +1,37 @@
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
+ require("../text/style.cjs");
5
+ //#region src/document/color-scheme.ts
6
+ const SLIDE_SCHEME_REC_INSTANCE = 1;
7
+ const COLOR_STRUCT_SIZE = 4;
8
+ function readSlideSchemeColorSchemeAtom(record) {
9
+ if (record.header.recType !== 2032) throw new require_errors.PptFormatError(`expected RT_ColorSchemeAtom (0x${require_record_types.RT_ColorSchemeAtom.toString(16)}) at offset ${record.offset}, found record type 0x${record.header.recType.toString(16)}`);
10
+ if (record.header.recInstance !== SLIDE_SCHEME_REC_INSTANCE) throw new require_errors.PptFormatError(`ColorSchemeAtom at offset ${record.offset} declares recInstance 0x${record.header.recInstance.toString(16)}, not the SlideSchemeColorSchemeAtom's own 0x${SLIDE_SCHEME_REC_INSTANCE.toString(16)}`);
11
+ const expectedLength = 32;
12
+ if (record.data.length !== expectedLength) throw new require_errors.PptFormatError(`SlideSchemeColorSchemeAtom at offset ${record.offset} carries ${record.data.length} bytes, not the mandated ${expectedLength} (8 four-byte scheme slots)`);
13
+ const { data } = record;
14
+ const colors = [];
15
+ for (let slot = 0; slot < 8; slot += 1) {
16
+ const at = slot * COLOR_STRUCT_SIZE;
17
+ const red = data[at];
18
+ const green = data[at + 1];
19
+ const blue = data[at + 2];
20
+ if (red === void 0 || green === void 0 || blue === void 0) throw new require_errors.PptFormatError(`SlideSchemeColorSchemeAtom slot ${slot} at offset ${record.offset + at} is missing bytes`);
21
+ colors.push({
22
+ red,
23
+ green,
24
+ blue
25
+ });
26
+ }
27
+ return colors;
28
+ }
29
+ /** Resolves a scheme-slot index (0-7, see text/style.ts's own ColorIndexStruct table) against a resolved 8-entry colour scheme. Throws rather than returning undefined for an out-of-range index: readColorIndexStruct already rejects any index outside 0x00-0x07/0xFE/0xFF at parse time, so a RunColor of kind "scheme" reaching this function always carries a genuinely valid slot. */
30
+ function resolveSchemeColor(schemeIndex, colorScheme) {
31
+ const color = colorScheme[schemeIndex];
32
+ if (color === void 0) throw new require_errors.PptFormatError(`colour scheme slot ${schemeIndex} has no entry in a ${colorScheme.length}-entry colour scheme`);
33
+ return color;
34
+ }
35
+ //#endregion
36
+ exports.readSlideSchemeColorSchemeAtom = readSlideSchemeColorSchemeAtom;
37
+ exports.resolveSchemeColor = resolveSchemeColor;
@@ -0,0 +1,8 @@
1
+ import { t as PptRecord } from "../tree-Du_LXAF0.cjs";
2
+ import { RgbColor } from "../text/style.cjs";
3
+ //#region src/document/color-scheme.d.ts
4
+ declare function readSlideSchemeColorSchemeAtom(record: PptRecord): readonly RgbColor[];
5
+ /** Resolves a scheme-slot index (0-7, see text/style.ts's own ColorIndexStruct table) against a resolved 8-entry colour scheme. Throws rather than returning undefined for an out-of-range index: readColorIndexStruct already rejects any index outside 0x00-0x07/0xFE/0xFF at parse time, so a RunColor of kind "scheme" reaching this function always carries a genuinely valid slot. */
6
+ declare function resolveSchemeColor(schemeIndex: number, colorScheme: readonly RgbColor[]): RgbColor;
7
+ //#endregion
8
+ export { readSlideSchemeColorSchemeAtom, resolveSchemeColor };
@@ -0,0 +1,8 @@
1
+ import { t as PptRecord } from "../tree-PMcPNgd-.js";
2
+ import { RgbColor } from "../text/style.js";
3
+ //#region src/document/color-scheme.d.ts
4
+ declare function readSlideSchemeColorSchemeAtom(record: PptRecord): readonly RgbColor[];
5
+ /** Resolves a scheme-slot index (0-7, see text/style.ts's own ColorIndexStruct table) against a resolved 8-entry colour scheme. Throws rather than returning undefined for an out-of-range index: readColorIndexStruct already rejects any index outside 0x00-0x07/0xFE/0xFF at parse time, so a RunColor of kind "scheme" reaching this function always carries a genuinely valid slot. */
6
+ declare function resolveSchemeColor(schemeIndex: number, colorScheme: readonly RgbColor[]): RgbColor;
7
+ //#endregion
8
+ export { readSlideSchemeColorSchemeAtom, resolveSchemeColor };
@@ -0,0 +1,35 @@
1
+ import { PptFormatError } from "../errors.js";
2
+ import { RT_ColorSchemeAtom } from "../record/types.js";
3
+ import "../text/style.js";
4
+ //#region src/document/color-scheme.ts
5
+ const SLIDE_SCHEME_REC_INSTANCE = 1;
6
+ const COLOR_STRUCT_SIZE = 4;
7
+ function readSlideSchemeColorSchemeAtom(record) {
8
+ if (record.header.recType !== 2032) throw new PptFormatError(`expected RT_ColorSchemeAtom (0x${RT_ColorSchemeAtom.toString(16)}) at offset ${record.offset}, found record type 0x${record.header.recType.toString(16)}`);
9
+ if (record.header.recInstance !== SLIDE_SCHEME_REC_INSTANCE) throw new PptFormatError(`ColorSchemeAtom at offset ${record.offset} declares recInstance 0x${record.header.recInstance.toString(16)}, not the SlideSchemeColorSchemeAtom's own 0x${SLIDE_SCHEME_REC_INSTANCE.toString(16)}`);
10
+ const expectedLength = 32;
11
+ if (record.data.length !== expectedLength) throw new PptFormatError(`SlideSchemeColorSchemeAtom at offset ${record.offset} carries ${record.data.length} bytes, not the mandated ${expectedLength} (8 four-byte scheme slots)`);
12
+ const { data } = record;
13
+ const colors = [];
14
+ for (let slot = 0; slot < 8; slot += 1) {
15
+ const at = slot * COLOR_STRUCT_SIZE;
16
+ const red = data[at];
17
+ const green = data[at + 1];
18
+ const blue = data[at + 2];
19
+ if (red === void 0 || green === void 0 || blue === void 0) throw new PptFormatError(`SlideSchemeColorSchemeAtom slot ${slot} at offset ${record.offset + at} is missing bytes`);
20
+ colors.push({
21
+ red,
22
+ green,
23
+ blue
24
+ });
25
+ }
26
+ return colors;
27
+ }
28
+ /** Resolves a scheme-slot index (0-7, see text/style.ts's own ColorIndexStruct table) against a resolved 8-entry colour scheme. Throws rather than returning undefined for an out-of-range index: readColorIndexStruct already rejects any index outside 0x00-0x07/0xFE/0xFF at parse time, so a RunColor of kind "scheme" reaching this function always carries a genuinely valid slot. */
29
+ function resolveSchemeColor(schemeIndex, colorScheme) {
30
+ const color = colorScheme[schemeIndex];
31
+ if (color === void 0) throw new PptFormatError(`colour scheme slot ${schemeIndex} has no entry in a ${colorScheme.length}-entry colour scheme`);
32
+ return color;
33
+ }
34
+ //#endregion
35
+ export { readSlideSchemeColorSchemeAtom, resolveSchemeColor };
@@ -0,0 +1,82 @@
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
+ require("../text/atoms.cjs");
5
+ //#region src/document/master.ts
6
+ /** Builds one MainMasterContainer's own MasterStyleTable from its direct-child TextMasterStyleAtoms, falling back to the document-wide default (the single OTHER-typed TextMasterStyleAtom [MS-PPT] states lives in the DocumentTextInfoContainer inside Environment) for any TextTypeEnum member the master itself carries no atom for. A real master this package's own writer produces always states TITLE/BODY/NOTES explicitly (master-write.ts's own comment), so the document default is realistically only ever consulted for OTHER-typed runs or a third-party master that omits one of the three -- but [MS-PPT] 2.9.35 makes it the genuine fallback of last resort for every type, not a special case for OTHER alone. */
7
+ function buildMasterStyleTable(masterAtoms, documentDefault) {
8
+ const byType = /* @__PURE__ */ new Map();
9
+ if (documentDefault !== void 0) byType.set(documentDefault.textType, documentDefault.levels);
10
+ for (const atom of masterAtoms) byType.set(atom.textType, atom.levels);
11
+ return { byType };
12
+ }
13
+ function typeFamilyFallback(textType) {
14
+ switch (textType) {
15
+ case 5:
16
+ case 7:
17
+ case 8: return 1;
18
+ case 6: return 0;
19
+ default: return;
20
+ }
21
+ }
22
+ function orderedMasterLevels(table, textType, indentLevel) {
23
+ const levelsFor = (type) => {
24
+ const levels = table.byType.get(type);
25
+ if (levels === void 0 || levels.length === 0) return [];
26
+ const clampedLevel = Math.min(indentLevel, levels.length - 1);
27
+ return levels.slice(0, clampedLevel + 1).reverse();
28
+ };
29
+ const fallbackType = typeFamilyFallback(textType);
30
+ return fallbackType === void 0 ? levelsFor(textType) : [...levelsFor(textType), ...levelsFor(fallbackType)];
31
+ }
32
+ function firstDefined(candidates, get) {
33
+ for (const candidate of candidates) {
34
+ if (candidate === void 0) continue;
35
+ const value = get(candidate);
36
+ if (value !== void 0) return value;
37
+ }
38
+ }
39
+ /** Resolves a run's own ParagraphProperties (possibly stating nothing at all, when the run's paragraph itself carries no TextPFException of its own) against the master cascade -- every field the run itself states wins outright; every field it doesn't falls through to the first master level that does. indentLevel is never itself resolved from the cascade: it is the run's own stated (or default-zero) outline depth, and is what selects which master levels apply in the first place. */
40
+ function resolveParagraphProperties(run, table, textType, indentLevel) {
41
+ const candidates = [run, ...orderedMasterLevels(table, textType, indentLevel).map((level) => level.paragraph)];
42
+ return {
43
+ indentLevel,
44
+ alignment: firstDefined(candidates, (c) => c.alignment),
45
+ lineSpacing: firstDefined(candidates, (c) => c.lineSpacing),
46
+ spaceBefore: firstDefined(candidates, (c) => c.spaceBefore),
47
+ spaceAfter: firstDefined(candidates, (c) => c.spaceAfter),
48
+ leftMargin: firstDefined(candidates, (c) => c.leftMargin),
49
+ indent: firstDefined(candidates, (c) => c.indent)
50
+ };
51
+ }
52
+ /** The character-property counterpart of resolveParagraphProperties -- see that function's own comment for the cascade this implements. `color` resolves to whichever RunColor (literal or still-unresolved scheme reference) the cascade finds first; converting a scheme reference to an actual RgbColor is document/color-scheme.ts's own concern, deliberately kept separate since it needs the slide's colour scheme rather than anything the text-formatting cascade itself touches. */
53
+ function resolveCharacterProperties(run, table, textType, indentLevel) {
54
+ const candidates = [run, ...orderedMasterLevels(table, textType, indentLevel).map((level) => level.character)];
55
+ return {
56
+ bold: firstDefined(candidates, (c) => c.bold),
57
+ italic: firstDefined(candidates, (c) => c.italic),
58
+ underline: firstDefined(candidates, (c) => c.underline),
59
+ shadow: firstDefined(candidates, (c) => c.shadow),
60
+ emboss: firstDefined(candidates, (c) => c.emboss),
61
+ fontRef: firstDefined(candidates, (c) => c.fontRef),
62
+ sizePt: firstDefined(candidates, (c) => c.sizePt),
63
+ color: firstDefined(candidates, (c) => c.color)
64
+ };
65
+ }
66
+ function readSlideAtom(record) {
67
+ if (record.header.recType !== 1007) throw new require_errors.PptFormatError(`expected RT_SlideAtom (0x${require_record_types.RT_SlideAtom.toString(16)}) at offset ${record.offset}, found record type 0x${record.header.recType.toString(16)}`);
68
+ const MASTER_ID_REF_OFFSET = 12;
69
+ const NOTES_ID_REF_OFFSET = 16;
70
+ if (record.data.length < 20) throw new require_errors.PptFormatError(`SlideAtom at offset ${record.offset} carries ${record.data.length} bytes, too few for its masterIdRef/notesIdRef fields`);
71
+ const { data } = record;
72
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
73
+ return {
74
+ masterIdRef: view.getUint32(MASTER_ID_REF_OFFSET, true),
75
+ notesIdRef: view.getUint32(NOTES_ID_REF_OFFSET, true)
76
+ };
77
+ }
78
+ //#endregion
79
+ exports.buildMasterStyleTable = buildMasterStyleTable;
80
+ exports.readSlideAtom = readSlideAtom;
81
+ exports.resolveCharacterProperties = resolveCharacterProperties;
82
+ exports.resolveParagraphProperties = resolveParagraphProperties;
@@ -0,0 +1,2 @@
1
+ import { a as readSlideAtom, i as buildMasterStyleTable, n as MasterStyleTable, o as resolveCharacterProperties, r as SlideAtomInfo, s as resolveParagraphProperties, t as MasterInfo } from "../master-zlMRXyPU.cjs";
2
+ export { MasterInfo, MasterStyleTable, SlideAtomInfo, buildMasterStyleTable, readSlideAtom, resolveCharacterProperties, resolveParagraphProperties };
@@ -0,0 +1,2 @@
1
+ import { a as readSlideAtom, i as buildMasterStyleTable, n as MasterStyleTable, o as resolveCharacterProperties, r as SlideAtomInfo, s as resolveParagraphProperties, t as MasterInfo } from "../master-Cs_sDM1-.js";
2
+ export { MasterInfo, MasterStyleTable, SlideAtomInfo, buildMasterStyleTable, readSlideAtom, resolveCharacterProperties, resolveParagraphProperties };