reamkit 1.21.0 → 1.23.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 (90) hide show
  1. package/dist/esm/core/bytes.d.ts +22 -4
  2. package/dist/esm/core/bytes.js +73 -5
  3. package/dist/esm/core/converter/ream.d.ts +16 -0
  4. package/dist/esm/core/converter/ream.js +59 -9
  5. package/dist/esm/core/document-model/types.d.ts +78 -0
  6. package/dist/esm/core/drawingml/chart-geometry.js +11 -5
  7. package/dist/esm/core/drawingml/chart-parser.d.ts +2 -1
  8. package/dist/esm/core/drawingml/chart-parser.js +23 -1
  9. package/dist/esm/core/drawingml/colors.d.ts +30 -4
  10. package/dist/esm/core/drawingml/colors.js +45 -6
  11. package/dist/esm/core/drawingml/preset-geometry.js +236 -0
  12. package/dist/esm/core/drawingml/theme-parser.d.ts +38 -0
  13. package/dist/esm/core/drawingml/theme-parser.js +64 -1
  14. package/dist/esm/core/fonts/families.d.ts +10 -0
  15. package/dist/esm/core/fonts/families.js +44 -0
  16. package/dist/esm/core/fonts/index.d.ts +3 -2
  17. package/dist/esm/core/fonts/remote-fonts.d.ts +50 -0
  18. package/dist/esm/core/fonts/remote-fonts.js +114 -8
  19. package/dist/esm/core/fonts/scripts.d.ts +26 -0
  20. package/dist/esm/core/fonts/scripts.js +177 -0
  21. package/dist/esm/core/metafile/blit.d.ts +33 -0
  22. package/dist/esm/core/metafile/blit.js +67 -0
  23. package/dist/esm/core/metafile/dib.d.ts +61 -0
  24. package/dist/esm/core/metafile/dib.js +205 -0
  25. package/dist/esm/core/metafile/emf.js +139 -24
  26. package/dist/esm/core/metafile/picture.d.ts +12 -1
  27. package/dist/esm/core/metafile/symbol-fonts.d.ts +42 -0
  28. package/dist/esm/core/metafile/symbol-fonts.js +154 -0
  29. package/dist/esm/core/metafile/wmf.js +183 -10
  30. package/dist/esm/core/ole/escher-blip.d.ts +12 -0
  31. package/dist/esm/core/ole/escher-blip.js +88 -0
  32. package/dist/esm/{pdf-reader → core}/png-encode.js +1 -1
  33. package/dist/esm/core/vector.d.ts +5 -0
  34. package/dist/esm/excel/print-model.js +3 -1
  35. package/dist/esm/excel/sheet-shape-parser.js +1 -12
  36. package/dist/esm/excel/styles-parser.js +1 -1
  37. package/dist/esm/excel/xls/biff-reader.js +53 -18
  38. package/dist/esm/excel/xls/biff-styles.d.ts +5 -2
  39. package/dist/esm/excel/xls/biff-styles.js +9 -7
  40. package/dist/esm/excel/xls/escher.js +90 -24
  41. package/dist/esm/excel/xlsx-reader.d.ts +1 -1
  42. package/dist/esm/excel/xlsx-reader.js +3 -3
  43. package/dist/esm/layout/page-doc.d.ts +77 -0
  44. package/dist/esm/layout/page-doc.js +34 -18
  45. package/dist/esm/layout/styled-layout.d.ts +2 -2
  46. package/dist/esm/layout/styled-layout.js +467 -76
  47. package/dist/esm/pdf/shading.d.ts +19 -0
  48. package/dist/esm/pdf/shading.js +84 -11
  49. package/dist/esm/pdf/styled-page-emitter.js +647 -444
  50. package/dist/esm/pdf-reader/image-decode.js +1 -1
  51. package/dist/esm/pptx/embedded-fonts.d.ts +31 -0
  52. package/dist/esm/pptx/embedded-fonts.js +104 -0
  53. package/dist/esm/pptx/ole-preview.d.ts +10 -0
  54. package/dist/esm/pptx/ole-preview.js +52 -0
  55. package/dist/esm/pptx/placeholder-cascade.d.ts +136 -5
  56. package/dist/esm/pptx/placeholder-cascade.js +260 -27
  57. package/dist/esm/pptx/ppt/ppt-reader.js +120 -10
  58. package/dist/esm/pptx/ppt/ppt-text.d.ts +66 -1
  59. package/dist/esm/pptx/ppt/ppt-text.js +576 -137
  60. package/dist/esm/pptx/pptx-reader.d.ts +8 -0
  61. package/dist/esm/pptx/pptx-reader.js +271 -41
  62. package/dist/esm/pptx/preset-table-styles.d.ts +9 -0
  63. package/dist/esm/pptx/preset-table-styles.js +27 -0
  64. package/dist/esm/pptx/slide-parser.d.ts +78 -10
  65. package/dist/esm/pptx/slide-parser.js +396 -95
  66. package/dist/esm/pptx/sp-helpers.d.ts +2 -1
  67. package/dist/esm/pptx/sp-helpers.js +22 -6
  68. package/dist/esm/pptx/table-style.d.ts +64 -0
  69. package/dist/esm/pptx/table-style.js +203 -0
  70. package/dist/esm/svg/svg-writer.js +53 -5
  71. package/dist/esm/word/document-parser.d.ts +7 -1
  72. package/dist/esm/word/document-parser.js +6 -6
  73. package/dist/esm/word/docx-reader.js +8 -5
  74. package/dist/esm/word/docx-to-pdf.d.ts +8 -7
  75. package/dist/esm/word/docx-to-pdf.js +25 -7
  76. package/dist/esm/word/drawing-parser.d.ts +34 -0
  77. package/dist/esm/word/drawing-parser.js +173 -15
  78. package/dist/esm/word/numbering-parser.d.ts +2 -1
  79. package/dist/esm/word/numbering-parser.js +6 -6
  80. package/dist/esm/word/paragraph-properties.d.ts +2 -1
  81. package/dist/esm/word/paragraph-properties.js +2 -2
  82. package/dist/esm/word/run-properties.d.ts +5 -2
  83. package/dist/esm/word/run-properties.js +13 -8
  84. package/dist/esm/word/styles-parser.d.ts +2 -1
  85. package/dist/esm/word/styles-parser.js +12 -12
  86. package/dist/esm/word/table-parser.js +3 -3
  87. package/dist/esm/word/theme-fonts.d.ts +10 -0
  88. package/dist/esm/word/theme-fonts.js +28 -0
  89. package/package.json +1 -1
  90. /package/dist/esm/{pdf-reader → core}/png-encode.d.ts +0 -0
