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.
- package/dist/esm/core/bmp.d.ts +33 -0
- package/dist/esm/core/bmp.js +276 -0
- package/dist/esm/core/converter/ream.d.ts +16 -0
- package/dist/esm/core/converter/ream.js +59 -9
- package/dist/esm/core/document-model/types.d.ts +55 -0
- package/dist/esm/core/drawingml/chart-geometry.js +14 -7
- package/dist/esm/core/drawingml/colors.d.ts +4 -2
- package/dist/esm/core/drawingml/colors.js +15 -0
- package/dist/esm/core/drawingml/diagram/colors.d.ts +52 -0
- package/dist/esm/core/drawingml/diagram/colors.js +125 -0
- package/dist/esm/core/drawingml/diagram/data-model.d.ts +47 -0
- package/dist/esm/core/drawingml/diagram/data-model.js +119 -0
- package/dist/esm/core/drawingml/diagram/layout-engine.d.ts +46 -0
- package/dist/esm/core/drawingml/diagram/layout-engine.js +948 -0
- package/dist/esm/core/drawingml/diagram/run.d.ts +37 -0
- package/dist/esm/core/drawingml/diagram/run.js +50 -0
- package/dist/esm/core/drawingml/diagram/to-drawing.d.ts +16 -0
- package/dist/esm/core/drawingml/diagram/to-drawing.js +86 -0
- package/dist/esm/core/drawingml/preset-geometry.js +116 -6
- package/dist/esm/core/drawingml/text-warp.d.ts +58 -0
- package/dist/esm/core/drawingml/text-warp.js +355 -0
- package/dist/esm/core/drawingml/theme-parser.d.ts +38 -0
- package/dist/esm/core/drawingml/theme-parser.js +73 -4
- package/dist/esm/core/font/measure.d.ts +12 -0
- package/dist/esm/core/font/measure.js +36 -0
- package/dist/esm/core/fonts/families.d.ts +10 -0
- package/dist/esm/core/fonts/families.js +44 -0
- package/dist/esm/core/fonts/index.d.ts +3 -2
- package/dist/esm/core/fonts/remote-fonts.d.ts +50 -0
- package/dist/esm/core/fonts/remote-fonts.js +114 -8
- package/dist/esm/core/fonts/scripts.d.ts +26 -0
- package/dist/esm/core/fonts/scripts.js +177 -0
- package/dist/esm/core/images.d.ts +30 -2
- package/dist/esm/core/images.js +121 -5
- package/dist/esm/core/metafile/blit.d.ts +33 -0
- package/dist/esm/core/metafile/blit.js +67 -0
- package/dist/esm/core/metafile/dib.d.ts +61 -0
- package/dist/esm/core/metafile/dib.js +205 -0
- package/dist/esm/core/metafile/emf.js +280 -38
- package/dist/esm/core/metafile/picture.d.ts +61 -1
- package/dist/esm/core/metafile/picture.js +70 -1
- package/dist/esm/core/metafile/wmf.js +202 -13
- package/dist/esm/core/ole/escher-blip.d.ts +12 -0
- package/dist/esm/core/ole/escher-blip.js +98 -0
- package/dist/esm/{pdf-reader → core}/png-encode.js +1 -1
- package/dist/esm/excel/print-model.js +5 -3
- package/dist/esm/excel/sheet-drawing.js +1 -1
- package/dist/esm/excel/sheet-to-flow.js +16 -2
- package/dist/esm/excel/styles-parser.js +1 -1
- package/dist/esm/excel/xls/biff-reader.js +53 -18
- package/dist/esm/excel/xls/biff-styles.d.ts +5 -2
- package/dist/esm/excel/xls/biff-styles.js +9 -7
- package/dist/esm/excel/xls/escher.js +90 -24
- package/dist/esm/html/html-writer.js +2 -1
- package/dist/esm/layout/page-doc.d.ts +74 -0
- package/dist/esm/layout/page-doc.js +14 -1
- package/dist/esm/layout/styled-layout.d.ts +2 -2
- package/dist/esm/layout/styled-layout.js +475 -48
- package/dist/esm/pdf/shading.js +8 -6
- package/dist/esm/pdf/styled-page-emitter.js +77 -5
- package/dist/esm/pdf/vector-graphics.js +1 -1
- package/dist/esm/pdf-reader/image-decode.js +1 -1
- package/dist/esm/pptx/embedded-fonts.d.ts +31 -0
- package/dist/esm/pptx/embedded-fonts.js +104 -0
- package/dist/esm/pptx/placeholder-cascade.d.ts +73 -6
- package/dist/esm/pptx/placeholder-cascade.js +114 -18
- package/dist/esm/pptx/ppt/ppt-reader.js +191 -13
- package/dist/esm/pptx/ppt/ppt-text.d.ts +107 -1
- package/dist/esm/pptx/ppt/ppt-text.js +873 -144
- package/dist/esm/pptx/pptx-reader.d.ts +8 -0
- package/dist/esm/pptx/pptx-reader.js +117 -15
- package/dist/esm/pptx/preset-table-styles.d.ts +9 -0
- package/dist/esm/pptx/preset-table-styles.js +27 -0
- package/dist/esm/pptx/slide-parser.d.ts +11 -2
- package/dist/esm/pptx/slide-parser.js +203 -44
- package/dist/esm/pptx/sp-helpers.d.ts +9 -4
- package/dist/esm/pptx/sp-helpers.js +48 -9
- package/dist/esm/pptx/table-style.d.ts +16 -1
- package/dist/esm/pptx/table-style.js +104 -10
- package/dist/esm/svg/svg-writer.js +1 -0
- package/dist/esm/word/document-parser.d.ts +11 -2
- package/dist/esm/word/document-parser.js +10 -7
- package/dist/esm/word/docx-reader.js +32 -16
- package/dist/esm/word/docx-to-pdf.d.ts +8 -7
- package/dist/esm/word/docx-to-pdf.js +25 -7
- package/dist/esm/word/docx-writer.js +4 -0
- package/dist/esm/word/drawing-parser.d.ts +20 -0
- package/dist/esm/word/drawing-parser.js +67 -22
- package/dist/esm/word/numbering-parser.d.ts +2 -1
- package/dist/esm/word/numbering-parser.js +6 -6
- package/dist/esm/word/paragraph-properties.d.ts +2 -1
- package/dist/esm/word/paragraph-properties.js +2 -2
- package/dist/esm/word/run-properties.d.ts +5 -2
- package/dist/esm/word/run-properties.js +13 -8
- package/dist/esm/word/styles-parser.d.ts +2 -1
- package/dist/esm/word/styles-parser.js +12 -12
- package/dist/esm/word/table-parser.js +6 -3
- package/dist/esm/word/theme-fonts.d.ts +10 -0
- package/dist/esm/word/theme-fonts.js +28 -0
- package/package.json +1 -1
- /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
|
-
|
|
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.
|
package/dist/esm/core/images.js
CHANGED
|
@@ -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 === "
|
|
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
|
|
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
|
+
};
|