reamkit 1.22.0 → 1.24.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 (101) hide show
  1. package/dist/esm/core/bmp.d.ts +33 -0
  2. package/dist/esm/core/bmp.js +276 -0
  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 +55 -0
  6. package/dist/esm/core/drawingml/chart-geometry.js +14 -7
  7. package/dist/esm/core/drawingml/colors.d.ts +4 -2
  8. package/dist/esm/core/drawingml/colors.js +15 -0
  9. package/dist/esm/core/drawingml/diagram/colors.d.ts +52 -0
  10. package/dist/esm/core/drawingml/diagram/colors.js +125 -0
  11. package/dist/esm/core/drawingml/diagram/data-model.d.ts +47 -0
  12. package/dist/esm/core/drawingml/diagram/data-model.js +119 -0
  13. package/dist/esm/core/drawingml/diagram/layout-engine.d.ts +46 -0
  14. package/dist/esm/core/drawingml/diagram/layout-engine.js +948 -0
  15. package/dist/esm/core/drawingml/diagram/run.d.ts +37 -0
  16. package/dist/esm/core/drawingml/diagram/run.js +50 -0
  17. package/dist/esm/core/drawingml/diagram/to-drawing.d.ts +16 -0
  18. package/dist/esm/core/drawingml/diagram/to-drawing.js +86 -0
  19. package/dist/esm/core/drawingml/preset-geometry.js +116 -6
  20. package/dist/esm/core/drawingml/text-warp.d.ts +58 -0
  21. package/dist/esm/core/drawingml/text-warp.js +355 -0
  22. package/dist/esm/core/drawingml/theme-parser.d.ts +38 -0
  23. package/dist/esm/core/drawingml/theme-parser.js +73 -4
  24. package/dist/esm/core/font/measure.d.ts +12 -0
  25. package/dist/esm/core/font/measure.js +36 -0
  26. package/dist/esm/core/fonts/families.d.ts +10 -0
  27. package/dist/esm/core/fonts/families.js +44 -0
  28. package/dist/esm/core/fonts/index.d.ts +3 -2
  29. package/dist/esm/core/fonts/remote-fonts.d.ts +50 -0
  30. package/dist/esm/core/fonts/remote-fonts.js +114 -8
  31. package/dist/esm/core/fonts/scripts.d.ts +26 -0
  32. package/dist/esm/core/fonts/scripts.js +177 -0
  33. package/dist/esm/core/images.d.ts +30 -2
  34. package/dist/esm/core/images.js +121 -5
  35. package/dist/esm/core/metafile/blit.d.ts +33 -0
  36. package/dist/esm/core/metafile/blit.js +67 -0
  37. package/dist/esm/core/metafile/dib.d.ts +61 -0
  38. package/dist/esm/core/metafile/dib.js +205 -0
  39. package/dist/esm/core/metafile/emf.js +280 -38
  40. package/dist/esm/core/metafile/picture.d.ts +61 -1
  41. package/dist/esm/core/metafile/picture.js +70 -1
  42. package/dist/esm/core/metafile/wmf.js +202 -13
  43. package/dist/esm/core/ole/escher-blip.d.ts +12 -0
  44. package/dist/esm/core/ole/escher-blip.js +98 -0
  45. package/dist/esm/{pdf-reader → core}/png-encode.js +1 -1
  46. package/dist/esm/excel/print-model.js +5 -3
  47. package/dist/esm/excel/sheet-drawing.js +1 -1
  48. package/dist/esm/excel/sheet-to-flow.js +16 -2
  49. package/dist/esm/excel/styles-parser.js +1 -1
  50. package/dist/esm/excel/xls/biff-reader.js +53 -18
  51. package/dist/esm/excel/xls/biff-styles.d.ts +5 -2
  52. package/dist/esm/excel/xls/biff-styles.js +9 -7
  53. package/dist/esm/excel/xls/escher.js +90 -24
  54. package/dist/esm/html/html-writer.js +2 -1
  55. package/dist/esm/layout/page-doc.d.ts +74 -0
  56. package/dist/esm/layout/page-doc.js +14 -1
  57. package/dist/esm/layout/styled-layout.d.ts +2 -2
  58. package/dist/esm/layout/styled-layout.js +475 -48
  59. package/dist/esm/pdf/shading.js +8 -6
  60. package/dist/esm/pdf/styled-page-emitter.js +77 -5
  61. package/dist/esm/pdf/vector-graphics.js +1 -1
  62. package/dist/esm/pdf-reader/image-decode.js +1 -1
  63. package/dist/esm/pptx/embedded-fonts.d.ts +31 -0
  64. package/dist/esm/pptx/embedded-fonts.js +104 -0
  65. package/dist/esm/pptx/placeholder-cascade.d.ts +73 -6
  66. package/dist/esm/pptx/placeholder-cascade.js +114 -18
  67. package/dist/esm/pptx/ppt/ppt-reader.js +191 -13
  68. package/dist/esm/pptx/ppt/ppt-text.d.ts +107 -1
  69. package/dist/esm/pptx/ppt/ppt-text.js +873 -144
  70. package/dist/esm/pptx/pptx-reader.d.ts +8 -0
  71. package/dist/esm/pptx/pptx-reader.js +117 -15
  72. package/dist/esm/pptx/preset-table-styles.d.ts +9 -0
  73. package/dist/esm/pptx/preset-table-styles.js +27 -0
  74. package/dist/esm/pptx/slide-parser.d.ts +11 -2
  75. package/dist/esm/pptx/slide-parser.js +203 -44
  76. package/dist/esm/pptx/sp-helpers.d.ts +9 -4
  77. package/dist/esm/pptx/sp-helpers.js +48 -9
  78. package/dist/esm/pptx/table-style.d.ts +16 -1
  79. package/dist/esm/pptx/table-style.js +104 -10
  80. package/dist/esm/svg/svg-writer.js +1 -0
  81. package/dist/esm/word/document-parser.d.ts +11 -2
  82. package/dist/esm/word/document-parser.js +10 -7
  83. package/dist/esm/word/docx-reader.js +32 -16
  84. package/dist/esm/word/docx-to-pdf.d.ts +8 -7
  85. package/dist/esm/word/docx-to-pdf.js +25 -7
  86. package/dist/esm/word/docx-writer.js +4 -0
  87. package/dist/esm/word/drawing-parser.d.ts +20 -0
  88. package/dist/esm/word/drawing-parser.js +67 -22
  89. package/dist/esm/word/numbering-parser.d.ts +2 -1
  90. package/dist/esm/word/numbering-parser.js +6 -6
  91. package/dist/esm/word/paragraph-properties.d.ts +2 -1
  92. package/dist/esm/word/paragraph-properties.js +2 -2
  93. package/dist/esm/word/run-properties.d.ts +5 -2
  94. package/dist/esm/word/run-properties.js +13 -8
  95. package/dist/esm/word/styles-parser.d.ts +2 -1
  96. package/dist/esm/word/styles-parser.js +12 -12
  97. package/dist/esm/word/table-parser.js +6 -3
  98. package/dist/esm/word/theme-fonts.d.ts +10 -0
  99. package/dist/esm/word/theme-fonts.js +28 -0
  100. package/package.json +1 -1
  101. /package/dist/esm/{pdf-reader → core}/png-encode.d.ts +0 -0