@@ -5,13 +5,31 @@
5
5
  */
6
6
  export declare function toBase64(bytes: Uint8Array): string;
7
7
  /**
8
- * Naive scan for an ASCII `needle` inside raw `haystack` bytes — used by reader
9
- * sniffs to spot OPC part names (e.g. `'word/document.xml'`) without unzipping.
8
+ * Whether the ZIP in `bytes` holds a part named `partName` — the name exactly,
9
+ * or anything under it when `partName` names a directory (ends with `/`).
10
+ *
11
+ * This is what a reader sniff should ask. A substring probe cannot tell a
12
+ * package from one EMBEDDED in it: a deck with a chart carries the chart's
13
+ * workbook as `ppt/embeddings/*.xlsx` STORED, uncompressed, so that workbook's
14
+ * own `xl/workbook.xml` sits in the outer file's bytes verbatim — and the xlsx
15
+ * sniff, which runs before the pptx one, claimed thirteen corpus presentations
16
+ * and then failed on them. The ZIP's central directory lists what is actually
17
+ * in this package, and walking that entry table is cheaper than scanning every
18
+ * byte for a needle.
19
+ *
20
+ * Names are compared without case, as OPC compares part names (§9.1.1.1) and
21
+ * as the package reader behind the sniff resolves them.
22
+ *
23
+ * @param bytes The package bytes.
24
+ * @param partName The part name, or a `/`-terminated directory prefix.
25
+ * @returns Whether the package's directory names it.
10
26
  */
27
+ export declare function packageHasPart(bytes: Uint8Array, partName: string): boolean;
11
28
  /**
12
29
  * Scan raw package bytes for an OPC part name (e.g. `'xl/workbook.xml'`)
13
- * without unzipping — the reader sniffs' cheap format probe. Accepts both the
14
- * spec's `/` separator and the `\` that Windows producers write.
30
+ * without unzipping. Prefer {@link packageHasPart}, which asks the archive
31
+ * what it holds instead of hoping the name appears nowhere else. Accepts both
32
+ * the spec's `/` separator and the `\` that Windows producers write.
15
33
  */
16
34
  export declare function bytesIncludePartName(haystack: Uint8Array, partName: string): boolean;
17
35
  /**
@@ -10,13 +10,81 @@ function toBase64(bytes) {
10
10
  return btoa(bin);
11
11
  }
12
12
  /**
13
- * Naive scan for an ASCII `needle` inside raw `haystack` bytes — used by reader
14
- * sniffs to spot OPC part names (e.g. `'word/document.xml'`) without unzipping.
13
+ * Whether the ZIP in `bytes` holds a part named `partName` — the name exactly,
14
+ * or anything under it when `partName` names a directory (ends with `/`).
15
+ *
16
+ * This is what a reader sniff should ask. A substring probe cannot tell a
17
+ * package from one EMBEDDED in it: a deck with a chart carries the chart's
18
+ * workbook as `ppt/embeddings/*.xlsx` STORED, uncompressed, so that workbook's
19
+ * own `xl/workbook.xml` sits in the outer file's bytes verbatim — and the xlsx
20
+ * sniff, which runs before the pptx one, claimed thirteen corpus presentations
21
+ * and then failed on them. The ZIP's central directory lists what is actually
22
+ * in this package, and walking that entry table is cheaper than scanning every
23
+ * byte for a needle.
24
+ *
25
+ * Names are compared without case, as OPC compares part names (§9.1.1.1) and
26
+ * as the package reader behind the sniff resolves them.
27
+ *
28
+ * @param bytes The package bytes.
29
+ * @param partName The part name, or a `/`-terminated directory prefix.
30
+ * @returns Whether the package's directory names it.
15
31
  */