@@ -6,6 +6,48 @@ var VARIANT_SUFFIX = {
6
6
  italic: "400Regular_Italic",
7
7
  boldItalic: "700Bold_Italic"
8
8
  };
9
+ var SCRIPTS = {
10
+ jp: {
11
+ pkg: "noto-sans-jp",
12
+ file: "NotoSansJP"
13
+ },
14
+ kr: {
15
+ pkg: "noto-sans-kr",
16
+ file: "NotoSansKR"
17
+ },
18
+ sc: {
19
+ pkg: "noto-sans-sc",
20
+ file: "NotoSansSC"
21
+ },
22
+ arabic: {
23
+ pkg: "noto-sans-arabic",
24
+ file: "NotoSansArabic"
25
+ },
26
+ hebrew: {
27
+ pkg: "noto-sans-hebrew",
28
+ file: "NotoSansHebrew"
29
+ },
30
+ thai: {
31
+ pkg: "noto-sans-thai",
32
+ file: "NotoSansThai"
33
+ },
34
+ symbols: {
35
+ pkg: "noto-sans-symbols-2",
36
+ file: "NotoSansSymbols2"
37
+ }
38
+ };
39
+ /**
40
+ * Fetch the ONE face a writing system is drawn with.
41
+ *
42
+ * @param script The writing system.
43
+ * @param fetchImpl Injectable `fetch` (defaults to the global one).
44
+ * @returns The regular face, or `undefined` when the download fails.
45
+ */
46
+ async function fetchScriptFont(script, fetchImpl = globalThis.fetch.bind(globalThis)) {
47
+ const family = SCRIPTS[script];
48
+ const bytes = await fetchTtf(`${CDN_BASE}/${family.pkg}/400Regular/${family.file}_400Regular.ttf`, fetchImpl, false);
49
+ return bytes ? { regular: bytes } : void 0;
50
+ }
9
51
  var FAMILIES = {
10
52
  arimo: {
11
53
  pkg: "arimo",
@@ -31,6 +73,7 @@ var FAMILIES = {
31
73
  };
32
74
  var EXACT = {
33
75
  calibri: "carlito",
76
+ "calibri light": "carlito",
34
77
  cambria: "caladea",
35
78
  arial: "arimo",
36
79
  helvetica: "arimo",
@@ -59,22 +102,85 @@ var MONO = new Set([
59
102
  "dejavu sans mono",
60
103
  "monospace"
61
104
  ]);
105
+ var WEIGHT_WORDS = new Map([
106
+ ["black", "bold"],
107
+ ["heavy", "bold"],
108
+ ["bold", "bold"],
109
+ ["semibold", "bold"],
110
+ ["demibold", "bold"],
111
+ ["demi", "bold"],
112
+ ["italic", "italic"],
113
+ ["oblique", "italic"],
114
+ ["light", "none"],
115
+ ["thin", "none"],
116
+ ["medium", "none"],
117
+ ["regular", "none"],
118
+ ["book", "none"],
119
+ ["narrow", "narrow"],
120
+ ["condensed", "narrow"],
121
+ ["cond", "narrow"],
122
+ ["expanded", "none"],
123
+ ["extra", "none"],
124
+ ["ultra", "none"],
125
+ ["pro", "none"]
126
+ ]);
127
+ /** Arial Narrow's advance widths, as a fraction of Arial's. */
128
+ var NARROW_SCALE = .82;
62
129
  /**
63
130
  * Map a document-referenced font family to a curated open substitute: an exact
64
131
  * metric twin when one is known (e.g. Calibri → Carlito), otherwise a
65
132
  * serif/mono/sans style fallback.
66
133
  *
134
+ * The name may also carry the FACE — `Times New Roman Bold` is the Times
135
+ * family, and read whole it matches no twin at all and fell through to the
136
+ * generic sans, which is how a Times heading came out in a grotesque.
137
+ *
138
+ * @param name The referenced family name (case-insensitive); empty ⇒ Arimo.
139
+ * @returns The chosen family and what the name said about the face.
140
+ */
141
+ function resolveFamilyStyle(name) {
142
+ if (!name) return { key: "arimo" };
143
+ const words = name.trim().toLowerCase().split(/[\s\-_,]+/u).filter((w) => w !== "");
144
+ let bold = false;
145
+ let italic = false;
146
+ let narrow = false;
147
+ while (words.length > 1) {
148
+ const word = WEIGHT_WORDS.get(words[words.length - 1]);
149
+ if (word === void 0) break;
150
+ if (word === "bold") bold = true;
151
+ if (word === "italic") italic = true;
152
+ if (word === "narrow") narrow = true;
153
+ words.pop();
154
+ }
155
+ const tries = [
156
+ words.join(" "),
157
+ words[words.length - 1],
158
+ words[0]
159
+ ];
160
+ let key = "arimo";
161
+ for (const n of tries) {
162
+ const found = EXACT[n] ?? (MONO.has(n) ? "cousine" : SERIF.has(n) ? "tinos" : void 0);
163
+ if (found) {
164
+ key = found;
165
+ break;
166
+ }
167
+ }
168
+ return {
169
+ key,
170
+ ...bold ? { bold } : {},
171
+ ...italic ? { italic } : {},
172
+ ...narrow ? { widthScale: NARROW_SCALE } : {}
173
+ };
174
+ }
175
+ /**
176
+ * The curated substitute for a family name — {@link resolveFamilyStyle} without
177
+ * the face it also carries.
178
+ *
67
179
  * @param name The referenced family name (case-insensitive); empty ⇒ Arimo.
68
180
  * @returns The chosen {@link FamilyKey}.
69
181
  */
70
182
  function resolveFamilyKey(name) {
71
- if (!name) return "arimo";
72
- const n = name.trim().toLowerCase();
73
- const exact = EXACT[n];
74
- if (exact) return exact;
75
- if (MONO.has(n)) return "cousine";
76
- if (SERIF.has(n)) return "tinos";
77
- return "arimo";
183
+ return resolveFamilyStyle(name).key;
78
184
  }
79
185
  function fontUrl(family, variant) {
80
186
  const suffix = VARIANT_SUFFIX[variant];
@@ -128,4 +234,4 @@ async function fetchFontSet(options = {}) {
128
234
  };
129
235
  }
130
236
  //#endregion
131
- export { fetchFontSet, resolveFamilyKey };
237
+ export { fetchFontSet, fetchScriptFont, resolveFamilyKey, resolveFamilyStyle };
@@ -0,0 +1,26 @@
1
+ import { FlowDoc } from '../ir/flow.js';
2
+ import { ScriptKey } from './remote-fonts.js';
3
+ /**
4
+ * The writing system a character needs a face for, or `undefined` when a Latin
5
+ * family covers it.
6
+ *
7
+ * @param cp The code point.
8
+ * @param hanFace Which face draws unified Han in this document (see
9
+ * {@link scriptsInFlow}); defaults to Simplified.
10
+ */
11
+ export declare function scriptForCodepoint(cp: number, hanFace?: ScriptKey): ScriptKey | undefined;
12
+ /**
13
+ * The writing systems a document holds text in, beside the Latin one.
14
+ *
15
+ * Unified Han is written the same in Japanese, Korean and Chinese and drawn
16
+ * differently in each, and the character does not say which — the document
17
+ * does: Kana beside it means Japanese, Hangul means Korean. So the whole text
18
+ * is read once, and the answer applies to every Han character in it.
19
+ *
20
+ * @param flow The parsed document.
21
+ * @returns The scripts to fetch a face for, and the one Han is drawn with.
22
+ */
23
+ export declare function scriptsInFlow(flow: FlowDoc): {
24
+ scripts: Set<ScriptKey>;
25
+ hanFace: ScriptKey;
26
+ };
@@ -0,0 +1,177 @@
1
+ //#region src/core/fonts/scripts.ts
2
+ var RANGES = [
3
+ [
4
+ 1424,
5
+ 1535,
6
+ "hebrew"
7
+ ],
8
+ [
9
+ 1536,
10
+ 1791,
11
+ "arabic"
12
+ ],
13
+ [
14
+ 1872,
15
+ 1919,
16
+ "arabic"
17
+ ],
18
+ [
19
+ 2208,
20
+ 2303,
21
+ "arabic"
22
+ ],
23
+ [
24
+ 3584,
25
+ 3711,
26
+ "thai"
27
+ ],
28
+ [
29
+ 4352,
30
+ 4607,
31
+ "kr"
32
+ ],
33
+ [
34
+ 8592,
35
+ 8703,
36
+ "symbols"
37
+ ],
38
+ [
39
+ 8960,
40
+ 9215,
41
+ "symbols"
42
+ ],
43
+ [
44
+ 9312,
45
+ 9471,
46
+ "symbols"
47
+ ],
48
+ [
49
+ 9472,
50
+ 10175,
51
+ "symbols"
52
+ ],
53
+ [
54
+ 11008,
55
+ 11263,
56
+ "symbols"
57
+ ],
58
+ [
59
+ 12288,
60
+ 12351,
61
+ "han"
62
+ ],
63
+ [
64
+ 12352,
65
+ 12543,
66
+ "jp"
67
+ ],
68
+ [
69
+ 12592,
70
+ 12687,
71
+ "kr"
72
+ ],
73
+ [
74
+ 13312,
75
+ 19903,
76
+ "han"
77
+ ],
78
+ [
79
+ 19968,
80
+ 40959,
81
+ "han"
82
+ ],
83
+ [
84
+ 44032,
85
+ 55215,
86
+ "kr"
87
+ ],
88
+ [
89
+ 63744,
90
+ 64255,
91
+ "han"
92
+ ],
93
+ [
94
+ 64285,
95
+ 64335,
96
+ "hebrew"
97
+ ],
98
+ [
99
+ 64336,
100
+ 65023,
101
+ "arabic"
102
+ ],
103
+ [
104
+ 65136,
105
+ 65279,
106
+ "arabic"
107
+ ],
108
+ [
109
+ 65280,
110
+ 65519,
111
+ "han"
112
+ ]
113
+ ];
114
+ /**
115
+ * The writing system a character needs a face for, or `undefined` when a Latin
116
+ * family covers it.
117
+ *
118
+ * @param cp The code point.
119
+ * @param hanFace Which face draws unified Han in this document (see
120
+ * {@link scriptsInFlow}); defaults to Simplified.
121
+ */
122
+ function scriptForCodepoint(cp, hanFace = "sc") {
123
+ for (const [lo, hi, key] of RANGES) {
124
+ if (cp < lo) break;
125
+ if (cp <= hi) return key === "han" ? hanFace : key;
126
+ }
127
+ }
128
+ /**
129
+ * The writing systems a document holds text in, beside the Latin one.
130
+ *
131
+ * Unified Han is written the same in Japanese, Korean and Chinese and drawn
132
+ * differently in each, and the character does not say which — the document
133
+ * does: Kana beside it means Japanese, Hangul means Korean. So the whole text
134
+ * is read once, and the answer applies to every Han character in it.
135
+ *
136
+ * @param flow The parsed document.
137
+ * @returns The scripts to fetch a face for, and the one Han is drawn with.
138
+ */
139
+ function scriptsInFlow(flow) {
140
+ const scripts = /* @__PURE__ */ new Set();
141
+ const marks = /* @__PURE__ */ new Set();
142
+ const read = (text) => {
143
+ for (const ch of text) {
144
+ const cp = ch.codePointAt(0) ?? 0;
145
+ if (cp < 1424) continue;
146
+ const key = scriptForCodepoint(cp, "sc");
147
+ if (key === void 0) continue;
148
+ if (key === "sc") {
149
+ marks.add("han");
150
+ continue;
151
+ }
152
+ scripts.add(key);
153
+ }
154
+ };
155
+ const shapeText = (shape) => {
156
+ if (shape.text) visit(shape.text.content);
157
+ for (const child of shape.children ?? []) shapeText(child.shape);
158
+ };
159
+ const visit = (elements) => {
160
+ for (const el of elements) if (el.kind === "paragraph") for (const run of el.paragraph.runs) read(run.text);
161
+ else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) visit(cell.content);
162
+ else if (el.kind === "shape") shapeText(el.shape);
163
+ };
164
+ visit(flow.body);
165
+ for (const band of flow.headersFooters?.values() ?? []) visit(band);
166
+ for (const note of flow.footnotes?.values() ?? []) visit(note);
167
+ for (const note of flow.endnotes?.values() ?? []) visit(note);
168
+ for (const comment of flow.comments?.values() ?? []) visit(comment.content);
169
+ const hanFace = scripts.has("jp") ? "jp" : scripts.has("kr") ? "kr" : "sc";
170
+ if (marks.has("han")) scripts.add(hanFace);
171
+ return {
172
+ scripts,
173
+ hanFace
174
+ };
175
+ }
176
+ //#endregion
177
+ export { scriptForCodepoint, scriptsInFlow };
@@ -1,11 +1,31 @@
1
1
  /** The raster formats this module recognizes and can prepare for embedding. */
2
- export type ImageFormat = 'jpeg' | 'png' | 'jpeg2000' | 'gif' | 'tiff';
2
+ export type ImageFormat = 'jpeg' | 'png' | 'jpeg2000' | 'gif' | 'tiff' | 'bmp';
3
3
  /**
4
4
  * Sniff the raster format from a file's leading magic bytes (JPEG SOI, the PNG
5
5
  * signature, or a JP2 box / raw JPEG 2000 codestream). Returns the
6
6
  * {@link ImageFormat}, or `null` when none matches.
7
7
  */
8
8
  export declare function detectImageFormat(bytes: Uint8Array): ImageFormat | null;
9
+ /**
10
+ * The same picture with one colour knocked out of it, as a PNG.
11
+ *
12
+ * A picture may name a colour it is drawn WITHOUT — MS-ODRAW's
13
+ * `pictureTransparent`, DrawingML's `a:clrChange` to nothing — which is how
14
+ * clip art of the pre-alpha age says "this rectangle of ground is not part of
15
+ * the drawing". It is a property of the USE, not of the file, so it is baked
16
+ * here into bytes of its own: two shapes may knock different colours out of the
17
+ * same blip, and a resource store that hashes content then keeps them apart by
18
+ * itself.
19
+ *
20
+ * 23884's satellite sits on a red field and its globe on a white one; drawn as
21
+ * stored, they are a red block and a white square on a blue slide.
22
+ *
23
+ * @param bytes The picture, in any format {@link prepareImage} reads.
24
+ * @param hex The colour to knock out, 6-hex and no leading `#`.
25
+ * @returns A PNG with that colour transparent, or `undefined` when the picture
26
+ * cannot be decoded to samples this can work on.
27
+ */
28
+ export declare function knockOutColor(bytes: Uint8Array, hex: string): Uint8Array | undefined;
9
29
  /** Options controlling how {@link prepareImage} emits an image. */
10
30
  export interface EmbedImageOptions {
11
31
  /**
@@ -24,7 +44,7 @@ export interface EmbedImageOptions {
24
44
  */
25
45
  export interface PreparedImage {
26
46
  readonly format: ImageFormat;
27
- readonly mimeType: 'image/jp2' | 'image/jpeg' | 'image/png' | 'image/gif' | 'image/tiff';
47
+ readonly mimeType: 'image/jp2' | 'image/jpeg' | 'image/png' | 'image/gif' | 'image/tiff' | 'image/bmp';
28
48
  readonly widthPx: number;
29
49
  readonly heightPx: number;
30
50
  /**
@@ -38,6 +58,14 @@ export interface PreparedImage {
38
58
  readonly data: Uint8Array;
39
59
  /** PNG alpha channel, already FlateDecode-compressed (DeviceGray, 8 bpc). */
40
60
  readonly smaskData?: Uint8Array;
61
+ /**
62
+ * The resolution the picture states for itself, in pixels per inch, when it
63
+ * states one (JFIF's `Xdensity`/`Ydensity`, PNG's `pHYs`). It is what makes a
64
+ * picture's NATURAL size: 800 pixels at 300 dpi is 192 points wide, not the
65
+ * 600 the 96-dpi default would give. Absent ⇒ the reader's own default.
66
+ */
67
+ readonly dpiX?: number;
68
+ readonly dpiY?: number;
41
69
  }
42
70
  /**
43
71
  * Decode and validate one image into a {@link PreparedImage} ready to embed.
@@ -1,3 +1,5 @@
1
+ import { decodeBmp, isBmp } from "./bmp.js";
2
+ import { encodePng } from "./png-encode.js";
1
3
  import { decodeTiff, isTiff } from "./tiff.js";
2
4
  import { unzlibSync, zlibSync } from "fflate";
3
5
  //#region src/core/images.ts
@@ -13,9 +15,61 @@ function detectImageFormat(bytes) {
13
15
  if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 79 && bytes[2] === 255 && bytes[3] === 81) return "jpeg2000";
14
16
  if (bytes.length >= 6 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56 && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97) return "gif";
15
17
  if (isTiff(bytes)) return "tiff";
18
+ if (isBmp(bytes)) return "bmp";
16
19
  return null;
17
20
  }
18
21
  /**
22
+ * The same picture with one colour knocked out of it, as a PNG.
23
+ *
24
+ * A picture may name a colour it is drawn WITHOUT — MS-ODRAW's
25
+ * `pictureTransparent`, DrawingML's `a:clrChange` to nothing — which is how
26
+ * clip art of the pre-alpha age says "this rectangle of ground is not part of
27
+ * the drawing". It is a property of the USE, not of the file, so it is baked
28
+ * here into bytes of its own: two shapes may knock different colours out of the
29
+ * same blip, and a resource store that hashes content then keeps them apart by
30
+ * itself.
31
+ *
32
+ * 23884's satellite sits on a red field and its globe on a white one; drawn as
33
+ * stored, they are a red block and a white square on a blue slide.
34
+ *
35
+ * @param bytes The picture, in any format {@link prepareImage} reads.
36
+ * @param hex The colour to knock out, 6-hex and no leading `#`.
37
+ * @returns A PNG with that colour transparent, or `undefined` when the picture
38
+ * cannot be decoded to samples this can work on.
39
+ */
40
+ function knockOutColor(bytes, hex) {
41
+ let prepared;
42
+ try {
43
+ prepared = prepareImage(bytes);
44
+ } catch {
45
+ return;
46
+ }
47
+ if (prepared.filter !== "FlateDecode" || prepared.bitsPerComponent !== 8) return void 0;
48
+ const key = [
49
+ 0,
50
+ 2,
51
+ 4
52
+ ].map((i) => Number.parseInt(hex.slice(i, i + 2), 16));
53
+ if (key.some((c) => !Number.isFinite(c))) return void 0;
54
+ const gray = prepared.colorSpace === "DeviceGray";
55
+ const src = unzlibSync(prepared.data);
56
+ const existing = prepared.smaskData ? unzlibSync(prepared.smaskData) : void 0;
57
+ const { widthPx: w, heightPx: h } = prepared;
58
+ const channels = gray ? 1 : 3;
59
+ if (src.length < w * h * channels) return void 0;
60
+ const out = new Uint8Array(w * h * 4);
61
+ for (let i = 0; i < w * h; i++) {
62
+ const r = gray ? src[i] : src[i * 3];
63
+ const g = gray ? src[i] : src[i * 3 + 1];
64
+ const b = gray ? src[i] : src[i * 3 + 2];
65
+ out[i * 4] = r;
66
+ out[i * 4 + 1] = g;
67
+ out[i * 4 + 2] = b;
68
+ out[i * 4 + 3] = r === key[0] && g === key[1] && b === key[2] ? 0 : existing?.[i] ?? 255;
69
+ }
70
+ return encodePng(w, h, "rgba", out);
71
+ }
72
+ /**
19
73
  * Decode and validate one image into a {@link PreparedImage} ready to embed.
20
74
  * JPEG and JPEG 2000 pass through verbatim (readers decode them); PNG is
21
75
  * inflated, de-filtered and re-compressed, splitting any alpha into a soft mask.
@@ -29,6 +83,7 @@ function prepareImage(bytes, options = {}) {
29
83
  if (format === "jpeg2000") return prepareJpeg2000(bytes);
30
84
  if (format === "gif") return prepareGif(bytes, options);
31
85
  if (format === "tiff") return prepareTiff(bytes, options);
86
+ if (format === "bmp") return prepareBmp(bytes, options);
32
87
  throw new Error("Unsupported image format");
33
88
  }
34
89
  function prepareJpeg2000(bytes) {
@@ -91,6 +146,7 @@ function readSiz(bytes, sizOffset) {
91
146
  }
92
147
  function prepareJpeg(bytes) {
93
148
  const info = readJpegInfo(bytes);
149
+ const density = readJfifDensity(bytes);
94
150
  return {
95
151
  format: "jpeg",
96
152
  mimeType: "image/jpeg",
@@ -99,7 +155,24 @@ function prepareJpeg(bytes) {
99
155
  colorSpace: info.numComponents === 1 ? "DeviceGray" : "DeviceRGB",
100
156
  bitsPerComponent: info.precision,
101
157
  filter: "DCTDecode",
102
- data: bytes
158
+ data: bytes,
159
+ ...density ?? {}
160
+ };
161
+ }
162
+ function readJfifDensity(bytes) {
163
+ if (bytes.length < 20 || bytes[2] !== 255 || bytes[3] !== 224) return void 0;
164
+ if (!(bytes[6] === 74 && bytes[7] === 70 && bytes[8] === 73 && bytes[9] === 70)) return void 0;
165
+ const units = bytes[13];
166
+ const x = bytes[14] << 8 | bytes[15];
167
+ const y = bytes[16] << 8 | bytes[17];
168
+ if (x <= 0 || y <= 0) return void 0;
169
+ if (units === 1) return {
170
+ dpiX: x,
171
+ dpiY: y
172
+ };
173
+ if (units === 2) return {
174
+ dpiX: x * 2.54,
175
+ dpiY: y * 2.54
103
176
  };
104
177
  }
105
178
  function readJpegInfo(bytes) {
@@ -198,6 +271,34 @@ function prepareTiff(bytes, options = {}) {
198
271
  ...alpha && !options.flattenAlpha ? { smaskData: zlibSync(alpha.slice()) } : {}
199
272
  };
200
273
  }
274
+ function prepareBmp(bytes, options = {}) {
275
+ const img = decodeBmp(bytes);
276
+ const data = img.data.slice();
277
+ const alpha = img.alpha;
278
+ if (alpha && options.flattenAlpha) for (let i = 0; i < alpha.length; i++) {
279
+ if (alpha[i] === 255) continue;
280
+ const a = alpha[i] / 255;
281
+ for (let c = 0; c < 3; c++) {
282
+ const at = i * 3 + c;
283
+ data[at] = Math.round(data[at] * a + 255 * (1 - a));
284
+ }
285
+ }
286
+ return {
287
+ format: "bmp",
288
+ mimeType: "image/bmp",
289
+ widthPx: img.width,
290
+ heightPx: img.height,
291
+ colorSpace: "DeviceRGB",
292
+ bitsPerComponent: 8,
293
+ filter: "FlateDecode",
294
+ data: zlibSync(data),
295
+ ...alpha && !options.flattenAlpha ? { smaskData: zlibSync(alpha.slice()) } : {},
296
+ ...img.dpiX !== void 0 && img.dpiY !== void 0 ? {
297
+ dpiX: img.dpiX,
298
+ dpiY: img.dpiY
299
+ } : {}
300
+ };
301
+ }
201
302
  function cmykToRgb(cmyk) {
202
303
  const out = new Uint8Array(cmyk.length / 4 * 3);
203
304
  for (let i = 0; i * 4 + 3 < cmyk.length; i++) {
@@ -356,7 +457,11 @@ function preparePng(bytes, options = {}) {
356
457
  bitsPerComponent: decoded.bitsPerComponent,
357
458
  filter: "FlateDecode",
358
459
  data: zlibSync(decoded.raw),
359
- ...decoded.smaskRaw ? { smaskData: zlibSync(decoded.smaskRaw) } : {}
460
+ ...decoded.smaskRaw ? { smaskData: zlibSync(decoded.smaskRaw) } : {},
461
+ ...decoded.dpiX !== void 0 && decoded.dpiY !== void 0 ? {
462
+ dpiX: decoded.dpiX,
463
+ dpiY: decoded.dpiY
464
+ } : {}
360
465
  };
361
466
  }
362
467
  function decodePng(bytes) {
@@ -367,6 +472,7 @@ function decodePng(bytes) {
367
472
  let interlaceMethod = 0;
368
473
  let palette;
369
474
  let paletteAlpha;
475
+ let density = {};
370
476
  const idatChunks = [];
371
477
  let pos = 8;
372
478
  while (pos + 12 <= bytes.length) {
@@ -381,7 +487,14 @@ function decodePng(bytes) {
381
487
  interlaceMethod = bytes[dataOff + 12];
382
488
  } else if (type === "PLTE") palette = bytes.subarray(dataOff, dataOff + len);
383
489
  else if (type === "tRNS") paletteAlpha = bytes.subarray(dataOff, dataOff + len);
384
- else if (type === "IDAT") idatChunks.push(bytes.subarray(dataOff, dataOff + len));
490
+ else if (type === "pHYs") {
491
+ const perX = readU32BE(bytes, dataOff);
492
+ const perY = readU32BE(bytes, dataOff + 4);
493
+ if (bytes[dataOff + 8] === 1 && perX > 0 && perY > 0) density = {
494
+ dpiX: perX * .0254,
495
+ dpiY: perY * .0254
496
+ };
497
+ } else if (type === "IDAT") idatChunks.push(bytes.subarray(dataOff, dataOff + len));
385
498
  else if (type === "IEND") break;
386
499
  pos = dataOff + len + 4;
387
500
  }
@@ -397,7 +510,10 @@ function decodePng(bytes) {
397
510
  const channels = pngChannels(colorType);
398
511
  if (channels === 0) throw new Error(`PNG color type ${colorType} not supported`);
399
512
  const raw = decodeSamples(unzlibSync(concatBytes(idatChunks)), width, height, channels, bitDepth, interlaceMethod === 1, colorType !== 3);
400
- return splitChannels(width, height, colorType, raw, palette, paletteAlpha);
513
+ return {
514
+ ...splitChannels(width, height, colorType, raw, palette, paletteAlpha),
515
+ ...density
516
+ };
401
517
  }
402
518
  /**
403
519
  * §7.2 / §9 — the image's samples, one byte each, in `width × height ×
@@ -665,4 +781,4 @@ function concatBytes(parts) {
665
781
  return out;
666
782
  }
667
783
  //#endregion
668
- export { detectImageFormat, prepareImage };
784
+ export { detectImageFormat, knockOutColor, prepareImage };
@@ -0,0 +1,33 @@
1
+ import { DibImage } from './dib.js';
2
+ import { PictureImage } from './picture.js';
3
+ /** How a blit's ROP wants the source drawn. */
4
+ export type BlitMode =
5
+ /** SRCCOPY and its neighbours: the source replaces what is under it. */
6
+ 'opaque'
7
+ /** SRCAND: a mask, held until the picture it belongs to arrives. */
8
+ | 'mask'
9
+ /** SRCPAINT: ORed in, so the source's BLACK leaves the ground showing. */
10
+ | 'or'
11
+ /** A ROP that ignores the source, or one whose result is not a picture. */
12
+ | 'skip';
13
+ /** The destination rectangle a blit paints, in the metafile's own units. */
14
+ export interface BlitRect {
15
+ readonly x: number;
16
+ readonly y: number;
17
+ readonly width: number;
18
+ readonly height: number;
19
+ }
20
+ /**
21
+ * What a ternary raster operation asks a reader to draw.
22
+ *
23
+ * @param rop The ROP code, as the record states it.
24
+ */
25
+ export declare function blitMode(rop: number): BlitMode;
26
+ /**
27
+ * A sink for a metafile's blits that puts the AND/OR pair back together.
28
+ *
29
+ * @param emit Where a finished picture goes — the reader's primitive list.
30
+ */
31
+ export declare function makeBlitter(emit: (prim: PictureImage) => void): {
32
+ blit: (img: DibImage, dest: BlitRect, rop: number) => void;
33
+ };