32
+ function packageHasPart(bytes, partName) {
33
+ const names = zipEntryNames(bytes);
34
+ if (names === void 0) return bytesIncludePartName(bytes, partName);
35
+ const wanted = partName.toLowerCase();
36
+ const isDir = wanted.endsWith("/");
37
+ return names.some((raw) => {
38
+ const name = raw.replace(/\\/g, "/").replace(/^\//u, "").toLowerCase();
39
+ return isDir ? name.startsWith(wanted) : name === wanted;
40
+ });
41
+ }
42
+ /**
43
+ * The names in a ZIP's central directory (APPNOTE §4.3.12), or `undefined`
44
+ * when it cannot be located or is malformed — including the ZIP64 spelling of
45
+ * an archive with more than 65 534 entries.
46
+ */
47
+ function zipEntryNames(bytes) {
48
+ if (bytes.length < 22) return void 0;
49
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
50
+ const floor = Math.max(0, bytes.length - 22 - 65535);
51
+ let end = -1;
52
+ for (let i = bytes.length - 22; i >= floor; i--) if (view.getUint32(i, true) === 101010256) {
53
+ end = i;
54
+ break;
55
+ }
56
+ if (end < 0) return void 0;
57
+ let count = view.getUint16(end + 10, true);
58
+ let start = view.getUint32(end + 16, true);
59
+ if (count === 65535 || start === 4294967295) {
60
+ const locator = end - 20;
61
+ if (locator < 0 || view.getUint32(locator, true) !== 117853008) return void 0;
62
+ const z64 = Number(view.getBigUint64(locator + 8, true));
63
+ if (z64 + 56 > bytes.length || view.getUint32(z64, true) !== 101075792) return void 0;
64
+ count = Number(view.getBigUint64(z64 + 32, true));
65
+ start = Number(view.getBigUint64(z64 + 48, true));
66
+ }
67
+ const names = [];
68
+ let p = start;
69
+ for (let i = 0; i < count; i++) {
70
+ if (p + 46 > bytes.length || view.getUint32(p, true) !== 33639248) return void 0;
71
+ const nameLen = view.getUint16(p + 28, true);
72
+ if (p + 46 + nameLen > bytes.length) return void 0;
73
+ names.push(decodeUtf8(bytes.subarray(p + 46, p + 46 + nameLen)));
74
+ p += 46 + nameLen + view.getUint16(p + 30, true) + view.getUint16(p + 32, true);
75
+ }
76
+ return names;
77
+ }
78
+ var utf8 = new TextDecoder();
79
+ /** An entry name as text. Names are UTF-8 or CP437; both decode ASCII alike. */
80
+ function decodeUtf8(bytes) {
81
+ return utf8.decode(bytes);
82
+ }
16
83
  /**
17
84
  * Scan raw package bytes for an OPC part name (e.g. `'xl/workbook.xml'`)
18
- * without unzipping — the reader sniffs' cheap format probe. Accepts both the
19
- * spec's `/` separator and the `\` that Windows producers write.
85
+ * without unzipping. Prefer {@link packageHasPart}, which asks the archive
86
+ * what it holds instead of hoping the name appears nowhere else. Accepts both
87
+ * the spec's `/` separator and the `\` that Windows producers write.
20
88
  */
21
89
  function bytesIncludePartName(haystack, partName) {
22
90
  if (bytesInclude(haystack, partName)) return true;
@@ -36,4 +104,4 @@ function bytesInclude(haystack, needle) {
36
104
  return false;
37
105
  }
38
106
  //#endregion
39
- export { bytesInclude, bytesIncludePartName, toBase64 };
107
+ export { bytesInclude, packageHasPart, toBase64 };
@@ -143,6 +143,22 @@ export declare class Ream {
143
143
  * @returns The font bytes and, for docx, optional per-family registries.
144
144
  */
145
145
  private resolveFonts;
146
+ /**
147
+ * Fetch one face per writing system the document holds text in — the curated
148
+ * families are Latin, and Han, Kana, Hangul, Arabic and the geometric symbols
149
+ * are a notdef box in every one of them.
150
+ *
151
+ * Only the regular weight is fetched: Noto Sans SC is ten megabytes, and a
152
+ * bold run in it is better stroked (see `SyntheticFace`) than downloaded four
153
+ * times over. A face that fails to arrive is a recorded loss, not a throw —
154
+ * the rest of the document still renders.
155
+ *
156
+ * @param into The registry map the run resolver looks in.
157
+ * @param flow The parsed document.
158
+ * @param options The convert options (for the injectable `fetch`).
159
+ * @param losses Where a face that could not be fetched records itself.
160
+ */
161
+ private addScriptFonts;
146
162
  /**
147
163
  * In strict mode, throw {@link ConversionLossError} for the first recorded loss.
148
164
  *
@@ -1,4 +1,5 @@
1
- import { fetchFontSet } from "../fonts/remote-fonts.js";
1
+ import { fetchFontSet, fetchScriptFont, resolveFamilyKey } from "../fonts/remote-fonts.js";
2
+ import { scriptsInFlow } from "../fonts/scripts.js";
2
3
  import { FontRegistry } from "../font/font-registry.js";
3
4
  import { createFontMeasure } from "../font/measure.js";
4
5
  import { ConversionLossError } from "../ir/loss.js";
@@ -12,8 +13,8 @@ import { writeDocx } from "../../word/docx-writer.js";
12
13
  import { writeXlsx } from "../../excel/xlsx-writer.js";
13
14
  import { writeHtml } from "../../html/html-writer.js";
14
15
  import { writeSvg } from "../../svg/svg-writer.js";
15
- import { resolveDocxAutoFonts } from "../../word/docx-to-pdf.js";
16
16
  import { DEFAULT_READERS, resolveFontsViaChain, toFlowDoc } from "./facade.js";
17
+ import { familiesInFlow } from "../fonts/families.js";
17
18
  import { decryptPackage, isEncryptedPackage } from "../crypto/offcrypto.js";
18
19
  //#region src/core/converter/ream.ts
19
20
  /**
@@ -150,7 +151,7 @@ var Ream = class Ream {
150
151
  losses
151
152
  };
152
153
  }
153
- const { fonts, registriesByFamily } = await this.resolveFonts(options, losses);
154
+ const { fonts, registriesByFamily } = await this.resolveFonts(options, losses, flow);
154
155
  const registry = FontRegistry.fromBytes(fonts);
155
156
  const paginated = this.sheet ? projectSheetDoc(this.sheet, {
156
157
  ...options.now ? { now: options.now } : {},
@@ -209,7 +210,7 @@ var Ream = class Ream {
209
210
  * @param losses The mutable loss list a substitution is appended to.
210
211
  * @returns The font bytes and, for docx, optional per-family registries.
211
212
  */
212
- async resolveFonts(options, losses) {
213
+ async resolveFonts(options, losses, flow) {
213
214
  const explicit = options.fonts ?? (options.fontBytes ? { regular: options.fontBytes } : void 0);
214
215
  if (explicit) return { fonts: explicit };
215
216
  if (options.fontProviders && options.fontProviders.length > 0) {
@@ -217,11 +218,60 @@ var Ream = class Ream {
217
218
  if (loss) losses.push(loss);
218
219
  if (fonts) return { fonts };
219
220
  }
220
- if (this.readerId === "docx") return resolveDocxAutoFonts(this.source, options);
221
- return { fonts: await fetchFontSet({
222
- ...options.fontFamily ? { family: options.fontFamily } : {},
223
- ...options.fontFetch ? { fetch: options.fontFetch } : {}
224
- }) };
221
+ const fetchOpt = options.fontFetch ? { fetch: options.fontFetch } : {};
222
+ const keys = options.fontFamily ? new Set([resolveFamilyKey(options.fontFamily)]) : familiesInFlow(flow);
223
+ if (keys.size <= 1 && scriptsInFlow(flow).scripts.size === 0) {
224
+ const family = keys.values().next().value;
225
+ return { fonts: await fetchFontSet({
226
+ ...family ? { family } : {},
227
+ ...fetchOpt
228
+ }) };
229
+ }
230
+ const registriesByFamily = /* @__PURE__ */ new Map();
231
+ let baseBytes;
232
+ for (const key of keys) {
233
+ const bytes = await fetchFontSet({
234
+ family: key,
235
+ ...fetchOpt
236
+ });
237
+ registriesByFamily.set(key, FontRegistry.fromBytes(bytes));
238
+ if (key === "arimo" || !baseBytes) baseBytes = bytes;
239
+ }
240
+ await this.addScriptFonts(registriesByFamily, flow, options, losses);
241
+ return {
242
+ fonts: baseBytes,
243
+ registriesByFamily
244
+ };
245
+ }
246
+ /**
247
+ * Fetch one face per writing system the document holds text in — the curated
248
+ * families are Latin, and Han, Kana, Hangul, Arabic and the geometric symbols
249
+ * are a notdef box in every one of them.
250
+ *
251
+ * Only the regular weight is fetched: Noto Sans SC is ten megabytes, and a
252
+ * bold run in it is better stroked (see `SyntheticFace`) than downloaded four
253
+ * times over. A face that fails to arrive is a recorded loss, not a throw —
254
+ * the rest of the document still renders.
255
+ *
256
+ * @param into The registry map the run resolver looks in.
257
+ * @param flow The parsed document.
258
+ * @param options The convert options (for the injectable `fetch`).
259
+ * @param losses Where a face that could not be fetched records itself.
260
+ */
261
+ async addScriptFonts(into, flow, options, losses) {
262
+ const { scripts } = scriptsInFlow(flow);
263
+ for (const script of scripts) {
264
+ const bytes = await fetchScriptFont(script, options.fontFetch);
265
+ if (!bytes) {
266
+ losses.push({
267
+ feature: "font",
268
+ detail: `no face could be fetched for the ${script} script; its text draws as boxes`,
269
+ severity: "substituted"
270
+ });
271
+ continue;
272
+ }
273
+ into.set(script, FontRegistry.fromBytes(bytes));
274
+ }
225
275
  }
226
276
  /**
227
277
  * In strict mode, throw {@link ConversionLossError} for the first recorded loss.
@@ -257,6 +257,17 @@ export interface InlineImage {
257
257
  readonly gain: number;
258
258
  readonly black: number;
259
259
  };
260
+ /**
261
+ * §20.1.8.16 `a:clrChange` — one colour of the picture replaced by another,
262
+ * or knocked out entirely when the destination states `a:alpha` at zero. A
263
+ * logo on a white card goes onto a dark slide that way (corpus: tdf113163,
264
+ * whose whole slide is a metafile with its white ground declared away).
265
+ */
266
+ readonly colorChange?: {
267
+ readonly fromHex: string;
268
+ readonly toHex: string;
269
+ readonly transparent: boolean;
270
+ };
260
271
  readonly width: Pt;
261
272
  readonly height: Pt;
262
273
  /** §20.1.8.55 `a:srcRect` — the part of the source the frame shows. */
@@ -915,6 +926,22 @@ export interface ImageBlock {
915
926
  readonly gain: number;
916
927
  readonly black: number;
917
928
  };
929
+ /**
930
+ * §20.1.8.4 `a:alphaModFix` — how opaque the picture is DRAWN, `0..1`; absent
931
+ * is opaque. A layout that lays a photograph behind its text sets it low
932
+ * (ArtisticEffectSample's cover shows one at 52%), and drawn full-strength
933
+ * the words on it are unreadable.
934
+ */
935
+ readonly alpha?: number;
936
+ /**
937
+ * §20.1.8.16 `a:clrChange` — one colour of the picture replaced by another,
938
+ * or knocked out entirely when the destination states `a:alpha` at zero.
939
+ */
940
+ readonly colorChange?: {
941
+ readonly fromHex: string;
942
+ readonly toHex: string;
943
+ readonly transparent: boolean;
944
+ };
918
945
  readonly width: Pt;
919
946
  readonly height: Pt;
920
947
  /** §20.1.8.55 `a:srcRect` — the part of the source the frame shows. */
@@ -998,6 +1025,24 @@ export interface ShapeFill {
998
1025
  * text box: stretched, such a fill is a blur where it should be a pattern.
999
1026
  */
1000
1027
  readonly tiled?: boolean;
1028
+ /**
1029
+ * The size ONE copy of a tiled fill occupies, when the shape states it —
1030
+ * MS-ODRAW §2.3.7.11/.12 `fillWidth` / `fillHeight`. Absent, a tile is the
1031
+ * picture at its own resolution, which is the default both formats give.
1032
+ */
1033
+ readonly tileSizePt?: {
1034
+ readonly widthPt: number;
1035
+ readonly heightPt: number;
1036
+ };
1037
+ /**
1038
+ * §20.1.8.58 `a:tile @sx @sy` — how far the picture is scaled BEFORE it is
1039
+ * repeated, as fractions of its own size (1 = 100 %). A texture halved tiles
1040
+ * four times as often, and read as a plain repeat it tiles once.
1041
+ */
1042
+ readonly tileScale?: {
1043
+ readonly sx: number;
1044
+ readonly sy: number;
1045
+ };
1001
1046
  /**
1002
1047
  * §20.1.8.14 `a:blipFill` — the picture painted across the shape's box. A
1003
1048
  * DrawingML picture IS a shape with one of these, which is how a `pic:pic`
@@ -1009,6 +1054,30 @@ export interface ShapeFill {
1009
1054
  * of the picture the box shows, as the fractions cut from each side.
1010
1055
  */
1011
1056
  readonly imageCrop?: ImageCrop;
1057
+ /**
1058
+ * §20.1.8.30 `a:stretch/a:fillRect` with POSITIVE insets — the part of the
1059
+ * shape's box the picture is stretched into, as fractions of that box. A
1060
+ * slide backed by a picture inset 55 % from the left shows it in the corner,
1061
+ * not across the slide (corpus: tdf153466.pptx). Negative insets are the
1062
+ * other case entirely — the picture zooming past the box — and read as a
1063
+ * crop of the source.
1064
+ */
1065
+ /**
1066
+ * §20.1.8.23 `a:duotone` — the picture recoloured into two tones: its dark
1067
+ * end becomes the first colour, its light end the second. An Office theme
1068
+ * that ships a photograph tints it this way, so a deck whose background is a
1069
+ * brown ridged texture is stored as a grey one (corpus: themes.pptx).
1070
+ */
1071
+ readonly duotone?: {
1072
+ readonly shadowHex: string;
1073
+ readonly highlightHex: string;
1074
+ };
1075
+ readonly imageFillRect?: {
1076
+ readonly left: number;
1077
+ readonly top: number;
1078
+ readonly right: number;
1079
+ readonly bottom: number;
1080
+ };
1012
1081
  /**
1013
1082
  * §20.1.2.3.1 `a:alpha` / §14.1.2.5 `@opacity` — how opaque the fill is,
1014
1083
  * `0..1`. Absent is opaque. The colour above is the fill's own, NOT composited
@@ -1315,6 +1384,15 @@ export interface Chart {
1315
1384
  * creates, and both references draw them.
1316
1385
  */
1317
1386
  readonly frameFillHex?: string;
1387
+ /**
1388
+ * §20.1.8.14 `a:blipFill` — the PICTURE the frame is filled with, when it is
1389
+ * filled with one rather than a colour. chart-texture-bg.pptx papers its
1390
+ * whole chart with a woven cloth, and a chart on white is a different chart.
1391
+ */
1392
+ readonly frameFillImage?: {
1393
+ readonly resource: ResourceId;
1394
+ readonly tiled?: boolean;
1395
+ };
1318
1396
  readonly frameLineHex?: string;
1319
1397
  /** The frame rule's width and dash, when it states them. */
1320
1398
  readonly frameLineWidthPt?: number;
@@ -46,7 +46,7 @@ function niceScale(dataMin, dataMax, maxTicks = 6) {
46
46
  const lo = Math.min(dataMin, dataMax);
47
47
  let hi = Math.max(dataMin, dataMax);
48
48
  if (lo === hi) hi = lo + 1;
49
- const step = niceNum(niceNum(hi - lo, false) / Math.max(1, maxTicks - 1), true);
49
+ const step = niceNum((hi - lo) / Math.max(1, maxTicks - 1), true);
50
50
  const headroom = hi > 0 ? hi * 1.05 : hi;
51
51
  return {
52
52
  min: Math.floor(lo / step) * step,
@@ -712,11 +712,17 @@ function autoRange(vals) {
712
712
  return [lo > 0 && lo < 5 / 6 * hi ? 0 : lo, hi];
713
713
  }
714
714
  /**
715
- * Ticks that fit along an axis `extentPt` long (mirrors {@link axisScale}).
716
- * A tick every 32pt is what both references draw: at 24 a 250pt plot carried
717
- * eleven of them (0, 0.5, 1 … 5.5) where both label 0…6 in whole numbers.
715
+ * How many ticks an axis `extentPt` long carries (mirrors {@link axisScale}).
716
+ *
717
+ * A tall axis takes more of them — a fixed six drew 0/100/200/300 down a plot
718
+ * where both references fit 0/50/…/300 — but the appetite is milder than it
719
+ * looks: asked for one every 32pt, a 427pt chart wanted ten, and Excel labels
720
+ * that chart's 0…5 data 0/1/…/6 where we drew half-steps (chart-texture-bg
721
+ * .pptx). One per ~48pt and never more than eight satisfies both, now that
722
+ * {@link niceScale} measures its step against the range the data spans rather
723
+ * than against Heckbert's rounded-up one.
718
724
  */
719
- var tickBudget = (extentPt) => Math.min(10, Math.max(4, Math.round(extentPt / 32)));
725
+ var tickBudget = (extentPt) => Math.min(8, Math.max(4, Math.round(extentPt / 48)));
720
726
  /** Side (points) of the square stamped for a series that names no symbol. */
721
727
  var DEFAULT_MARKER_PT = 4;
722
728
  /**
@@ -1,4 +1,5 @@
1
1
  import { Chart } from '../document-model/index.js';
2
+ import { ResourceId } from '../ir/index.js';
2
3
  import { OpcPackage } from '../opc/index.js';
3
4
  import { ColorResolver } from './colors.js';
4
5
  /**
@@ -14,7 +15,7 @@ import { ColorResolver } from './colors.js';
14
15
  * @param resolveColor Maps a DrawingML colour reference to a 6-hex string.
15
16
  * @returns The parsed chart, or `null` when there is no `c:chart` / `c:plotArea`.
16
17
  */
17
- export declare function parseChart(chartXml: Uint8Array, resolveColor: ColorResolver): Chart | null;
18
+ export declare function parseChart(chartXml: Uint8Array, resolveColor: ColorResolver, resolveImage?: (relId: string) => ResourceId | undefined): Chart | null;
18
19
  /**
19
20
  * MS-ODRAWXML chartColorStyle (charts/colorsN.xml): the top-level colour list is
20
21
  * the series cycle (`meth="cycle"` — the common case; variations are luminance
@@ -37,7 +37,7 @@ var TYPE_OF_TAG = {
37
37
  * @param resolveColor Maps a DrawingML colour reference to a 6-hex string.
38
38
  * @returns The parsed chart, or `null` when there is no `c:chart` / `c:plotArea`.
39
39
  */
40
- function parseChart(chartXml, resolveColor) {
40
+ function parseChart(chartXml, resolveColor, resolveImage) {
41
41
  const tree = parser.parse(decoder.decode(chartXml));
42
42
  const chart = poFindByPath(tree, ["c:chartSpace", "c:chart"]);
43
43
  if (!chart) return null;
@@ -105,6 +105,7 @@ function parseChart(chartXml, resolveColor) {
105
105
  const frameLine = spaceSpPr ? poChildren(spaceSpPr).find((c) => poIs(c, "a:ln")) : void 0;
106
106
  const frameStyle = lineStyleOf(chartSpace, resolveColor);
107
107
  const frameFillHex = spaceSpPr ? frameFillOf(spaceSpPr, resolveColor) : "FFFFFF";
108
+ const frameFillImage = spaceSpPr ? blipFillOf(spaceSpPr, resolveImage) : void 0;
108
109
  const plotSpPr = poChildren(plotArea).find((c) => poIs(c, "c:spPr"));
109
110
  const plotFillHex = plotSpPr ? frameFillOf(plotSpPr, resolveColor) : void 0;
110
111
  const plotLine = lineStyleOf(plotArea, resolveColor);
@@ -142,6 +143,7 @@ function parseChart(chartXml, resolveColor) {
142
143
  ...valAxisMin !== void 0 ? { valAxisMin } : {},
143
144
  ...valAxisMax !== void 0 ? { valAxisMax } : {},
144
145
  ...frameFillHex ? { frameFillHex } : {},
146
+ ...frameFillImage ? { frameFillImage } : {},
145
147
  ...frameLineHex ? { frameLineHex } : {},
146
148
  ...frameStyle?.widthPt !== void 0 ? { frameLineWidthPt: frameStyle.widthPt } : {},
147
149
  ...frameStyle?.dash ? { frameLineDash: frameStyle.dash } : {},
@@ -248,6 +250,26 @@ function dataPointColors(ser, resolveColor) {
248
250
  return out;
249
251
  }
250
252
  /**
253
+ * §20.1.8.14 `a:blipFill` — the picture a chart element is filled with, and
254
+ * whether it TILES at its own size rather than stretching over the box.
255
+ *
256
+ * @param spPr The element's `c:spPr`.
257
+ * @param resolveImage The chart part's own blip resolver, when the caller has one.
258
+ * @returns The stored picture, or `undefined` when the fill is not one.
259
+ */
260
+ function blipFillOf(spPr, resolveImage) {
261
+ const blipFill = poChildren(spPr).find((c) => poIs(c, "a:blipFill"));
262
+ const blip = blipFill ? poChildren(blipFill).find((c) => poIs(c, "a:blip")) : void 0;
263
+ const relId = blip ? poAttr(blip, "embed") : void 0;
264
+ const resource = relId !== void 0 ? resolveImage?.(relId) : void 0;
265
+ if (resource === void 0 || !blipFill) return void 0;
266
+ const tiled = poChildren(blipFill).some((c) => poIs(c, "a:tile"));
267
+ return {
268
+ resource,
269
+ ...tiled ? { tiled } : {}
270
+ };
271
+ }
272
+ /**
251
273
  * The BACKGROUND fill of a frame (the chart space, the plot rectangle): its own
252
274
  * `a:solidFill` and nothing else. Unlike a series, a frame that states
253
275
  * `<a:noFill/>` is not filled in the colour of its own rule —
@@ -3,10 +3,12 @@ import { PoNode } from '../po-helpers.js';
3
3
  * A colour transform child (§20.1.2.3): `lumMod`/`lumOff` modulate luminance,
4
4
  * `shade` darkens toward black, `tint` lightens toward white. `val` is normalised
5
5
  * to 0..1 (the XML stores thousandths of a percent). `alpha` composites over white
6
- * (solid fills emit no transparency).
6
+ * (solid fills emit no transparency). `gamma`/`invGamma` carry no value: they
7
+ * shift the colour into the other light and back, which changes what the
8
+ * transforms between them mean.
7
9
  */
8
10
  export interface ColorMod {
9
- readonly kind: 'lumMod' | 'lumOff' | 'shade' | 'tint' | 'alpha' | 'satMod' | 'hueOff' | 'satOff';
11
+ readonly kind: 'lumMod' | 'lumOff' | 'shade' | 'tint' | 'alpha' | 'satMod' | 'hueMod' | 'hueOff' | 'satOff' | 'gamma' | 'invGamma';
10
12
  readonly val: number;
11
13
  }
12
14
  /**
@@ -38,20 +40,44 @@ export declare function applyColorMods(hex: string, mods: ReadonlyArray<ColorMod
38
40
  * scheme slots when a document carries no custom theme part.
39
41
  */
40
42
  export declare const DEFAULT_THEME_PALETTE: ReadonlyMap<string, string>;
43
+ /**
44
+ * Which theme slot each `schemeClr` name stands for. Only the text/background
45
+ * aliases are ever remapped; anything absent resolves under its own name.
46
+ */
47
+ export type SchemeAliases = Readonly<Record<string, string>>;
48
+ export declare const DEFAULT_SCHEME_ALIAS: SchemeAliases;
41
49
  /**
42
50
  * Resolve a `schemeClr` text/background alias (`tx1`/`bg1`/`tx2`/`bg2`,
43
51
  * §20.1.2.3.29) to its underlying `dk`/`lt` slot name; other names pass through.
52
+ *
53
+ * @param name The `schemeClr` value.
54
+ * @param alias The mapping to read it under — a presentation states its own in
55
+ * the master's `p:clrMap` (§19.3.1.6), where `bg1` may well mean `dk2`.
56
+ * @returns The theme slot to look up.
44
57
  */
45
- export declare function resolveSchemeName(name: string): string;
58
+ export declare function resolveSchemeName(name: string, alias?: SchemeAliases): string;
46
59
  /**
47
60
  * Build a {@link ColorResolver} over a scheme-name → hex `palette`. sRGB
48
61
  * references pass through verbatim (upper-cased); scheme references are aliased
49
62
  * (via {@link resolveSchemeName}) then looked up; colour transforms are applied.
50
63
  *
51
64
  * @param palette The scheme-name → RRGGBB map (e.g. {@link DEFAULT_THEME_PALETTE}).
65
+ * @param alias How the bg/tx aliases map onto the theme's slots; the default
66
+ * is the fixed DrawingML one, which is what a document without a colour map of
67
+ * its own (docx, xlsx) uses.
52
68
  * @returns A resolver that maps a {@link RawColor} to a hex string or `undefined`.
53
69
  */
54
- export declare function makeColorResolver(palette: ReadonlyMap<string, string>): ColorResolver;
70
+ export declare function makeColorResolver(palette: ReadonlyMap<string, string>, alias?: SchemeAliases): ColorResolver;
71
+ /**
72
+ * §20.1.4.2.10 `phClr` — inside a theme's style list the colours are written as
73
+ * "the placeholder", and the reference that reaches the style says which colour
74
+ * that is. Everything else resolves as it always did.
75
+ *
76
+ * @param base The document's own resolver.
77
+ * @param phHex The colour the reference carries (`a:fillRef`, `p:bgRef`, …).
78
+ * @returns A resolver that answers `phClr` with that colour, transforms and all.
79
+ */
80
+ export declare function placeholderColors(base: ColorResolver, phHex: string): ColorResolver;
55
81
  /** A {@link ColorResolver} backed by the {@link DEFAULT_THEME_PALETTE}. */
56
82
  export declare const defaultColorResolver: ColorResolver;
57
83
  /**
@@ -99,10 +99,20 @@ function applyColorMods(hex, mods) {
99
99
  r = toSrgb(toLinear(r) * m.val + (1 - m.val));
100
100
  g = toSrgb(toLinear(g) * m.val + (1 - m.val));
101
101
  b = toSrgb(toLinear(b) * m.val + (1 - m.val));
102
+ } else if (m.kind === "gamma" || m.kind === "invGamma") {
103
+ const shift = m.kind === "gamma" ? toSrgb : toLinear;
104
+ [r, g, b] = [
105
+ shift(r),
106
+ shift(g),
107
+ shift(b)
108
+ ];
102
109
  } else if (m.kind === "lumMod" || m.kind === "lumOff") {
103
110
  const [h, s, l] = rgbToHsl(r, g, b);
104
111
  const l2 = m.kind === "lumMod" ? l * m.val : clamp01(l + m.val);
105
112
  [r, g, b] = hslToRgb(h, s, l2);
113
+ } else if (m.kind === "hueMod") {
114
+ const [h, sat, l] = rgbToHsl(r, g, b);
115
+ [r, g, b] = hslToRgb((h * m.val % 360 + 360) % 360, sat, l);
106
116
  } else if (m.kind === "hueOff") {
107
117
  const [h, sat, l] = rgbToHsl(r, g, b);
108
118
  [r, g, b] = hslToRgb(((h + m.val) % 360 + 360) % 360, sat, l);
@@ -138,7 +148,7 @@ var DEFAULT_THEME_PALETTE = new Map([
138
148
  ["hlink", "0563C1"],
139
149
  ["folHlink", "954F72"]
140
150
  ]);
141
- var SCHEME_ALIAS = {
151
+ var DEFAULT_SCHEME_ALIAS = {
142
152
  tx1: "dk1",
143
153
  bg1: "lt1",
144
154
  tx2: "dk2",
@@ -147,9 +157,14 @@ var SCHEME_ALIAS = {
147
157
  /**
148
158
  * Resolve a `schemeClr` text/background alias (`tx1`/`bg1`/`tx2`/`bg2`,
149
159
  * §20.1.2.3.29) to its underlying `dk`/`lt` slot name; other names pass through.
160
+ *
161
+ * @param name The `schemeClr` value.
162
+ * @param alias The mapping to read it under — a presentation states its own in
163
+ * the master's `p:clrMap` (§19.3.1.6), where `bg1` may well mean `dk2`.
164
+ * @returns The theme slot to look up.
150
165
  */
151
- function resolveSchemeName(name) {
152
- return SCHEME_ALIAS[name] ?? name;
166
+ function resolveSchemeName(name, alias = DEFAULT_SCHEME_ALIAS) {
167
+ return alias[name] ?? name;
153
168
  }
154
169
  /**
155
170
  * Build a {@link ColorResolver} over a scheme-name → hex `palette`. sRGB
@@ -157,15 +172,30 @@ function resolveSchemeName(name) {
157
172
  * (via {@link resolveSchemeName}) then looked up; colour transforms are applied.
158
173
  *
159
174
  * @param palette The scheme-name → RRGGBB map (e.g. {@link DEFAULT_THEME_PALETTE}).
175
+ * @param alias How the bg/tx aliases map onto the theme's slots; the default
176
+ * is the fixed DrawingML one, which is what a document without a colour map of
177
+ * its own (docx, xlsx) uses.
160
178
  * @returns A resolver that maps a {@link RawColor} to a hex string or `undefined`.
161
179
  */
162
- function makeColorResolver(palette) {
180
+ function makeColorResolver(palette, alias = DEFAULT_SCHEME_ALIAS) {
163
181
  return (raw) => {
164
- const base = "srgb" in raw ? raw.srgb.toUpperCase() : palette.get(resolveSchemeName(raw.scheme));
182
+ const base = "srgb" in raw ? raw.srgb.toUpperCase() : palette.get(resolveSchemeName(raw.scheme, alias));
165
183
  if (base === void 0) return void 0;
166
184
  return raw.mods && raw.mods.length > 0 ? applyColorMods(base, raw.mods) : base;
167
185
  };
168
186
  }
187
+ /**
188
+ * §20.1.4.2.10 `phClr` — inside a theme's style list the colours are written as
189
+ * "the placeholder", and the reference that reaches the style says which colour
190
+ * that is. Everything else resolves as it always did.
191
+ *
192
+ * @param base The document's own resolver.
193
+ * @param phHex The colour the reference carries (`a:fillRef`, `p:bgRef`, …).
194
+ * @returns A resolver that answers `phClr` with that colour, transforms and all.
195
+ */
196
+ function placeholderColors(base, phHex) {
197
+ return (raw) => "scheme" in raw && raw.scheme === "phClr" ? applyColorMods(phHex, raw.mods ?? []) : base(raw);
198
+ }
169
199
  /** A {@link ColorResolver} backed by the {@link DEFAULT_THEME_PALETTE}. */
170
200
  var defaultColorResolver = makeColorResolver(DEFAULT_THEME_PALETTE);
171
201
  var PRESET_COLORS = new Map([
@@ -198,6 +228,7 @@ function readColorMods(colorNode) {
198
228
  "tint",
199
229
  "alpha",
200
230
  "satMod",
231
+ "hueMod",
201
232
  "satOff"
202
233
  ]) if (poIs(c, `a:${kind}`)) {
203
234
  const v = poIntAttr(c, "val");
@@ -206,6 +237,14 @@ function readColorMods(colorNode) {
206
237
  val: v / 1e5
207
238
  });
208
239
  }
240
+ if (poIs(c, "a:gamma")) mods.push({
241
+ kind: "gamma",
242
+ val: 0
243
+ });
244
+ if (poIs(c, "a:invGamma")) mods.push({
245
+ kind: "invGamma",
246
+ val: 0
247
+ });
209
248
  if (poIs(c, "a:hueOff")) {
210
249
  const v = poIntAttr(c, "val");
211
250
  if (v !== void 0) mods.push({
@@ -266,4 +305,4 @@ function resolveColorNode(c, resolveColor) {
266
305
  } : raw);
267
306
  }
268
307
  //#endregion
269
- export { DEFAULT_THEME_PALETTE, applyColorMods, defaultColorResolver, makeColorResolver, readColorMods, resolveColorNode };
308
+ export { DEFAULT_SCHEME_ALIAS, DEFAULT_THEME_PALETTE, applyColorMods, defaultColorResolver, makeColorResolver, placeholderColors, readColorMods, resolveColorNode };