@docx-editor.dev/docx-to-pdf 0.0.1 → 2.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.
- package/LICENSE.md +121 -0
- package/README.md +127 -3
- package/THIRD_PARTY_NOTICES.md +32 -0
- package/assets/NotoEmoji-Regular.ttf +0 -0
- package/assets/NotoSansArabic-Regular.ttf +0 -0
- package/assets/NotoSansMath-Regular.ttf +0 -0
- package/assets/NotoSansSymbols2-Regular.ttf +0 -0
- package/assets/README.md +7 -0
- package/assets/TwemojiMozilla.ttf +0 -0
- package/assets/sources.json +60 -0
- package/dist/index.cjs +3299 -0
- package/dist/index.d.cts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +3270 -0
- package/docs/api.md +127 -0
- package/docs/fonts.md +151 -0
- package/docs/integrations.md +107 -0
- package/docs/markdown-contract.md +111 -0
- package/licenses/NotoEmoji-OFL.txt +93 -0
- package/licenses/NotoSansArabic-OFL.txt +93 -0
- package/licenses/NotoSansMath-OFL.txt +93 -0
- package/licenses/NotoSansSymbols2-OFL.txt +93 -0
- package/licenses/TwemojiMozilla-LICENSE.md +35 -0
- package/package.json +81 -5
package/dist/index.js
ADDED
|
@@ -0,0 +1,3270 @@
|
|
|
1
|
+
// src/export-pdf.ts
|
|
2
|
+
import { PDFDocument as PDFDocument5 } from "pdf-lib";
|
|
3
|
+
import { hasFontBackedExportCapabilities } from "@docx-editor.dev/core/export";
|
|
4
|
+
|
|
5
|
+
// src/context.ts
|
|
6
|
+
import { ExportResourceError } from "@docx-editor.dev/core/export";
|
|
7
|
+
import { deflateSync } from "zlib";
|
|
8
|
+
var MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
9
|
+
var MAX_OPERATIONS = 2e6;
|
|
10
|
+
function positiveLimit(value, maximum, name) {
|
|
11
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum)
|
|
12
|
+
throw new RangeError(`${name} must be an integer from 1 to ${maximum}`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function number(value) {
|
|
16
|
+
if (!Number.isFinite(value) || Math.abs(value) > 1e6)
|
|
17
|
+
throw new RangeError("Invalid PDF coordinate");
|
|
18
|
+
return String(Number(value.toFixed(6)));
|
|
19
|
+
}
|
|
20
|
+
var PAINT_GRID_PT = 0.24;
|
|
21
|
+
var onGrid = (value) => Math.round(value / PAINT_GRID_PT) * PAINT_GRID_PT;
|
|
22
|
+
function rect(box, x, y, height, grid = false) {
|
|
23
|
+
const left = box.x + x;
|
|
24
|
+
const bottom = height - box.y - y - box.height;
|
|
25
|
+
if (!grid)
|
|
26
|
+
return `${number(left)} ${number(bottom)} ${number(box.width)} ${number(box.height)} re`;
|
|
27
|
+
const extent = (value) => value > 0 ? Math.max(PAINT_GRID_PT, Math.floor(value / PAINT_GRID_PT) * PAINT_GRID_PT) : 0;
|
|
28
|
+
return `${number(onGrid(left))} ${number(onGrid(bottom))} ${number(extent(box.width))} ${number(extent(box.height))} re`;
|
|
29
|
+
}
|
|
30
|
+
function pdfLiteralUri(href) {
|
|
31
|
+
if (href.length === 0 || href.length > 2048) return null;
|
|
32
|
+
for (let index = 0; index < href.length; index += 1) {
|
|
33
|
+
const code = href.charCodeAt(index);
|
|
34
|
+
if (code < 32 || code > 126) return null;
|
|
35
|
+
}
|
|
36
|
+
return href.replace(/[\\()]/g, (character) => `\\${character}`);
|
|
37
|
+
}
|
|
38
|
+
function flateStream(context, contents, dict = {}) {
|
|
39
|
+
const bytes = typeof contents === "string" ? Buffer.from(contents, "latin1") : contents;
|
|
40
|
+
const deflated = deflateSync(bytes);
|
|
41
|
+
const owned = deflated.buffer.byteLength > deflated.byteLength * 2 ? new Uint8Array(deflated) : deflated;
|
|
42
|
+
return context.stream(owned, { ...dict, Filter: "FlateDecode" });
|
|
43
|
+
}
|
|
44
|
+
var pageHeights = /* @__PURE__ */ new WeakMap();
|
|
45
|
+
function pageHeight(page) {
|
|
46
|
+
let height = pageHeights.get(page);
|
|
47
|
+
if (height === void 0) pageHeights.set(page, height = page.getHeight());
|
|
48
|
+
return height;
|
|
49
|
+
}
|
|
50
|
+
var HEX_BYTES = Array.from({ length: 256 }, (_, byte) => byte.toString(16).padStart(2, "0"));
|
|
51
|
+
function hex(value) {
|
|
52
|
+
if (value >= 0 && value <= 65535 && Number.isInteger(value))
|
|
53
|
+
return HEX_BYTES[value >> 8] + HEX_BYTES[value & 255];
|
|
54
|
+
return value.toString(16).padStart(4, "0");
|
|
55
|
+
}
|
|
56
|
+
function unicodeHex(text) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (let i = 0; i < text.length; i++) out.push(hex(text.charCodeAt(i)));
|
|
59
|
+
return out.join("");
|
|
60
|
+
}
|
|
61
|
+
function color(value) {
|
|
62
|
+
const v = value && /^[a-f\d]{6}$/i.test(value) ? value : "000000";
|
|
63
|
+
return [0, 2, 4].map((i) => number(parseInt(v.slice(i, i + 2), 16) / 255)).join(" ");
|
|
64
|
+
}
|
|
65
|
+
var Work = class {
|
|
66
|
+
constructor(signal, deadline = Infinity) {
|
|
67
|
+
this.signal = signal;
|
|
68
|
+
this.deadline = deadline;
|
|
69
|
+
}
|
|
70
|
+
signal;
|
|
71
|
+
deadline;
|
|
72
|
+
diagnostics = [];
|
|
73
|
+
seen = /* @__PURE__ */ new Set();
|
|
74
|
+
operations = 0;
|
|
75
|
+
contentBytes = 0;
|
|
76
|
+
check() {
|
|
77
|
+
if (Date.now() >= this.deadline)
|
|
78
|
+
throw new ExportResourceError("timedOut", "PDF conversion timed out");
|
|
79
|
+
if (this.signal.aborted)
|
|
80
|
+
throw this.signal.reason instanceof ExportResourceError ? this.signal.reason : new ExportResourceError("aborted", "PDF conversion aborted", {
|
|
81
|
+
cause: this.signal.reason
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
reserveContent(bytes) {
|
|
85
|
+
this.contentBytes += bytes;
|
|
86
|
+
if (this.contentBytes > MAX_OUTPUT_BYTES) throw new PdfWorkLimitError();
|
|
87
|
+
}
|
|
88
|
+
tick() {
|
|
89
|
+
this.check();
|
|
90
|
+
if (++this.operations > MAX_OPERATIONS) throw new PdfWorkLimitError();
|
|
91
|
+
}
|
|
92
|
+
async yield() {
|
|
93
|
+
this.check();
|
|
94
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
95
|
+
this.check();
|
|
96
|
+
}
|
|
97
|
+
report(code, message, pageIndex, severity = "unsupported", origin) {
|
|
98
|
+
const key = `${code}:${pageIndex ?? ""}:${origin?.originIndex ?? ""}:${message}`;
|
|
99
|
+
if (this.seen.has(key)) return;
|
|
100
|
+
if (this.diagnostics.length >= 1e4) throw new PdfWorkLimitError();
|
|
101
|
+
this.seen.add(key);
|
|
102
|
+
this.diagnostics.push(
|
|
103
|
+
Object.freeze({
|
|
104
|
+
code,
|
|
105
|
+
message,
|
|
106
|
+
pageIndex,
|
|
107
|
+
...pageIndex === void 0 ? {} : { pageNumber: pageIndex + 1 },
|
|
108
|
+
severity,
|
|
109
|
+
...origin
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var PdfWorkLimitError = class extends Error {
|
|
115
|
+
code = "workLimitExceeded";
|
|
116
|
+
constructor() {
|
|
117
|
+
super("PDF content, operation, or diagnostic limit exceeded");
|
|
118
|
+
this.name = "PdfWorkLimitError";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var Commands = class extends Array {
|
|
122
|
+
constructor(work) {
|
|
123
|
+
super();
|
|
124
|
+
this.work = work;
|
|
125
|
+
}
|
|
126
|
+
work;
|
|
127
|
+
push(...values) {
|
|
128
|
+
for (const value of values) this.work.reserveContent(value.length + 1);
|
|
129
|
+
return super.push(...values);
|
|
130
|
+
}
|
|
131
|
+
unshift(...values) {
|
|
132
|
+
for (const value of values) this.work.reserveContent(value.length + 1);
|
|
133
|
+
return super.unshift(...values);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/options.ts
|
|
138
|
+
function validateOptions(options) {
|
|
139
|
+
if (!options || typeof options !== "object" || Array.isArray(options))
|
|
140
|
+
throw new TypeError("options must be an object");
|
|
141
|
+
for (const [name, choices] of [
|
|
142
|
+
["fidelityPolicy", ["strict", "best-effort"]],
|
|
143
|
+
["fontPolicy", ["strict", "best-effort"]],
|
|
144
|
+
["displayMode", ["proposed", "original", "all-markup"]]
|
|
145
|
+
]) {
|
|
146
|
+
const value = options[name];
|
|
147
|
+
if (value !== void 0 && !choices.includes(value))
|
|
148
|
+
throw new RangeError(`Invalid ${name}: expected ${choices.join(", ")}`);
|
|
149
|
+
}
|
|
150
|
+
for (const name of ["useSystemFonts", "comments", "documentLigatures"]) {
|
|
151
|
+
if (options[name] !== void 0 && typeof options[name] !== "boolean")
|
|
152
|
+
throw new TypeError(`${name} must be boolean`);
|
|
153
|
+
}
|
|
154
|
+
for (const name of ["resourceTimeoutMs", "fontResolutionTimeoutMs"]) {
|
|
155
|
+
const value = options[name];
|
|
156
|
+
if (value !== void 0 && (!Number.isFinite(value) || value <= 0 || value > 2147483647))
|
|
157
|
+
throw new RangeError(`${name} must be positive and at most 2147483647`);
|
|
158
|
+
}
|
|
159
|
+
for (const name of ["onFontResolution", "convertPreservedImage"]) {
|
|
160
|
+
if (options[name] !== void 0 && typeof options[name] !== "function")
|
|
161
|
+
throw new TypeError(`${name} must be a function`);
|
|
162
|
+
}
|
|
163
|
+
if (options.imageDecodePort !== void 0 && (!options.imageDecodePort || typeof options.imageDecodePort !== "object" || typeof options.imageDecodePort.decode !== "function"))
|
|
164
|
+
throw new TypeError("imageDecodePort must provide a decode function");
|
|
165
|
+
const signal = options.signal;
|
|
166
|
+
if (signal !== void 0 && (!signal || typeof signal.aborted !== "boolean" || typeof signal.addEventListener !== "function" || typeof signal.removeEventListener !== "function"))
|
|
167
|
+
throw new TypeError("signal must be an AbortSignal");
|
|
168
|
+
for (const name of ["measurer", "producer", "reuseAcrossRevisions"]) {
|
|
169
|
+
if (options[name] !== void 0)
|
|
170
|
+
throw new TypeError(`${name} is not supported by exportPdf`);
|
|
171
|
+
}
|
|
172
|
+
const { timeoutMs = 6e4, maxOutputBytes = MAX_OUTPUT_BYTES, maxPages = 1e4 } = options;
|
|
173
|
+
return {
|
|
174
|
+
timeoutMs: positiveLimit(timeoutMs, 2147483647, "timeoutMs"),
|
|
175
|
+
maxBytes: positiveLimit(maxOutputBytes, MAX_OUTPUT_BYTES, "maxOutputBytes"),
|
|
176
|
+
maxPages: positiveLimit(maxPages, 1e4, "maxPages")
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/open-session.ts
|
|
181
|
+
import {
|
|
182
|
+
createPackagedFileFetch,
|
|
183
|
+
openFontBackedDocumentForExport
|
|
184
|
+
} from "@docx-editor.dev/core/export";
|
|
185
|
+
import { HARD_MAX_FONT_BYTES } from "@docx-editor.dev/core/layout";
|
|
186
|
+
import { FONT_ASSET_ROOT as FONT_ASSET_ROOT2, packagedFonts } from "@docx-editor.dev/fonts";
|
|
187
|
+
|
|
188
|
+
// src/font-provisioning.ts
|
|
189
|
+
import { create as openFont } from "fontkit";
|
|
190
|
+
import { readFile, stat } from "fs/promises";
|
|
191
|
+
import { homedir } from "os";
|
|
192
|
+
import { join } from "path";
|
|
193
|
+
import { pathToFileURL, URL as NodeURL } from "url";
|
|
194
|
+
import { createFontSource, defineFontResolver } from "@docx-editor.dev/core/editor";
|
|
195
|
+
import { FONT_ASSET_ROOT } from "@docx-editor.dev/fonts";
|
|
196
|
+
var assetRoot = new NodeURL(
|
|
197
|
+
"../assets/",
|
|
198
|
+
typeof __dirname === "string" ? pathToFileURL(__dirname + "/").href : import.meta.url
|
|
199
|
+
);
|
|
200
|
+
var latinSubstitute = (family, prefix) => [
|
|
201
|
+
["Regular", 400, "normal"],
|
|
202
|
+
["Bold", 700, "normal"],
|
|
203
|
+
["Italic", 400, "italic"],
|
|
204
|
+
["BoldItalic", 700, "italic"]
|
|
205
|
+
].map(([suffix, weight, style]) => [
|
|
206
|
+
family,
|
|
207
|
+
new NodeURL(`${prefix}-${suffix}.ttf`, FONT_ASSET_ROOT),
|
|
208
|
+
weight,
|
|
209
|
+
style
|
|
210
|
+
]);
|
|
211
|
+
var CJK_FAMILY = "Noto Sans CJK JP";
|
|
212
|
+
var supplemental = [
|
|
213
|
+
["Noto Sans Symbols 2", new NodeURL("NotoSansSymbols2-Regular.ttf", assetRoot), 400, "normal"],
|
|
214
|
+
["Noto Sans Math", new NodeURL("NotoSansMath-Regular.ttf", assetRoot), 400, "normal"],
|
|
215
|
+
["Noto Sans Arabic", new NodeURL("NotoSansArabic-Regular.ttf", assetRoot), 400, "normal"],
|
|
216
|
+
["Twemoji Mozilla", new NodeURL("TwemojiMozilla.ttf", assetRoot), 400, "normal"],
|
|
217
|
+
["Noto Emoji", new NodeURL("NotoEmoji-Regular.ttf", assetRoot), 400, "normal"],
|
|
218
|
+
...latinSubstitute("Liberation Sans", "LiberationSans"),
|
|
219
|
+
...latinSubstitute("Liberation Serif", "LiberationSerif"),
|
|
220
|
+
...latinSubstitute("Liberation Mono", "LiberationMono"),
|
|
221
|
+
...latinSubstitute("Carlito", "Carlito"),
|
|
222
|
+
...latinSubstitute("Caladea", "Caladea")
|
|
223
|
+
];
|
|
224
|
+
var PDF_GLYPH_FALLBACKS = [
|
|
225
|
+
"Segoe UI Symbol",
|
|
226
|
+
"Noto Sans Symbols 2",
|
|
227
|
+
"Noto Sans Math",
|
|
228
|
+
"Noto Sans Arabic",
|
|
229
|
+
// Hebrew, and Latin, Greek or Cyrillic letters the faces above lack. A missing glyph falls
|
|
230
|
+
// back to Times New Roman in the reference renderer. A host without it resolves the packaged
|
|
231
|
+
// Liberation Serif, whose Latin letters share its widths but whose Hebrew runs about 12%
|
|
232
|
+
// wider, so Hebrew in that face can wrap earlier.
|
|
233
|
+
"Times New Roman",
|
|
234
|
+
// Prefer available Word CJK faces; Noto Sans CJK JP from `@docx-editor.dev/fonts-cjk` is
|
|
235
|
+
// the portable fallback when that package is installed.
|
|
236
|
+
"SimSun",
|
|
237
|
+
"Batang",
|
|
238
|
+
CJK_FAMILY,
|
|
239
|
+
// Color first: a COLR face paints its layers in the writer. The monochrome face stays
|
|
240
|
+
// behind it for the few symbols the color set leaves out.
|
|
241
|
+
"Twemoji Mozilla",
|
|
242
|
+
"Noto Emoji"
|
|
243
|
+
].map((family) => ({ family, weight: 400, style: "normal" }));
|
|
244
|
+
var face = (family, weight = 400, style = "normal") => ({ family, weight, style });
|
|
245
|
+
var substitutes = {
|
|
246
|
+
"Cambria Math": "Noto Sans Math",
|
|
247
|
+
Georgia: "Liberation Serif",
|
|
248
|
+
Verdana: "Liberation Sans",
|
|
249
|
+
Helvetica: "Liberation Sans",
|
|
250
|
+
...Object.fromEntries(
|
|
251
|
+
[
|
|
252
|
+
"MS Gothic",
|
|
253
|
+
"MS PGothic",
|
|
254
|
+
"MS UI Gothic",
|
|
255
|
+
"MS Mincho",
|
|
256
|
+
"MS PMincho",
|
|
257
|
+
"Meiryo",
|
|
258
|
+
"Yu Gothic",
|
|
259
|
+
"Yu Mincho",
|
|
260
|
+
"SimSun",
|
|
261
|
+
"NSimSun",
|
|
262
|
+
"SimHei",
|
|
263
|
+
"FangSong",
|
|
264
|
+
"KaiTi",
|
|
265
|
+
"DengXian",
|
|
266
|
+
"Microsoft YaHei",
|
|
267
|
+
"Microsoft JhengHei",
|
|
268
|
+
"MingLiU",
|
|
269
|
+
"PMingLiU",
|
|
270
|
+
"Batang",
|
|
271
|
+
"Gulim",
|
|
272
|
+
"Dotum",
|
|
273
|
+
"Malgun Gothic"
|
|
274
|
+
].map((family) => [family, CJK_FAMILY])
|
|
275
|
+
)
|
|
276
|
+
};
|
|
277
|
+
var localizedFamilies = {
|
|
278
|
+
\u5B8B\u4F53: "SimSun",
|
|
279
|
+
\u65B0\u5B8B\u4F53: "NSimSun",
|
|
280
|
+
\u9ED1\u4F53: "SimHei",
|
|
281
|
+
\u4EFF\u5B8B: "FangSong",
|
|
282
|
+
\u6977\u4F53: "KaiTi",
|
|
283
|
+
\u7B49\u7EBF: "DengXian",
|
|
284
|
+
\u5FAE\u8F6F\u96C5\u9ED1: "Microsoft YaHei",
|
|
285
|
+
\u5FAE\u8EDF\u6B63\u9ED1\u9AD4: "Microsoft JhengHei",
|
|
286
|
+
\u65B0\u7D30\u660E\u9AD4: "PMingLiU",
|
|
287
|
+
\u7D30\u660E\u9AD4: "MingLiU",
|
|
288
|
+
"\uFF2D\uFF33 \u660E\u671D": "MS Mincho",
|
|
289
|
+
"\uFF2D\uFF33 \uFF30\u660E\u671D": "MS PMincho",
|
|
290
|
+
"\uFF2D\uFF33 \u30B4\u30B7\u30C3\u30AF": "MS Gothic",
|
|
291
|
+
"\uFF2D\uFF33 \uFF30\u30B4\u30B7\u30C3\u30AF": "MS PGothic",
|
|
292
|
+
"\uFF2D\uFF33 \uFF35\uFF29 \uFF27\uFF4F\uFF54\uFF48\uFF49\uFF43": "MS UI Gothic",
|
|
293
|
+
\u30E1\u30A4\u30EA\u30AA: "Meiryo",
|
|
294
|
+
\u6E38\u30B4\u30B7\u30C3\u30AF: "Yu Gothic",
|
|
295
|
+
\u6E38\u660E\u671D: "Yu Mincho",
|
|
296
|
+
"\uB9D1\uC740 \uACE0\uB515": "Malgun Gothic",
|
|
297
|
+
\uBC14\uD0D5: "Batang",
|
|
298
|
+
\uAD74\uB9BC: "Gulim",
|
|
299
|
+
\uB3CB\uC6C0: "Dotum"
|
|
300
|
+
};
|
|
301
|
+
function canonicalFamily(family) {
|
|
302
|
+
let base = family;
|
|
303
|
+
let bold = false;
|
|
304
|
+
let italic = false;
|
|
305
|
+
if (family.length <= 128) {
|
|
306
|
+
const lower = family.toLowerCase();
|
|
307
|
+
for (const [suffix, flags] of [
|
|
308
|
+
[" bold italic", [true, true]],
|
|
309
|
+
[" bolditalic", [true, true]],
|
|
310
|
+
[" bold", [true, false]],
|
|
311
|
+
[" italic", [false, true]]
|
|
312
|
+
]) {
|
|
313
|
+
if (!lower.endsWith(suffix) || lower.length === suffix.length) continue;
|
|
314
|
+
base = family.slice(0, family.length - suffix.length).trimEnd();
|
|
315
|
+
[bold, italic] = flags;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
family: Object.hasOwn(localizedFamilies, base) ? localizedFamilies[base] : base,
|
|
321
|
+
bold,
|
|
322
|
+
italic
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
var packagedSubstitutes = {
|
|
326
|
+
Calibri: "Carlito",
|
|
327
|
+
Cambria: "Caladea",
|
|
328
|
+
"Times New Roman": "Liberation Serif",
|
|
329
|
+
Arial: "Liberation Sans",
|
|
330
|
+
"Courier New": "Liberation Mono",
|
|
331
|
+
"Century Gothic": "TeX Gyre Adventor"
|
|
332
|
+
};
|
|
333
|
+
var substituteFor = (family) => {
|
|
334
|
+
const canonical = canonicalFamily(family).family;
|
|
335
|
+
if (Object.hasOwn(substitutes, canonical)) return substitutes[canonical];
|
|
336
|
+
if (canonical !== family && Object.hasOwn(packagedSubstitutes, canonical))
|
|
337
|
+
return packagedSubstitutes[canonical];
|
|
338
|
+
return void 0;
|
|
339
|
+
};
|
|
340
|
+
function genericSubstituteFor(family) {
|
|
341
|
+
const name = canonicalFamily(family).family;
|
|
342
|
+
if (/mono|courier|consol|menlo|typewriter|\bcode\b|fixed/i.test(name)) return "Liberation Mono";
|
|
343
|
+
if (/symbol/i.test(name)) return "Noto Sans Symbols 2";
|
|
344
|
+
if (/sans/i.test(name)) return "Liberation Sans";
|
|
345
|
+
if (/serif|roman|garamond|georgia|times|book|baskerville|cambria|minion|palatino|century|didot|caslon|constantia|sagona|lora|merriweather|playfair|charter|bodoni|perpetua|rockwell|goudy|bembo|sabon|antiqua/i.test(
|
|
346
|
+
name
|
|
347
|
+
))
|
|
348
|
+
return "Liberation Serif";
|
|
349
|
+
return "Liberation Sans";
|
|
350
|
+
}
|
|
351
|
+
var knownFamilies = /* @__PURE__ */ new Set([
|
|
352
|
+
...supplemental.map(([family]) => family),
|
|
353
|
+
CJK_FAMILY,
|
|
354
|
+
...Object.keys(substitutes),
|
|
355
|
+
...Object.keys(packagedSubstitutes),
|
|
356
|
+
...Object.values(packagedSubstitutes),
|
|
357
|
+
"Symbol",
|
|
358
|
+
"Wingdings",
|
|
359
|
+
"Wingdings 2",
|
|
360
|
+
"Wingdings 3",
|
|
361
|
+
"Webdings",
|
|
362
|
+
"MT Extra"
|
|
363
|
+
]);
|
|
364
|
+
var cjkWordFamilies = /* @__PURE__ */ new Set([
|
|
365
|
+
CJK_FAMILY,
|
|
366
|
+
...Object.keys(substitutes).filter((family) => substitutes[family] === CJK_FAMILY)
|
|
367
|
+
]);
|
|
368
|
+
var CJK_PACKAGE = "@docx-editor.dev/fonts-cjk";
|
|
369
|
+
function isPackageNotFound(error, specifier) {
|
|
370
|
+
if (typeof error !== "object" || error === null) return false;
|
|
371
|
+
const { code, message } = error;
|
|
372
|
+
if (code !== "ERR_MODULE_NOT_FOUND" && code !== "MODULE_NOT_FOUND") return false;
|
|
373
|
+
if (typeof message !== "string") return false;
|
|
374
|
+
return [`Cannot find package '${specifier}'`, `Cannot find module '${specifier}'`].some(
|
|
375
|
+
(prefix) => message.startsWith(prefix)
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
async function importCjkFace() {
|
|
379
|
+
let module;
|
|
380
|
+
try {
|
|
381
|
+
module = await import(
|
|
382
|
+
/* webpackIgnore: true */
|
|
383
|
+
"@docx-editor.dev/fonts-cjk"
|
|
384
|
+
);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (isPackageNotFound(error, CJK_PACKAGE)) return null;
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
const file = module.NOTO_SANS_CJK_JP_URL;
|
|
390
|
+
if (typeof file?.href !== "string")
|
|
391
|
+
throw new TypeError(`${CJK_PACKAGE} does not export NOTO_SANS_CJK_JP_URL`);
|
|
392
|
+
return new NodeURL(file.href);
|
|
393
|
+
}
|
|
394
|
+
var CJK_TEXT = /[\u1100-\u11ff\u2e80-\u2fdf\u3000-\u9fff\ua960-\ua97f\uac00-\ud7ff\uf900-\ufaff\ufe30-\ufe4f\uff00-\uffef]|[\u{20000}-\u{3ffff}]/u;
|
|
395
|
+
var substitutionsTo = (family, target) => {
|
|
396
|
+
const { bold, italic } = canonicalFamily(family);
|
|
397
|
+
return [400, 700].flatMap(
|
|
398
|
+
(weight) => ["normal", "italic"].map((style) => ({
|
|
399
|
+
from: face(family, weight, style),
|
|
400
|
+
// The packaged CJK, symbol and mathematics faces come in one weight and one style.
|
|
401
|
+
to: target.startsWith("Noto") ? face(target) : face(
|
|
402
|
+
target,
|
|
403
|
+
bold || weight === 700 ? 700 : 400,
|
|
404
|
+
italic || style === "italic" ? "italic" : "normal"
|
|
405
|
+
)
|
|
406
|
+
}))
|
|
407
|
+
);
|
|
408
|
+
};
|
|
409
|
+
function packagedFontResolvers(locateCjkFace = importCjkFace) {
|
|
410
|
+
let located;
|
|
411
|
+
let cjkMissing = false;
|
|
412
|
+
const cjkFace = () => located ??= locateCjkFace().then(
|
|
413
|
+
(file) => {
|
|
414
|
+
cjkMissing = file === null;
|
|
415
|
+
return file;
|
|
416
|
+
},
|
|
417
|
+
(error) => {
|
|
418
|
+
cjkMissing = true;
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
const readFaces = (faces, signal) => Promise.all(
|
|
423
|
+
faces.map(async ([family, file, weight, style]) => {
|
|
424
|
+
const bytes = new Uint8Array(await readFontFile(file, signal));
|
|
425
|
+
const result = createFontSource(bytes, face(family, weight, style));
|
|
426
|
+
if ("failure" in result) throw new Error(`Invalid packaged PDF font: ${String(file)}`);
|
|
427
|
+
return result.source;
|
|
428
|
+
})
|
|
429
|
+
);
|
|
430
|
+
const supplementalFonts2 = defineFontResolver(async ({ families, signal }) => {
|
|
431
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
432
|
+
for (const family of families) {
|
|
433
|
+
wanted.add(family);
|
|
434
|
+
const target = substituteFor(family);
|
|
435
|
+
if (target) wanted.add(target);
|
|
436
|
+
}
|
|
437
|
+
const sources = await readFaces(
|
|
438
|
+
supplemental.filter(([family]) => wanted.has(family)),
|
|
439
|
+
signal
|
|
440
|
+
);
|
|
441
|
+
return {
|
|
442
|
+
sources,
|
|
443
|
+
substitutions: families.flatMap((family) => {
|
|
444
|
+
const target = substituteFor(family);
|
|
445
|
+
return target && target !== CJK_FAMILY ? substitutionsTo(family, target) : [];
|
|
446
|
+
})
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
const cjkFonts2 = defineFontResolver(async ({ families, signal }) => {
|
|
450
|
+
const planned = families.filter((family) => substituteFor(family) === CJK_FAMILY);
|
|
451
|
+
if (!families.includes(CJK_FAMILY) && planned.length === 0) return { sources: [] };
|
|
452
|
+
let sources;
|
|
453
|
+
try {
|
|
454
|
+
const file = await cjkFace();
|
|
455
|
+
if (!file) return { sources: [] };
|
|
456
|
+
sources = await readFaces([[CJK_FAMILY, file, 400, "normal"]], signal);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (signal?.aborted) throw error;
|
|
459
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
460
|
+
throw new Error(`${CJK_PACKAGE} could not supply its face: ${detail}`, { cause: error });
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
sources,
|
|
464
|
+
substitutions: planned.flatMap((family) => substitutionsTo(family, CJK_FAMILY))
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
const standInFonts2 = defineFontResolver(async ({ families, signal, resolvedFaces }) => {
|
|
468
|
+
const covered = new Set(
|
|
469
|
+
(resolvedFaces ?? []).map(
|
|
470
|
+
(face2) => `${face2.family.toLowerCase()}/${face2.weight}/${face2.style}`
|
|
471
|
+
)
|
|
472
|
+
);
|
|
473
|
+
const has = (family, weight, style) => covered.has(`${family.toLowerCase()}/${weight}/${style}`);
|
|
474
|
+
const faces = [
|
|
475
|
+
[400, "normal"],
|
|
476
|
+
[700, "normal"],
|
|
477
|
+
[400, "italic"],
|
|
478
|
+
[700, "italic"]
|
|
479
|
+
];
|
|
480
|
+
const substitutions = [];
|
|
481
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
482
|
+
for (const family of families) {
|
|
483
|
+
if (has(family, 400, "normal")) {
|
|
484
|
+
for (const [weight, style] of faces)
|
|
485
|
+
if (!has(family, weight, style))
|
|
486
|
+
substitutions.push({ from: face(family, weight, style), to: face(family) });
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const canonical = canonicalFamily(family).family;
|
|
490
|
+
if (knownFamilies.has(canonical) && !cjkWordFamilies.has(canonical)) continue;
|
|
491
|
+
const target = genericSubstituteFor(family);
|
|
492
|
+
wanted.add(target);
|
|
493
|
+
const { bold, italic } = canonicalFamily(family);
|
|
494
|
+
for (const [weight, style] of faces)
|
|
495
|
+
substitutions.push({
|
|
496
|
+
from: face(family, weight, style),
|
|
497
|
+
to: face(
|
|
498
|
+
target,
|
|
499
|
+
bold || weight === 700 ? 700 : 400,
|
|
500
|
+
italic || style === "italic" ? "italic" : "normal"
|
|
501
|
+
)
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
const sources = await readFaces(
|
|
505
|
+
supplemental.filter(([family]) => wanted.has(family)),
|
|
506
|
+
signal
|
|
507
|
+
);
|
|
508
|
+
return { sources, substitutions };
|
|
509
|
+
});
|
|
510
|
+
return {
|
|
511
|
+
supplementalFonts: supplementalFonts2,
|
|
512
|
+
cjkFonts: cjkFonts2,
|
|
513
|
+
standInFonts: standInFonts2,
|
|
514
|
+
/**
|
|
515
|
+
* Whether a face resolved through the generic stand-in rather than a metric-compatible
|
|
516
|
+
* plan: a substitution whose family this package does not answer for, or a Word CJK
|
|
517
|
+
* family in its stand-in rather than the CJK face. The writer reports these, since the
|
|
518
|
+
* page carries the text in another font's metrics.
|
|
519
|
+
*/
|
|
520
|
+
isGenericSubstitution(family, sourceFamily) {
|
|
521
|
+
if (family === sourceFamily) return false;
|
|
522
|
+
const canonical = canonicalFamily(family).family;
|
|
523
|
+
if (cjkWordFamilies.has(canonical)) return sourceFamily === genericSubstituteFor(family);
|
|
524
|
+
return !knownFamilies.has(canonical);
|
|
525
|
+
},
|
|
526
|
+
/**
|
|
527
|
+
* What a missing-glyph report adds for `text`: the package to install, when the text is
|
|
528
|
+
* CJK and the lookup found no usable package. Empty otherwise.
|
|
529
|
+
*/
|
|
530
|
+
missingGlyphHint(text) {
|
|
531
|
+
return cjkMissing && CJK_TEXT.test(text) ? `; install ${CJK_PACKAGE} for Chinese, Japanese, and Korean text` : "";
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
var {
|
|
536
|
+
supplementalFonts,
|
|
537
|
+
cjkFonts,
|
|
538
|
+
standInFonts,
|
|
539
|
+
isGenericSubstitution,
|
|
540
|
+
missingGlyphHint
|
|
541
|
+
} = packagedFontResolvers();
|
|
542
|
+
var wordFontRoots = process.platform === "darwin" ? [
|
|
543
|
+
"/System/Library/Fonts/Supplemental",
|
|
544
|
+
"/Library/Fonts",
|
|
545
|
+
join(homedir(), "Library/Fonts"),
|
|
546
|
+
"/Applications/Microsoft Word.app/Contents/Resources/DFonts"
|
|
547
|
+
] : process.platform === "win32" ? [join(process.env.WINDIR ?? "C:\\Windows", "Fonts")] : [
|
|
548
|
+
"/usr/share/fonts/truetype/msttcorefonts",
|
|
549
|
+
"/usr/share/fonts/truetype/msttcore",
|
|
550
|
+
join(homedir(), ".local/share/fonts")
|
|
551
|
+
];
|
|
552
|
+
function installedWordFontResolver(roots) {
|
|
553
|
+
return defineFontResolver(async ({ families, defaultFamily, signal }) => {
|
|
554
|
+
const names = {
|
|
555
|
+
Arial: ["Arial", "arial"],
|
|
556
|
+
"Times New Roman": ["Times New Roman", "times"],
|
|
557
|
+
"Courier New": ["Courier New", "cour"],
|
|
558
|
+
Georgia: ["Georgia", "georgia"],
|
|
559
|
+
Verdana: ["Verdana", "verdana"],
|
|
560
|
+
Calibri: ["Calibri", "calibri"],
|
|
561
|
+
"Segoe UI Symbol": ["Segoe UI Symbol", "seguisym"],
|
|
562
|
+
SimSun: ["SimSun", "simsun"],
|
|
563
|
+
Batang: ["Batang", "batang"],
|
|
564
|
+
Cambria: ["Cambria", "cambria"],
|
|
565
|
+
"Cambria Math": ["Cambria Math"],
|
|
566
|
+
"MS Gothic": ["MS Gothic", "msgothic"],
|
|
567
|
+
"MS PGothic": ["MS PGothic"],
|
|
568
|
+
"MS UI Gothic": ["MS UI Gothic"],
|
|
569
|
+
"Arial Narrow": ["Arial Narrow", "ArialNarrow"],
|
|
570
|
+
"Malgun Gothic": ["malgun"],
|
|
571
|
+
// Legacy symbol-encoded faces. A Word bullet is the font's own byte plus 0xF000
|
|
572
|
+
// (U+F0B7 in Symbol), a private-use codepoint only these faces carry — and the face
|
|
573
|
+
// also SIZES the line it sits on, because a 12 pt Symbol ascends 12.06 pt where a
|
|
574
|
+
// 12 pt text face ascends 11.52. Without them the glyph falls to the shared symbol
|
|
575
|
+
// fallback, which has neither the right outline nor the right ascent.
|
|
576
|
+
Symbol: ["Symbol", "symbol"],
|
|
577
|
+
Wingdings: ["Wingdings", "wingding"],
|
|
578
|
+
"Wingdings 2": ["Wingdings 2", "wingdng2"],
|
|
579
|
+
"Wingdings 3": ["Wingdings 3", "wingdng3"],
|
|
580
|
+
Webdings: ["Webdings", "webdings"]
|
|
581
|
+
};
|
|
582
|
+
const files = {
|
|
583
|
+
Aptos: [["Aptos.ttf"], ["Aptos-Bold.ttf"], ["Aptos-Italic.ttf"], ["Aptos-Bold-Italic.ttf"]],
|
|
584
|
+
"Aptos Narrow": [
|
|
585
|
+
["Aptos-Narrow.ttf"],
|
|
586
|
+
["Aptos-Narrow-Bold.ttf"],
|
|
587
|
+
["Aptos-Narrow-Italic.ttf"],
|
|
588
|
+
["Aptos-Narrow-Bold-Italic.ttf"]
|
|
589
|
+
],
|
|
590
|
+
Helvetica: [["Helvetica.ttc"], ["Helvetica.ttc"], ["Helvetica.ttc"], ["Helvetica.ttc"]],
|
|
591
|
+
SimHei: [["SimHei.ttf", "simhei.ttf"], [], [], []],
|
|
592
|
+
FangSong: [["Fangsong.ttf", "simfang.ttf"], [], [], []],
|
|
593
|
+
KaiTi: [["Kaiti.ttf", "simkai.ttf"], [], [], []],
|
|
594
|
+
DengXian: [["Deng.ttf"], ["Dengb.ttf"], [], []],
|
|
595
|
+
"MS Mincho": [["msmincho.ttc"], [], [], []],
|
|
596
|
+
"MS PMincho": [["msmincho.ttc"], [], [], []],
|
|
597
|
+
"Microsoft YaHei": [["msyh.ttc"], ["msyhbd.ttc"], [], []],
|
|
598
|
+
Meiryo: [["meiryo.ttc"], ["meiryob.ttc"], [], []],
|
|
599
|
+
MingLiU: [["mingliu.ttc"], [], [], []],
|
|
600
|
+
PMingLiU: [["mingliu.ttc"], [], [], []],
|
|
601
|
+
Gulim: [["gulim.ttc"], [], [], []],
|
|
602
|
+
Dotum: [["gulim.ttc"], [], [], []]
|
|
603
|
+
};
|
|
604
|
+
const sources = [];
|
|
605
|
+
for (const requested of /* @__PURE__ */ new Set([...families, ...defaultFamily ? [defaultFamily] : []])) {
|
|
606
|
+
const { family, bold, italic } = canonicalFamily(requested);
|
|
607
|
+
if (!Object.hasOwn(names, family) && !Object.hasOwn(files, family)) continue;
|
|
608
|
+
for (const [weight, style] of [
|
|
609
|
+
[400, "normal"],
|
|
610
|
+
[700, "normal"],
|
|
611
|
+
[400, "italic"],
|
|
612
|
+
[700, "italic"]
|
|
613
|
+
]) {
|
|
614
|
+
const faceBold = bold || weight === 700;
|
|
615
|
+
const faceItalic = italic || style === "italic";
|
|
616
|
+
const slot = (faceBold ? 1 : 0) + (faceItalic ? 2 : 0);
|
|
617
|
+
const [suffix, short] = [
|
|
618
|
+
["", ""],
|
|
619
|
+
[" Bold", "bd"],
|
|
620
|
+
[" Italic", "i"],
|
|
621
|
+
[" Bold Italic", "bi"]
|
|
622
|
+
][slot];
|
|
623
|
+
let found = false;
|
|
624
|
+
for (const root of roots) {
|
|
625
|
+
if (found) break;
|
|
626
|
+
const shortSuffix = ["Georgia", "Verdana", "Calibri", "Cambria"].includes(family) ? { bd: "b", bi: "z" }[short] ?? short : short;
|
|
627
|
+
const candidates = (names[family] ?? []).flatMap((name) => [
|
|
628
|
+
name + suffix + ".ttf",
|
|
629
|
+
name + shortSuffix + ".ttf"
|
|
630
|
+
]);
|
|
631
|
+
candidates.push(...files[family]?.[slot] ?? []);
|
|
632
|
+
if (slot === 0 && ["Cambria", "Cambria Math"].includes(family))
|
|
633
|
+
candidates.push("Cambria.ttc", "cambria.ttc");
|
|
634
|
+
if (slot === 0 && ["MS Gothic", "MS PGothic", "MS UI Gothic"].includes(family))
|
|
635
|
+
candidates.push("msgothic.ttc", "MSGOTHIC.TTC");
|
|
636
|
+
if (slot === 0) {
|
|
637
|
+
if (family === "SimSun") candidates.push("Simsun.ttc", "simsun.ttc");
|
|
638
|
+
if (family === "Batang") candidates.push("batang.ttc", "Batang.ttc");
|
|
639
|
+
}
|
|
640
|
+
for (const filename of new Set(candidates)) {
|
|
641
|
+
if (signal?.aborted) throw signal.reason;
|
|
642
|
+
try {
|
|
643
|
+
const bytes = new Uint8Array(await readFontFile(join(root, filename), signal));
|
|
644
|
+
let faceIndex = 0;
|
|
645
|
+
if (filename.toLowerCase().endsWith(".ttc")) {
|
|
646
|
+
const collection = openFont(Buffer.from(bytes));
|
|
647
|
+
if (!("fonts" in collection)) continue;
|
|
648
|
+
faceIndex = collection.fonts.findIndex(
|
|
649
|
+
(font) => collectionFaceMatches(font.postscriptName, family, faceBold, faceItalic)
|
|
650
|
+
);
|
|
651
|
+
if (faceIndex < 0) continue;
|
|
652
|
+
}
|
|
653
|
+
const source = createFontSource(bytes, {
|
|
654
|
+
...face(requested, weight, style),
|
|
655
|
+
faceIndex
|
|
656
|
+
});
|
|
657
|
+
if ("failure" in source) continue;
|
|
658
|
+
sources.push(source.source);
|
|
659
|
+
found = true;
|
|
660
|
+
break;
|
|
661
|
+
} catch (error) {
|
|
662
|
+
if (signal?.aborted) throw error;
|
|
663
|
+
if (error.code !== "ENOENT") throw error;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (found) break;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return { sources };
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
function collectionFaceMatches(postscriptName, family, bold, italic) {
|
|
674
|
+
if (typeof postscriptName !== "string") return false;
|
|
675
|
+
const actual = postscriptName.replace(/[- ]/g, "").replace(/Oblique$/, "Italic").toLowerCase();
|
|
676
|
+
const expected = `${family.replace(/[- ]/g, "")}${bold ? "Bold" : ""}${italic ? "Italic" : ""}`;
|
|
677
|
+
return actual === expected.toLowerCase();
|
|
678
|
+
}
|
|
679
|
+
var installedWordFonts = installedWordFontResolver(wordFontRoots);
|
|
680
|
+
async function readFontFile(path, signal) {
|
|
681
|
+
if (signal?.aborted) throw signal.reason;
|
|
682
|
+
if ((await stat(path)).size > 32 * 1024 * 1024) throw new RangeError("Local font exceeds 32 MiB");
|
|
683
|
+
let bytes;
|
|
684
|
+
try {
|
|
685
|
+
bytes = await readFile(path, signal ? { signal } : void 0);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
if (!signal || error.code !== "ERR_INVALID_ARG_TYPE") throw error;
|
|
688
|
+
bytes = await readFile(path);
|
|
689
|
+
}
|
|
690
|
+
if (signal?.aborted) throw signal.reason;
|
|
691
|
+
return bytes;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/open-session.ts
|
|
695
|
+
var bundledFonts = packagedFonts({
|
|
696
|
+
install: false,
|
|
697
|
+
fetcher: createPackagedFileFetch({
|
|
698
|
+
trustedRoot: new URL("./", FONT_ASSET_ROOT2),
|
|
699
|
+
maxBytes: HARD_MAX_FONT_BYTES
|
|
700
|
+
})
|
|
701
|
+
});
|
|
702
|
+
function openExportSession(source, options, signal) {
|
|
703
|
+
const { fonts, fallbackFonts, lastResortFonts, useSystemFonts = true, ...core } = options;
|
|
704
|
+
const origins = [
|
|
705
|
+
...fonts ? Array.isArray(fonts) ? fonts : [fonts] : [],
|
|
706
|
+
...useSystemFonts ? [installedWordFonts] : [],
|
|
707
|
+
bundledFonts,
|
|
708
|
+
...fallbackFonts ? Array.isArray(fallbackFonts) ? fallbackFonts : [fallbackFonts] : [],
|
|
709
|
+
supplementalFonts,
|
|
710
|
+
cjkFonts
|
|
711
|
+
];
|
|
712
|
+
return openFontBackedDocumentForExport(source, {
|
|
713
|
+
...core,
|
|
714
|
+
documentLigatures: options.documentLigatures ?? true,
|
|
715
|
+
signal,
|
|
716
|
+
displayMode: options.displayMode ?? "proposed",
|
|
717
|
+
reuseAcrossRevisions: false,
|
|
718
|
+
// Every packaged fallback face is on offer. They are read lazily, only when a family the
|
|
719
|
+
// document uses needs one, so offering the whole list costs a plain Latin document nothing.
|
|
720
|
+
glyphFallbacks: options.glyphFallbacks ?? PDF_GLYPH_FALLBACKS,
|
|
721
|
+
fonts: origins,
|
|
722
|
+
// After the document's own embedded fonts: a stand-in for what nothing else covers.
|
|
723
|
+
lastResortFonts: [
|
|
724
|
+
...lastResortFonts ? Array.isArray(lastResortFonts) ? lastResortFonts : [lastResortFonts] : [],
|
|
725
|
+
standInFonts
|
|
726
|
+
]
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/font-diagnostics.ts
|
|
731
|
+
function reportFontDiagnostics(resolution, work) {
|
|
732
|
+
for (const failure of resolution.originFailures) {
|
|
733
|
+
let detail = "unknown error";
|
|
734
|
+
try {
|
|
735
|
+
if (failure.cause instanceof Error && typeof failure.cause.message === "string")
|
|
736
|
+
detail = failure.cause.message.slice(0, 1024);
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
work.report("font-origin-failed", `A font source failed: ${detail}`, void 0, "information", {
|
|
740
|
+
originIndex: failure.originIndex,
|
|
741
|
+
...failure.originName === void 0 ? {} : { originName: failure.originName }
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
for (const family of resolution.families) {
|
|
745
|
+
if (family.coverage !== "complete" || family.faces.some(
|
|
746
|
+
(face2) => face2.via === "substitution" && face2.sourceFamily.toLowerCase() === family.family.toLowerCase()
|
|
747
|
+
))
|
|
748
|
+
work.report(
|
|
749
|
+
"incomplete-font",
|
|
750
|
+
family.coverage !== "complete" ? `Font face coverage is incomplete for ${family.family}; inspect fontResolution` : `Font variants use substitute faces from ${family.family}; inspect fontResolution`,
|
|
751
|
+
void 0,
|
|
752
|
+
"information"
|
|
753
|
+
);
|
|
754
|
+
const stand = family.faces.find(
|
|
755
|
+
(face2) => face2.via === "substitution" && isGenericSubstitution(family.family, face2.sourceFamily)
|
|
756
|
+
);
|
|
757
|
+
if (stand)
|
|
758
|
+
work.report(
|
|
759
|
+
"font-substitution",
|
|
760
|
+
`${family.family} is not available; best-effort export renders it in ${stand.sourceFamily}`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/paint.ts
|
|
766
|
+
import {
|
|
767
|
+
forEachSemanticSpan,
|
|
768
|
+
forEachSemanticStory,
|
|
769
|
+
forEachSemanticDrawing,
|
|
770
|
+
runBorderStrokesForLine
|
|
771
|
+
} from "@docx-editor.dev/core/layout";
|
|
772
|
+
|
|
773
|
+
// src/text.ts
|
|
774
|
+
import { PDFName as PDFName2 } from "pdf-lib";
|
|
775
|
+
import {
|
|
776
|
+
baselineShiftPtOf,
|
|
777
|
+
noteSeparatorRuleBox,
|
|
778
|
+
glyphSizeFactorOf,
|
|
779
|
+
styleForFontSlot as styleForFontSlot2,
|
|
780
|
+
TAB_LEADER_GLYPH,
|
|
781
|
+
tabLeaderPattern
|
|
782
|
+
} from "@docx-editor.dev/core/layout";
|
|
783
|
+
|
|
784
|
+
// src/underline-gap.ts
|
|
785
|
+
import { styleForFontSlot } from "@docx-editor.dev/core/layout";
|
|
786
|
+
function underlineGap(visit) {
|
|
787
|
+
const { line, span } = visit;
|
|
788
|
+
const index = line.spans.indexOf(span);
|
|
789
|
+
if (index < 0 || (line.drawings?.length ?? 0) > 0) return 0;
|
|
790
|
+
const next = line.spans[index + 1];
|
|
791
|
+
if (!next || next.style.hidden || next.equation || next.text.includes(" ")) return 0;
|
|
792
|
+
if (!span.text.endsWith(" ") || /\s/u.test(span.text.slice(0, -1))) return 0;
|
|
793
|
+
if ((next.wrapAdvanceBefore ?? 0) > 1e-3 || span.link !== next.link) return 0;
|
|
794
|
+
if (span.range.paragraphId !== next.range.paragraphId) return 0;
|
|
795
|
+
if (span.revisions?.length || next.revisions?.length) return 0;
|
|
796
|
+
const left = styleForFontSlot(span.style, span.fontSlot);
|
|
797
|
+
const right = styleForFontSlot(next.style, next.fontSlot);
|
|
798
|
+
if (left.underline?.variant !== "single" || right.underline?.variant !== "single") return 0;
|
|
799
|
+
if (left.horizontalScalePercent !== 100 || right.horizontalScalePercent !== 100) return 0;
|
|
800
|
+
if (left.shaping?.baseLevel === 1 || right.shaping?.baseLevel === 1) return 0;
|
|
801
|
+
if (left.fontFamily !== right.fontFamily || left.fontSizePt !== right.fontSizePt || left.bold !== right.bold || left.italic !== right.italic || left.verticalAlign !== right.verticalAlign || left.baselineShiftPt !== right.baselineShiftPt || (left.underline.color ?? left.color) !== (right.underline.color ?? right.color))
|
|
802
|
+
return 0;
|
|
803
|
+
const gap = next.box.x - (span.box.x + span.box.width);
|
|
804
|
+
return gap > 0.25 ? gap : 0;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/paragraph-grid-origin.ts
|
|
808
|
+
var PDF_PAINT_GRID_PT = 0.24;
|
|
809
|
+
function paragraphGridOffsetX(visit) {
|
|
810
|
+
const alignment = visit.paragraph.alignment;
|
|
811
|
+
const origin = alignment === "left" || alignment === "both" ? visit.storyOrigin.x + visit.line.contentX : visit.storyOrigin.x + visit.paragraph.box.x;
|
|
812
|
+
return Math.round(origin / PDF_PAINT_GRID_PT) * PDF_PAINT_GRID_PT - origin;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/fonts.ts
|
|
816
|
+
import { create as openFont2 } from "fontkit";
|
|
817
|
+
import { PDFName, PDFString } from "pdf-lib";
|
|
818
|
+
import { mapSymbolPuaText } from "@docx-editor.dev/core/layout";
|
|
819
|
+
|
|
820
|
+
// src/pdf-font-embedding.ts
|
|
821
|
+
import { create as createFontkitFont } from "fontkit";
|
|
822
|
+
var MAX_SFNT_TABLES = 4096;
|
|
823
|
+
var MAX_TTC_FACES = 256;
|
|
824
|
+
var BITMAP_ONLY_EMBEDDING = 512;
|
|
825
|
+
var TTC_VERSION_1 = 65536;
|
|
826
|
+
var TTC_VERSION_2 = 131072;
|
|
827
|
+
function readUint16(bytes, offset) {
|
|
828
|
+
if (offset < 0 || offset > bytes.byteLength - 2) return null;
|
|
829
|
+
return bytes[offset] << 8 | bytes[offset + 1];
|
|
830
|
+
}
|
|
831
|
+
function readUint32(bytes, offset) {
|
|
832
|
+
if (offset < 0 || offset > bytes.byteLength - 4) return null;
|
|
833
|
+
return bytes[offset] * 16777216 + (bytes[offset + 1] << 16) + (bytes[offset + 2] << 8) + bytes[offset + 3] >>> 0;
|
|
834
|
+
}
|
|
835
|
+
function tagAt(bytes, offset) {
|
|
836
|
+
if (offset < 0 || offset > bytes.byteLength - 4) return null;
|
|
837
|
+
return String.fromCharCode(
|
|
838
|
+
bytes[offset],
|
|
839
|
+
bytes[offset + 1],
|
|
840
|
+
bytes[offset + 2],
|
|
841
|
+
bytes[offset + 3]
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
function isFontCollection(bytes) {
|
|
845
|
+
return tagAt(bytes, 0) === "ttcf";
|
|
846
|
+
}
|
|
847
|
+
function refuse(reason) {
|
|
848
|
+
return { kind: "refuse", reason };
|
|
849
|
+
}
|
|
850
|
+
function embed(collectionSelector) {
|
|
851
|
+
return { kind: "embed", collectionSelector };
|
|
852
|
+
}
|
|
853
|
+
function isFontkitFont(value) {
|
|
854
|
+
return value !== null && typeof value.hasGlyphForCodePoint === "function";
|
|
855
|
+
}
|
|
856
|
+
function isFontkitCollection(value) {
|
|
857
|
+
return typeof value.getFont === "function";
|
|
858
|
+
}
|
|
859
|
+
function postscriptSelector(face2) {
|
|
860
|
+
const name = face2.postscriptName;
|
|
861
|
+
if (typeof name !== "string") return null;
|
|
862
|
+
const trimmed = name.trim();
|
|
863
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
864
|
+
}
|
|
865
|
+
function sfntEmbeddingPermission(bytes, base) {
|
|
866
|
+
const tableCount = readUint16(bytes, base + 4);
|
|
867
|
+
if (tableCount === null || tableCount > MAX_SFNT_TABLES) {
|
|
868
|
+
return refuse("The admitted font has no bounded SFNT table directory");
|
|
869
|
+
}
|
|
870
|
+
const directoryEnd = base + 12 + tableCount * 16;
|
|
871
|
+
if (!Number.isSafeInteger(directoryEnd) || directoryEnd > bytes.byteLength) {
|
|
872
|
+
return refuse("The admitted font has a truncated SFNT table directory");
|
|
873
|
+
}
|
|
874
|
+
for (let index = 0; index < tableCount; index += 1) {
|
|
875
|
+
const record = base + 12 + index * 16;
|
|
876
|
+
const tag = tagAt(bytes, record);
|
|
877
|
+
if (tag !== "OS/2") continue;
|
|
878
|
+
const offset = readUint32(bytes, record + 8);
|
|
879
|
+
const length = readUint32(bytes, record + 12);
|
|
880
|
+
if (offset === null || length === null || offset > bytes.byteLength || length > bytes.byteLength - offset || length < 10) {
|
|
881
|
+
return refuse("The admitted font has an invalid OS/2 table range");
|
|
882
|
+
}
|
|
883
|
+
const fsType = readUint16(bytes, offset + 8);
|
|
884
|
+
if (fsType === null) {
|
|
885
|
+
return refuse("The admitted font has a truncated OS/2 fsType value");
|
|
886
|
+
}
|
|
887
|
+
if ((fsType & 2) !== 0) {
|
|
888
|
+
return refuse("The OS/2 fsType forbids font embedding");
|
|
889
|
+
}
|
|
890
|
+
if ((fsType & 256) !== 0) {
|
|
891
|
+
return refuse(
|
|
892
|
+
"The OS/2 fsType forbids subsetting, and this writer has no safe full-font embedding mode"
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if ((fsType & BITMAP_ONLY_EMBEDDING) !== 0) {
|
|
896
|
+
return refuse(
|
|
897
|
+
"The OS/2 fsType permits bitmap embedding only, and this writer embeds outline data"
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
return embed(null);
|
|
901
|
+
}
|
|
902
|
+
return embed(null);
|
|
903
|
+
}
|
|
904
|
+
function collectionFaceOffset(bytes, faceIndex) {
|
|
905
|
+
if (bytes.byteLength < 12) {
|
|
906
|
+
return { reason: "The admitted font collection header is truncated" };
|
|
907
|
+
}
|
|
908
|
+
const version = readUint32(bytes, 4);
|
|
909
|
+
if (version !== TTC_VERSION_1 && version !== TTC_VERSION_2) {
|
|
910
|
+
return { reason: "The admitted font collection has an unsupported TTC version" };
|
|
911
|
+
}
|
|
912
|
+
const faceCount = readUint32(bytes, 8);
|
|
913
|
+
if (faceCount === null) {
|
|
914
|
+
return { reason: "The admitted font collection header is truncated" };
|
|
915
|
+
}
|
|
916
|
+
if (faceCount === 0) {
|
|
917
|
+
return { reason: "The admitted font collection has no faces" };
|
|
918
|
+
}
|
|
919
|
+
if (faceCount > MAX_TTC_FACES) {
|
|
920
|
+
return { reason: `The admitted font collection exceeds the ${MAX_TTC_FACES} face limit` };
|
|
921
|
+
}
|
|
922
|
+
const offsetsEnd = 12 + faceCount * 4;
|
|
923
|
+
if (!Number.isSafeInteger(offsetsEnd) || offsetsEnd > bytes.byteLength) {
|
|
924
|
+
return { reason: "The admitted font collection face directory is truncated" };
|
|
925
|
+
}
|
|
926
|
+
if (!Number.isSafeInteger(faceIndex) || faceIndex < 0) {
|
|
927
|
+
return { reason: "The admitted font collection faceIndex is invalid" };
|
|
928
|
+
}
|
|
929
|
+
if (faceIndex >= faceCount) {
|
|
930
|
+
return {
|
|
931
|
+
reason: `The admitted font collection faceIndex ${faceIndex} is out of range for ${faceCount} faces`
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const faceOffset = readUint32(bytes, 12 + faceIndex * 4);
|
|
935
|
+
if (faceOffset === null || faceOffset < offsetsEnd || faceOffset > bytes.byteLength - 12) {
|
|
936
|
+
return { reason: "The admitted font collection face offset is invalid" };
|
|
937
|
+
}
|
|
938
|
+
return { offset: faceOffset };
|
|
939
|
+
}
|
|
940
|
+
function collectionFaceSelector(bytes, faceIndex) {
|
|
941
|
+
let opened;
|
|
942
|
+
try {
|
|
943
|
+
opened = createFontkitFont(Buffer.from(bytes));
|
|
944
|
+
} catch {
|
|
945
|
+
return { reason: "The admitted font collection could not be opened" };
|
|
946
|
+
}
|
|
947
|
+
if (!isFontkitCollection(opened)) {
|
|
948
|
+
return { reason: "The admitted font collection did not parse as a font collection" };
|
|
949
|
+
}
|
|
950
|
+
const fonts = opened.fonts;
|
|
951
|
+
if (faceIndex >= fonts.length) {
|
|
952
|
+
return {
|
|
953
|
+
reason: `The admitted font collection faceIndex ${faceIndex} is out of range for ${fonts.length} faces`
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
const face2 = fonts[faceIndex];
|
|
957
|
+
if (!isFontkitFont(face2)) {
|
|
958
|
+
return { reason: "The selected collection face is not a font face" };
|
|
959
|
+
}
|
|
960
|
+
const selector = postscriptSelector(face2);
|
|
961
|
+
if (selector === null) {
|
|
962
|
+
return { reason: "The selected collection face has no PostScript name" };
|
|
963
|
+
}
|
|
964
|
+
for (let index = 0; index < fonts.length; index += 1) {
|
|
965
|
+
if (index === faceIndex) continue;
|
|
966
|
+
if (postscriptSelector(fonts[index]) === selector) {
|
|
967
|
+
return { reason: "The selected collection face PostScript name is not unique" };
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
let selected;
|
|
971
|
+
try {
|
|
972
|
+
selected = createFontkitFont(Buffer.from(bytes), selector);
|
|
973
|
+
} catch {
|
|
974
|
+
return { reason: "The selected collection face PostScript name did not resolve" };
|
|
975
|
+
}
|
|
976
|
+
if (!isFontkitFont(selected)) {
|
|
977
|
+
return { reason: "The selected collection face PostScript name did not resolve" };
|
|
978
|
+
}
|
|
979
|
+
return { selector };
|
|
980
|
+
}
|
|
981
|
+
function fontEmbeddingDecision(font) {
|
|
982
|
+
const { bytes, faceIndex } = font;
|
|
983
|
+
if (!isFontCollection(bytes)) {
|
|
984
|
+
if (faceIndex !== 0) {
|
|
985
|
+
return refuse("A nonzero faceIndex requires a TrueType collection resource");
|
|
986
|
+
}
|
|
987
|
+
return sfntEmbeddingPermission(bytes, 0);
|
|
988
|
+
}
|
|
989
|
+
const located = collectionFaceOffset(bytes, faceIndex);
|
|
990
|
+
if ("reason" in located) return refuse(located.reason);
|
|
991
|
+
const permission = sfntEmbeddingPermission(bytes, located.offset);
|
|
992
|
+
if (permission.kind === "refuse") return permission;
|
|
993
|
+
const selected = collectionFaceSelector(bytes, faceIndex);
|
|
994
|
+
if ("reason" in selected) return refuse(selected.reason);
|
|
995
|
+
return embed(selected.selector);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/font-metrics.ts
|
|
999
|
+
function strikeMetrics(face2) {
|
|
1000
|
+
const bytes = face2.bytes, view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1001
|
+
let base = 0;
|
|
1002
|
+
if (bytes.length < 12) return null;
|
|
1003
|
+
if (view.getUint32(0) === 1953784678) {
|
|
1004
|
+
if (12 + face2.faceIndex * 4 + 4 > bytes.length) return null;
|
|
1005
|
+
base = view.getUint32(12 + face2.faceIndex * 4);
|
|
1006
|
+
}
|
|
1007
|
+
if (base + 12 > bytes.length) return null;
|
|
1008
|
+
const count = view.getUint16(base + 4);
|
|
1009
|
+
if (count > 4096 || base + 12 + count * 16 > bytes.length) return null;
|
|
1010
|
+
for (let i = 0; i < count; i++) {
|
|
1011
|
+
const entry = base + 12 + i * 16;
|
|
1012
|
+
if (view.getUint32(entry) !== 1330851634) continue;
|
|
1013
|
+
const offset = view.getUint32(entry + 8), length = view.getUint32(entry + 12);
|
|
1014
|
+
if (length < 30 || offset + length > bytes.length) return null;
|
|
1015
|
+
return { thickness: view.getInt16(offset + 26), position: view.getInt16(offset + 28) };
|
|
1016
|
+
}
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/fonts.ts
|
|
1021
|
+
function cmap(body, type) {
|
|
1022
|
+
return `/CIDInit /ProcSet findresource begin
|
|
1023
|
+
12 dict begin
|
|
1024
|
+
begincmap
|
|
1025
|
+
/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> def
|
|
1026
|
+
/CMapName /Docx${type} def
|
|
1027
|
+
/CMapType ${type} def
|
|
1028
|
+
1 begincodespacerange
|
|
1029
|
+
<0000> <ffff>
|
|
1030
|
+
endcodespacerange
|
|
1031
|
+
${body}
|
|
1032
|
+
endcmap
|
|
1033
|
+
CMapName currentdict /CMap defineresource pop
|
|
1034
|
+
end
|
|
1035
|
+
end`;
|
|
1036
|
+
}
|
|
1037
|
+
function groups(rows, operator) {
|
|
1038
|
+
const out = [];
|
|
1039
|
+
for (let i = 0; i < rows.length; i += 100) {
|
|
1040
|
+
const slice = rows.slice(i, i + 100);
|
|
1041
|
+
out.push(`${slice.length} begin${operator}
|
|
1042
|
+
${slice.join("\n")}
|
|
1043
|
+
end${operator}`);
|
|
1044
|
+
}
|
|
1045
|
+
return out.join("\n");
|
|
1046
|
+
}
|
|
1047
|
+
var EmbeddedFace = class {
|
|
1048
|
+
constructor(doc, admitted, index) {
|
|
1049
|
+
this.doc = doc;
|
|
1050
|
+
const gate = fontEmbeddingDecision(admitted);
|
|
1051
|
+
if (gate.kind === "refuse") throw new Error(gate.reason);
|
|
1052
|
+
const font = gate.collectionSelector ? openFont2(Buffer.from(admitted.bytes), gate.collectionSelector) : openFont2(Buffer.from(admitted.bytes));
|
|
1053
|
+
if (!font || !("createSubset" in font))
|
|
1054
|
+
throw new Error("The selected resource is not a font face");
|
|
1055
|
+
if (Object.keys(font.variationAxes ?? {}).length)
|
|
1056
|
+
throw new Error("Variable fonts require an exact static instance");
|
|
1057
|
+
this.font = font;
|
|
1058
|
+
this.colorLayers = Boolean(font.COLR && font.CPAL);
|
|
1059
|
+
const tables = font;
|
|
1060
|
+
const os2 = tables["OS/2"];
|
|
1061
|
+
const macStyle = tables.head?.macStyle;
|
|
1062
|
+
this.bold = (os2?.usWeightClass ?? 400) >= 600 || Boolean(os2?.fsSelection?.bold || macStyle?.bold);
|
|
1063
|
+
this.slanted = Boolean(os2?.fsSelection?.italic || os2?.fsSelection?.oblique || macStyle?.italic) || Number.isFinite(font.italicAngle) && font.italicAngle !== 0;
|
|
1064
|
+
this.family = admitted.request.family;
|
|
1065
|
+
this.strike = strikeMetrics(admitted);
|
|
1066
|
+
this.subset = font.createSubset();
|
|
1067
|
+
this.trueType = Boolean(
|
|
1068
|
+
font.directory?.tables?.glyf
|
|
1069
|
+
);
|
|
1070
|
+
this.ref = doc.context.nextRef();
|
|
1071
|
+
this.name = `F${index}`;
|
|
1072
|
+
}
|
|
1073
|
+
doc;
|
|
1074
|
+
ref;
|
|
1075
|
+
name;
|
|
1076
|
+
font;
|
|
1077
|
+
strike;
|
|
1078
|
+
/**
|
|
1079
|
+
* A COLR/CPAL face. Its glyphs are painted as filled palette layers by the text writer and
|
|
1080
|
+
* never written as text, so the face is not embedded: the subsetter cannot encode a color
|
|
1081
|
+
* glyph, and the page carries the text in its line's `ActualText` instead.
|
|
1082
|
+
*/
|
|
1083
|
+
colorLayers;
|
|
1084
|
+
/**
|
|
1085
|
+
* Whether the face's own outlines are bold or slanted. A bold or italic run drawn in a
|
|
1086
|
+
* face that is not (a single-weight face, or a substituted regular) gets Word's synthetic
|
|
1087
|
+
* style at paint; a real bold or italic face draws as it is.
|
|
1088
|
+
*/
|
|
1089
|
+
bold;
|
|
1090
|
+
slanted;
|
|
1091
|
+
/** The family this face was admitted under, for legacy symbol-encoding extraction. */
|
|
1092
|
+
family;
|
|
1093
|
+
subset;
|
|
1094
|
+
/** A `glyf` face, whose subset has a `loca` table; see {@link encodeSubset}. */
|
|
1095
|
+
trueType;
|
|
1096
|
+
codes = /* @__PURE__ */ new Map();
|
|
1097
|
+
rows = [];
|
|
1098
|
+
widths = /* @__PURE__ */ new Map();
|
|
1099
|
+
/**
|
|
1100
|
+
* One drawn glyph, and the text it should EXTRACT as.
|
|
1101
|
+
*
|
|
1102
|
+
* A legacy symbol face draws Word's own private-use codepoint (U+F0B7 is Symbol's bullet),
|
|
1103
|
+
* which is the right glyph and the wrong character: copied out of the page it is a private
|
|
1104
|
+
* character that means nothing outside that font. `ToUnicode` therefore carries the Unicode
|
|
1105
|
+
* twin, exactly as Word's own PDF export does. Drawing and extraction stay separate — the
|
|
1106
|
+
* glyph id is untouched — and a codepoint with no exact twin keeps what the file had rather
|
|
1107
|
+
* than gaining an approximate character.
|
|
1108
|
+
*/
|
|
1109
|
+
encode(glyph, text) {
|
|
1110
|
+
if (!Number.isInteger(glyph) || glyph <= 0 || glyph >= this.font.numGlyphs)
|
|
1111
|
+
throw new Error("Core produced a missing or invalid glyph");
|
|
1112
|
+
text = mapSymbolPuaText(text, this.family);
|
|
1113
|
+
const key = `${glyph}:${text}`;
|
|
1114
|
+
let code = this.codes.get(key);
|
|
1115
|
+
if (code === void 0) {
|
|
1116
|
+
if (this.rows.length >= 65534) throw new Error("Font character code limit exceeded");
|
|
1117
|
+
const cid = this.cidFor(glyph, text);
|
|
1118
|
+
if (!Number.isInteger(cid) || cid < 0 || cid > 65535)
|
|
1119
|
+
throw new Error("Invalid subset glyph identifier");
|
|
1120
|
+
code = this.rows.length + 1;
|
|
1121
|
+
this.rows.push({ cid, text });
|
|
1122
|
+
this.codes.set(key, code);
|
|
1123
|
+
this.widths.set(cid, this.font.getGlyph(glyph).advanceWidth * 1e3 / this.font.unitsPerEm);
|
|
1124
|
+
}
|
|
1125
|
+
return hex(code);
|
|
1126
|
+
}
|
|
1127
|
+
/** The text each subset glyph first extracted as; see {@link cidFor}. */
|
|
1128
|
+
cidText = /* @__PURE__ */ new Map();
|
|
1129
|
+
/**
|
|
1130
|
+
* The subset glyph for one (glyph, text) pair.
|
|
1131
|
+
*
|
|
1132
|
+
* An Arabic face draws BEH, TEH, NOON and YEH on one dotless base glyph and adds the dots
|
|
1133
|
+
* as separate glyphs, so one glyph extracts as several letters. Codes already differ per
|
|
1134
|
+
* letter, but MuPDF resolves `ToUnicode` per CID, so one shared CID read every such letter
|
|
1135
|
+
* as the last one mapped. The subset takes the glyph again under a new CID, the way
|
|
1136
|
+
* LibreOffice's writer does. Both fontkit subsets write one entry per listed glyph: a
|
|
1137
|
+
* TrueType subset one `glyf` record, a CFF subset one charstring under a single charset
|
|
1138
|
+
* range, so the CID is the subset index either way.
|
|
1139
|
+
*
|
|
1140
|
+
* Bounded: no copy once the copies and every glyph composites could still pull in could
|
|
1141
|
+
* reach 65535, where the glyph count and component ids wrap. A TrueType subset with copies
|
|
1142
|
+
* is written with long `loca` offsets (see `encodeSubset`).
|
|
1143
|
+
*/
|
|
1144
|
+
cidFor(glyph, text) {
|
|
1145
|
+
const known = this.subset.mapping[glyph];
|
|
1146
|
+
if (known === void 0) {
|
|
1147
|
+
const cid2 = this.subset.includeGlyph(glyph);
|
|
1148
|
+
this.cidText.set(cid2, text);
|
|
1149
|
+
return cid2;
|
|
1150
|
+
}
|
|
1151
|
+
const glyphs = this.subset.glyphs;
|
|
1152
|
+
if (this.cidText.get(known) === text || !Array.isArray(glyphs)) return known;
|
|
1153
|
+
if (glyphs.length + this.font.numGlyphs >= 65535) return known;
|
|
1154
|
+
glyphs.push(glyph);
|
|
1155
|
+
this.copies += 1;
|
|
1156
|
+
const cid = glyphs.length - 1;
|
|
1157
|
+
this.cidText.set(cid, text);
|
|
1158
|
+
return cid;
|
|
1159
|
+
}
|
|
1160
|
+
/** Glyphs the subset lists more than once; see {@link cidFor}. */
|
|
1161
|
+
copies = 0;
|
|
1162
|
+
/**
|
|
1163
|
+
* Encode the subset. A TrueType face with short `loca` offsets can address 131070 bytes of
|
|
1164
|
+
* `glyf`, and fontkit copies the face's format into the subset; copies can pass that, and
|
|
1165
|
+
* the offsets then wrap and later glyphs draw other glyphs' outlines. fontkit reads the
|
|
1166
|
+
* format from the face's parsed `loca` (whose offsets are already decoded, so the format
|
|
1167
|
+
* no longer affects reads), so it says long while the subset encodes.
|
|
1168
|
+
*/
|
|
1169
|
+
encodeSubset() {
|
|
1170
|
+
const loca = this.font.loca;
|
|
1171
|
+
if (!this.trueType || this.copies === 0 || !loca || loca.version !== 0) {
|
|
1172
|
+
return this.subset.encode();
|
|
1173
|
+
}
|
|
1174
|
+
loca.version = 1;
|
|
1175
|
+
try {
|
|
1176
|
+
return this.subset.encode();
|
|
1177
|
+
} finally {
|
|
1178
|
+
loca.version = 0;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* A code that draws this face's space and extracts as `text`: the carrier for characters
|
|
1183
|
+
* painted some other way, such as the palette layers of a color emoji. Drawn invisibly, it
|
|
1184
|
+
* gives a reader that ignores `ActualText` the characters all the same. Null when the face
|
|
1185
|
+
* has no space glyph to lend.
|
|
1186
|
+
*/
|
|
1187
|
+
carrier(text) {
|
|
1188
|
+
const space = this.font.glyphForCodePoint(32)?.id;
|
|
1189
|
+
if (!Number.isInteger(space) || space <= 0) return null;
|
|
1190
|
+
return this.encode(space, text);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* The advance this PDF declares for a character code, in 1/1000 em.
|
|
1194
|
+
*
|
|
1195
|
+
* What a viewer moves the pen by after drawing the glyph, so it is what a `TJ` adjustment
|
|
1196
|
+
* has to be measured against. Returns 0 for a code that was never encoded, which cannot
|
|
1197
|
+
* happen on the paint path because `encode` always runs first.
|
|
1198
|
+
*/
|
|
1199
|
+
declaredWidth(code) {
|
|
1200
|
+
const row = this.rows[parseInt(code, 16) - 1];
|
|
1201
|
+
const width = row === void 0 ? 0 : this.widths.get(row.cid) ?? 0;
|
|
1202
|
+
return Math.round(width * 1e5) / 1e5;
|
|
1203
|
+
}
|
|
1204
|
+
async finish(work) {
|
|
1205
|
+
if (this.colorLayers) return;
|
|
1206
|
+
await work.yield();
|
|
1207
|
+
const bytes = this.encodeSubset();
|
|
1208
|
+
work.check();
|
|
1209
|
+
const cff = bytes[0] === 1 && bytes[1] === 0;
|
|
1210
|
+
const ctx = this.doc.context;
|
|
1211
|
+
const prefix = Number(this.name.slice(1)).toString(26).padStart(6, "0").split("").map((c) => String.fromCharCode(65 + parseInt(c, 26))).join("");
|
|
1212
|
+
const base = PDFName.of(`${prefix}+DocxFont`);
|
|
1213
|
+
const file = ctx.register(
|
|
1214
|
+
flateStream(ctx, bytes, cff ? { Subtype: "CIDFontType0C" } : { Length1: bytes.length })
|
|
1215
|
+
);
|
|
1216
|
+
const factor = 1e3 / this.font.unitsPerEm;
|
|
1217
|
+
const bbox = this.font.bbox;
|
|
1218
|
+
const descriptor = ctx.register(
|
|
1219
|
+
ctx.obj({
|
|
1220
|
+
Type: "FontDescriptor",
|
|
1221
|
+
FontName: base,
|
|
1222
|
+
Flags: 4,
|
|
1223
|
+
FontBBox: [bbox.minX, bbox.minY, bbox.maxX, bbox.maxY].map((v) => v * factor),
|
|
1224
|
+
ItalicAngle: this.font.italicAngle || 0,
|
|
1225
|
+
Ascent: this.font.ascent * factor,
|
|
1226
|
+
Descent: this.font.descent * factor,
|
|
1227
|
+
CapHeight: (this.font.capHeight || this.font.ascent) * factor,
|
|
1228
|
+
StemV: 80,
|
|
1229
|
+
[cff ? "FontFile3" : "FontFile2"]: file
|
|
1230
|
+
})
|
|
1231
|
+
);
|
|
1232
|
+
const widths = [];
|
|
1233
|
+
for (const [cid, width] of [...this.widths].sort((a, b) => a[0] - b[0]))
|
|
1234
|
+
widths.push(cid, [width]);
|
|
1235
|
+
const descendant = ctx.register(
|
|
1236
|
+
ctx.obj({
|
|
1237
|
+
Type: "Font",
|
|
1238
|
+
Subtype: cff ? "CIDFontType0" : "CIDFontType2",
|
|
1239
|
+
BaseFont: base,
|
|
1240
|
+
// Registry and Ordering are PDF byte strings, `(Adobe)` and `(Identity)`, not text
|
|
1241
|
+
// strings. `PDFHexString.fromText` writes UTF-16BE behind a BOM, and a consumer that
|
|
1242
|
+
// compares these as bytes, which Acrobat and PDF/A checkers do, cannot match the
|
|
1243
|
+
// CIDFont to its CMap and drops it.
|
|
1244
|
+
CIDSystemInfo: {
|
|
1245
|
+
Registry: PDFString.of("Adobe"),
|
|
1246
|
+
Ordering: PDFString.of("Identity"),
|
|
1247
|
+
Supplement: 0
|
|
1248
|
+
},
|
|
1249
|
+
FontDescriptor: descriptor,
|
|
1250
|
+
DW: 0,
|
|
1251
|
+
W: widths,
|
|
1252
|
+
...cff ? {} : { CIDToGIDMap: "Identity" }
|
|
1253
|
+
})
|
|
1254
|
+
);
|
|
1255
|
+
const encoding = ctx.register(
|
|
1256
|
+
flateStream(
|
|
1257
|
+
ctx,
|
|
1258
|
+
cmap(
|
|
1259
|
+
groups(
|
|
1260
|
+
this.rows.map((row, i) => `<${hex(i + 1)}> ${row.cid}`),
|
|
1261
|
+
"cidchar"
|
|
1262
|
+
),
|
|
1263
|
+
1
|
|
1264
|
+
)
|
|
1265
|
+
)
|
|
1266
|
+
);
|
|
1267
|
+
const mappings = this.rows.flatMap(
|
|
1268
|
+
(row, i) => row.text ? [`<${hex(i + 1)}> <${unicodeHex(row.text)}>`] : []
|
|
1269
|
+
);
|
|
1270
|
+
const unicode = ctx.register(flateStream(ctx, cmap(groups(mappings, "bfchar"), 2)));
|
|
1271
|
+
ctx.assign(
|
|
1272
|
+
this.ref,
|
|
1273
|
+
ctx.obj({
|
|
1274
|
+
Type: "Font",
|
|
1275
|
+
Subtype: "Type0",
|
|
1276
|
+
BaseFont: base,
|
|
1277
|
+
Encoding: encoding,
|
|
1278
|
+
DescendantFonts: [descendant],
|
|
1279
|
+
ToUnicode: unicode
|
|
1280
|
+
})
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
// src/color-glyphs.ts
|
|
1286
|
+
import { ExportResourceError as ExportResourceError2 } from "@docx-editor.dev/core/export";
|
|
1287
|
+
var MAX_LAYERS = 256;
|
|
1288
|
+
var MAX_COMMANDS = 8192;
|
|
1289
|
+
var MAX_COMPOSITE_DEPTH = 8;
|
|
1290
|
+
var MAX_EXPANDED_POINTS = MAX_COMMANDS;
|
|
1291
|
+
var pathSafety = /* @__PURE__ */ new WeakMap();
|
|
1292
|
+
function pathIsSafe(font, glyph) {
|
|
1293
|
+
const cached = pathSafety.get(glyph);
|
|
1294
|
+
if (cached !== void 0) return cached;
|
|
1295
|
+
let safe;
|
|
1296
|
+
try {
|
|
1297
|
+
const { points, height } = expanded(font, glyph, 0, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
|
|
1298
|
+
safe = points <= MAX_EXPANDED_POINTS && height <= MAX_COMPOSITE_DEPTH;
|
|
1299
|
+
} catch {
|
|
1300
|
+
safe = false;
|
|
1301
|
+
}
|
|
1302
|
+
pathSafety.set(glyph, safe);
|
|
1303
|
+
return safe;
|
|
1304
|
+
}
|
|
1305
|
+
function expanded(font, glyph, depth, memo, open) {
|
|
1306
|
+
const cached = memo.get(glyph);
|
|
1307
|
+
if (cached) return cached;
|
|
1308
|
+
if (depth > MAX_COMPOSITE_DEPTH || open.has(glyph)) return { points: Infinity, height: Infinity };
|
|
1309
|
+
const decode = glyph._decode;
|
|
1310
|
+
if (typeof decode !== "function") return { points: 0, height: 0 };
|
|
1311
|
+
const decoded = decode.call(glyph);
|
|
1312
|
+
let points = (decoded?.points?.length ?? 0) + 1;
|
|
1313
|
+
let height = 0;
|
|
1314
|
+
open.add(glyph);
|
|
1315
|
+
for (const component of decoded?.components ?? []) {
|
|
1316
|
+
const child = expanded(font, font.getGlyph(component.glyphID), depth + 1, memo, open);
|
|
1317
|
+
points += child.points;
|
|
1318
|
+
height = Math.max(height, child.height + 1);
|
|
1319
|
+
if (points > MAX_EXPANDED_POINTS || height > MAX_COMPOSITE_DEPTH) break;
|
|
1320
|
+
}
|
|
1321
|
+
open.delete(glyph);
|
|
1322
|
+
const result = { points, height };
|
|
1323
|
+
memo.set(glyph, result);
|
|
1324
|
+
return result;
|
|
1325
|
+
}
|
|
1326
|
+
function boundedPathCommands(font, glyph) {
|
|
1327
|
+
if (!pathIsSafe(font, glyph)) return null;
|
|
1328
|
+
try {
|
|
1329
|
+
return glyph.path.commands;
|
|
1330
|
+
} catch {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
1335
|
+
function colorGlyph(font, glyphId, work) {
|
|
1336
|
+
let byGlyph = cache.get(font);
|
|
1337
|
+
if (!byGlyph) {
|
|
1338
|
+
byGlyph = /* @__PURE__ */ new Map();
|
|
1339
|
+
cache.set(font, byGlyph);
|
|
1340
|
+
}
|
|
1341
|
+
const known = byGlyph.get(glyphId);
|
|
1342
|
+
if (known) return known;
|
|
1343
|
+
let result;
|
|
1344
|
+
try {
|
|
1345
|
+
result = readColorGlyph(font, glyphId, work);
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
if (error instanceof PdfWorkLimitError || error instanceof ExportResourceError2) throw error;
|
|
1348
|
+
result = { kind: "refused", reason: "the color glyph could not be read" };
|
|
1349
|
+
}
|
|
1350
|
+
byGlyph.set(glyphId, result);
|
|
1351
|
+
return result;
|
|
1352
|
+
}
|
|
1353
|
+
function readColorGlyph(font, glyphId, work) {
|
|
1354
|
+
const glyph = font.getGlyph(glyphId);
|
|
1355
|
+
const layers = glyph.layers;
|
|
1356
|
+
if (!layers || layers.length === 0) {
|
|
1357
|
+
const commands = boundedPathCommands(font, glyph);
|
|
1358
|
+
return { kind: "none", outlined: commands === null || commands.length > 0 };
|
|
1359
|
+
}
|
|
1360
|
+
if (layers.length > MAX_LAYERS)
|
|
1361
|
+
return { kind: "refused", reason: `${layers.length} layers exceed the ${MAX_LAYERS} allowed` };
|
|
1362
|
+
const out = [];
|
|
1363
|
+
for (const layer of layers) {
|
|
1364
|
+
work.tick();
|
|
1365
|
+
const commands = boundedPathCommands(font, layer.glyph);
|
|
1366
|
+
const path = commands && pathOperators(commands, work);
|
|
1367
|
+
if (path === null)
|
|
1368
|
+
return { kind: "refused", reason: "a layer outline is malformed or too long" };
|
|
1369
|
+
if (!path) continue;
|
|
1370
|
+
const { red, green, blue, alpha } = layer.color;
|
|
1371
|
+
out.push({
|
|
1372
|
+
fill: `${number(clampByte(red) / 255)} ${number(clampByte(green) / 255)} ${number(clampByte(blue) / 255)} rg`,
|
|
1373
|
+
alpha: clampByte(alpha) / 255,
|
|
1374
|
+
path
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
return { kind: "layers", layers: out };
|
|
1378
|
+
}
|
|
1379
|
+
var outlines = /* @__PURE__ */ new WeakMap();
|
|
1380
|
+
function glyphOutline(font, glyphId, work) {
|
|
1381
|
+
let byGlyph = outlines.get(font);
|
|
1382
|
+
if (!byGlyph) {
|
|
1383
|
+
byGlyph = /* @__PURE__ */ new Map();
|
|
1384
|
+
outlines.set(font, byGlyph);
|
|
1385
|
+
}
|
|
1386
|
+
if (byGlyph.has(glyphId)) return byGlyph.get(glyphId);
|
|
1387
|
+
const commands = boundedPathCommands(font, font.getGlyph(glyphId));
|
|
1388
|
+
const path = commands && pathOperators(commands, work);
|
|
1389
|
+
byGlyph.set(glyphId, path);
|
|
1390
|
+
return path;
|
|
1391
|
+
}
|
|
1392
|
+
function clampByte(value) {
|
|
1393
|
+
return Number.isFinite(value) ? Math.min(255, Math.max(0, Math.round(value))) : 0;
|
|
1394
|
+
}
|
|
1395
|
+
function pathOperators(commands, work) {
|
|
1396
|
+
if (commands.length > MAX_COMMANDS) return null;
|
|
1397
|
+
const out = [];
|
|
1398
|
+
let x = 0;
|
|
1399
|
+
let y = 0;
|
|
1400
|
+
for (const { command, args } of commands) {
|
|
1401
|
+
work.tick();
|
|
1402
|
+
if (args.some((value) => !Number.isFinite(value))) return null;
|
|
1403
|
+
let operator;
|
|
1404
|
+
switch (command) {
|
|
1405
|
+
case "moveTo":
|
|
1406
|
+
[x, y] = [args[0], args[1]];
|
|
1407
|
+
operator = `${number(x)} ${number(y)} m`;
|
|
1408
|
+
break;
|
|
1409
|
+
case "lineTo":
|
|
1410
|
+
[x, y] = [args[0], args[1]];
|
|
1411
|
+
operator = `${number(x)} ${number(y)} l`;
|
|
1412
|
+
break;
|
|
1413
|
+
case "quadraticCurveTo": {
|
|
1414
|
+
const [cx, cy, ex, ey] = [args[0], args[1], args[2], args[3]];
|
|
1415
|
+
const c1x = x + 2 / 3 * (cx - x);
|
|
1416
|
+
const c1y = y + 2 / 3 * (cy - y);
|
|
1417
|
+
const c2x = ex + 2 / 3 * (cx - ex);
|
|
1418
|
+
const c2y = ey + 2 / 3 * (cy - ey);
|
|
1419
|
+
operator = `${number(c1x)} ${number(c1y)} ${number(c2x)} ${number(c2y)} ${number(ex)} ${number(ey)} c`;
|
|
1420
|
+
[x, y] = [ex, ey];
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
case "bezierCurveTo":
|
|
1424
|
+
operator = args.slice(0, 6).map(number).join(" ") + " c";
|
|
1425
|
+
[x, y] = [args[4], args[5]];
|
|
1426
|
+
break;
|
|
1427
|
+
case "closePath":
|
|
1428
|
+
operator = "h";
|
|
1429
|
+
break;
|
|
1430
|
+
default:
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1433
|
+
work.reserveContent(operator.length + 1);
|
|
1434
|
+
out.push(operator);
|
|
1435
|
+
}
|
|
1436
|
+
return out.join(" ");
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/outline-placement.ts
|
|
1440
|
+
var MAX_OUTLINE_MOVE = 1e5;
|
|
1441
|
+
var movable = (value) => Number.isFinite(value) && Math.abs(value) <= MAX_OUTLINE_MOVE;
|
|
1442
|
+
function outlineOperators(placements) {
|
|
1443
|
+
const out = [];
|
|
1444
|
+
let group = null;
|
|
1445
|
+
let [ra, rc, rd, px, py] = [1, 0, 1, 0, 0];
|
|
1446
|
+
for (const placement of placements) {
|
|
1447
|
+
const same = group !== null && group.state === placement.state && group.a === placement.a && group.c === placement.c && group.d === placement.d;
|
|
1448
|
+
const rawV = same && rd !== 0 ? (placement.y - py) / rd : NaN;
|
|
1449
|
+
const v = movable(rawV) ? Number(number(rawV)) : NaN;
|
|
1450
|
+
const rawU = movable(v) && ra !== 0 ? (placement.x - px - rc * v) / ra : NaN;
|
|
1451
|
+
const u = movable(rawU) ? Number(number(rawU)) : NaN;
|
|
1452
|
+
if (movable(u) && movable(v)) {
|
|
1453
|
+
out.push(`1 0 0 1 ${number(u)} ${number(v)} cm /${placement.form} Do`);
|
|
1454
|
+
px += ra * u + rc * v;
|
|
1455
|
+
py += rd * v;
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
if (group) out.push("Q");
|
|
1459
|
+
group = placement;
|
|
1460
|
+
const { a, c, d, x, y } = placement;
|
|
1461
|
+
out.push(`q ${placement.state}${[a, 0, c, d, x, y].map(number).join(" ")} cm /${placement.form} Do`);
|
|
1462
|
+
[ra, rc, rd, px, py] = [a, c, d, x, y].map((value) => Number(number(value)));
|
|
1463
|
+
}
|
|
1464
|
+
if (group) out.push("Q");
|
|
1465
|
+
return out.join("\n");
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// src/text.ts
|
|
1469
|
+
var PDF_PAINT_GRID_PT2 = 0.24;
|
|
1470
|
+
function reducedHalfPoints(fontSizePt, factor) {
|
|
1471
|
+
const authored = Math.max(1, Math.round(fontSizePt * 2));
|
|
1472
|
+
return factor === 1 ? authored : Math.max(1, Math.round(authored * factor));
|
|
1473
|
+
}
|
|
1474
|
+
function snapToGrid(units) {
|
|
1475
|
+
const snapped = Math.round(units * 2) / 2;
|
|
1476
|
+
return Math.abs(units - snapped) < 1e-9 ? snapped : units;
|
|
1477
|
+
}
|
|
1478
|
+
function griddedBaselineInLine(line, face2, paragraph) {
|
|
1479
|
+
const units = (value) => Math.round(value / PDF_PAINT_GRID_PT2);
|
|
1480
|
+
const above = Math.abs(face2.font.ascent) + Math.abs(face2.font.lineGap);
|
|
1481
|
+
const descent = above > 0 ? Math.abs(face2.font.descent) / above * line.baseline : 0;
|
|
1482
|
+
const naturalBox = line.baseline + descent;
|
|
1483
|
+
const endsParagraph = paragraph.paragraphEnd === true && paragraph.spacing.after > 0 && paragraph.lines[paragraph.lines.length - 1] === line;
|
|
1484
|
+
if (endsParagraph || !(descent > 0) || line.box.height > naturalBox + 0.01)
|
|
1485
|
+
return units(line.baseline) * PDF_PAINT_GRID_PT2;
|
|
1486
|
+
return (units(naturalBox) - units(descent)) * PDF_PAINT_GRID_PT2;
|
|
1487
|
+
}
|
|
1488
|
+
var SYNTHETIC_ITALIC_SKEW = 87 / 256;
|
|
1489
|
+
var SYNTHETIC_BOLD_VERTICAL = 1 / 1.02;
|
|
1490
|
+
var syntheticBoldStrokePt = (size) => size / 50 + 0.12;
|
|
1491
|
+
var rightToLeftLines = /* @__PURE__ */ new WeakMap();
|
|
1492
|
+
function rightToLeftOnly(line) {
|
|
1493
|
+
let known = rightToLeftLines.get(line);
|
|
1494
|
+
if (known === void 0) {
|
|
1495
|
+
known = line.spans.every(
|
|
1496
|
+
(span) => !/\S/u.test(span.text) || (span.style.shaping?.level ?? 0) % 2 === 1
|
|
1497
|
+
);
|
|
1498
|
+
rightToLeftLines.set(line, known);
|
|
1499
|
+
}
|
|
1500
|
+
return known;
|
|
1501
|
+
}
|
|
1502
|
+
var MAX_TJ_ADJUSTMENT = 1e6;
|
|
1503
|
+
function penOrigins(run) {
|
|
1504
|
+
const advances = run.exactAdvancesX;
|
|
1505
|
+
if (!advances || advances.length !== run.glyphs.length) return void 0;
|
|
1506
|
+
const origins = [];
|
|
1507
|
+
let pen = 0;
|
|
1508
|
+
for (const advance of advances) {
|
|
1509
|
+
origins.push(pen);
|
|
1510
|
+
pen += advance;
|
|
1511
|
+
}
|
|
1512
|
+
return origins;
|
|
1513
|
+
}
|
|
1514
|
+
function createGlyphBatch(out, horizontal) {
|
|
1515
|
+
let face2 = null;
|
|
1516
|
+
let size = 0;
|
|
1517
|
+
let baselineY = 0;
|
|
1518
|
+
let startX = 0;
|
|
1519
|
+
let penX = 0;
|
|
1520
|
+
let parts = [];
|
|
1521
|
+
let skew = 0;
|
|
1522
|
+
let vertical = 1;
|
|
1523
|
+
let rise = 0;
|
|
1524
|
+
const flush = () => {
|
|
1525
|
+
if (parts.length === 0) return;
|
|
1526
|
+
const show = parts.length === 1 && parts[0].startsWith("<") ? `${parts[0]} Tj` : `[${parts.join("")}] TJ`;
|
|
1527
|
+
out.push(
|
|
1528
|
+
`${number(horizontal)} 0 ${number(skew * vertical)} ${number(vertical)} ${number(startX)} ${number(baselineY + rise)} Tm ${show}`
|
|
1529
|
+
);
|
|
1530
|
+
parts = [];
|
|
1531
|
+
face2 = null;
|
|
1532
|
+
};
|
|
1533
|
+
return {
|
|
1534
|
+
flush,
|
|
1535
|
+
/** Change the synthetic matrix; the next glyph starts a new batch when it changes. */
|
|
1536
|
+
matrix(nextSkew, nextVertical, nextRise) {
|
|
1537
|
+
if (nextSkew === skew && nextVertical === vertical && nextRise === rise) return;
|
|
1538
|
+
flush();
|
|
1539
|
+
skew = nextSkew;
|
|
1540
|
+
vertical = nextVertical;
|
|
1541
|
+
rise = nextRise;
|
|
1542
|
+
},
|
|
1543
|
+
add(nextFace, nextSize, gx, gy, code) {
|
|
1544
|
+
const unit = nextSize * horizontal;
|
|
1545
|
+
const continues = face2 === nextFace && size === nextSize && baselineY === gy && unit !== 0;
|
|
1546
|
+
const adjustment = continues ? 1e3 * (penX - gx) / unit : Number.NaN;
|
|
1547
|
+
if (!Number.isFinite(adjustment) || Math.abs(adjustment) > MAX_TJ_ADJUSTMENT) {
|
|
1548
|
+
flush();
|
|
1549
|
+
face2 = nextFace;
|
|
1550
|
+
size = nextSize;
|
|
1551
|
+
baselineY = gy;
|
|
1552
|
+
startX = gx;
|
|
1553
|
+
penX = gx;
|
|
1554
|
+
} else if (gx !== penX) {
|
|
1555
|
+
const written = number(adjustment);
|
|
1556
|
+
parts.push(written);
|
|
1557
|
+
penX -= Number(written) / 1e3 * unit;
|
|
1558
|
+
}
|
|
1559
|
+
parts.push(`<${code}>`);
|
|
1560
|
+
penX += nextFace.declaredWidth(code) / 1e3 * unit;
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
var TextWriter = class {
|
|
1565
|
+
constructor(doc, session, work, showRevisionMarkup) {
|
|
1566
|
+
this.doc = doc;
|
|
1567
|
+
this.session = session;
|
|
1568
|
+
this.work = work;
|
|
1569
|
+
this.showRevisionMarkup = showRevisionMarkup;
|
|
1570
|
+
}
|
|
1571
|
+
doc;
|
|
1572
|
+
session;
|
|
1573
|
+
work;
|
|
1574
|
+
showRevisionMarkup;
|
|
1575
|
+
faces = /* @__PURE__ */ new Map();
|
|
1576
|
+
/** Outline glyphs of the current line, for {@link takeOutlines}. */
|
|
1577
|
+
pendingOutlines = [];
|
|
1578
|
+
/** The outline glyphs painted since the last call; the caller places them after a line. */
|
|
1579
|
+
takeOutlines() {
|
|
1580
|
+
const taken = this.pendingOutlines.length > 0 ? outlineOperators(this.pendingOutlines) : "";
|
|
1581
|
+
this.pendingOutlines = [];
|
|
1582
|
+
return taken;
|
|
1583
|
+
}
|
|
1584
|
+
refused = /* @__PURE__ */ new Map();
|
|
1585
|
+
paint(visit, page) {
|
|
1586
|
+
const { span, line, storyOrigin, absoluteBox } = visit;
|
|
1587
|
+
if (span.style.hidden || !span.text) return "";
|
|
1588
|
+
if (span.noteSeparator) {
|
|
1589
|
+
const box = noteSeparatorRuleBox(span, line);
|
|
1590
|
+
return `${color(span.style.color)} rg ${rect(box, storyOrigin.x - visit.page.box.x, storyOrigin.y - visit.page.box.y, pageHeight(page), true)} f`;
|
|
1591
|
+
}
|
|
1592
|
+
if (/^[\t\n\r\f]+$/.test(span.text)) return span.tabLeader ? this.leader(visit, page) : "";
|
|
1593
|
+
const report = (code, message) => {
|
|
1594
|
+
this.work.report(code, message, visit.page.index);
|
|
1595
|
+
return "";
|
|
1596
|
+
};
|
|
1597
|
+
const shaped = this.session.shapeLaidOutText(span);
|
|
1598
|
+
const family = styleForFontSlot2(span.style, span.fontSlot).fontFamily ?? "the default font";
|
|
1599
|
+
if (!shaped)
|
|
1600
|
+
return report(
|
|
1601
|
+
"unshaped-text",
|
|
1602
|
+
`Core could not provide exact shaping for a visible span in ${family}`
|
|
1603
|
+
);
|
|
1604
|
+
if (shaped.run.glyphs.some((g) => g.id === 0))
|
|
1605
|
+
return report(
|
|
1606
|
+
"missing-glyph",
|
|
1607
|
+
`A visible span in ${family} contains missing glyphs${missingGlyphHint(span.text)}`
|
|
1608
|
+
);
|
|
1609
|
+
const face2 = this.embeddedFace(shaped.font, visit, page);
|
|
1610
|
+
if (!face2) return "";
|
|
1611
|
+
const style = styleForFontSlot2(span.style, span.fontSlot);
|
|
1612
|
+
const factor = glyphSizeFactorOf(style);
|
|
1613
|
+
const size = Number(
|
|
1614
|
+
(Math.max(
|
|
1615
|
+
1,
|
|
1616
|
+
Math.round(reducedHalfPoints(style.fontSizePt, factor) / 2 / PDF_PAINT_GRID_PT2)
|
|
1617
|
+
) * PDF_PAINT_GRID_PT2).toFixed(6)
|
|
1618
|
+
);
|
|
1619
|
+
const horizontal = style.horizontalScalePercent / 100;
|
|
1620
|
+
const scale = factor / shaped.fixedPointScale;
|
|
1621
|
+
const x = absoluteBox.x - visit.page.box.x + (span.glyphOffsetPt ?? 0) + paragraphGridOffsetX(visit);
|
|
1622
|
+
const baselineFromTop = storyOrigin.y - visit.page.box.y + line.box.y + griddedBaselineInLine(line, face2, visit.paragraph) - baselineShiftPtOf(style);
|
|
1623
|
+
const unitsFromTop = snapToGrid(baselineFromTop / PDF_PAINT_GRID_PT2);
|
|
1624
|
+
const baseline = pageHeight(page) - Math.ceil(unitsFromTop - 0.5) * PDF_PAINT_GRID_PT2;
|
|
1625
|
+
let foreground = style.color;
|
|
1626
|
+
const revisions = this.showRevisionMarkup ? span.revisions ?? [] : [];
|
|
1627
|
+
const insert = revisions.some((r) => r.kind === "insert" || r.kind === "moveTo");
|
|
1628
|
+
const deleted = revisions.some((r) => r.kind === "delete" || r.kind === "moveFrom");
|
|
1629
|
+
if (insert) foreground = "008000";
|
|
1630
|
+
if (deleted) foreground = "C00000";
|
|
1631
|
+
const out = [`${color(foreground)} rg`, "BT"];
|
|
1632
|
+
if (!face2.colorLayers) out.push(`/${face2.name} ${number(size)} Tf`);
|
|
1633
|
+
if (style.textOutline)
|
|
1634
|
+
out.unshift(`q ${color(style.textOutline.color)} RG ${number(style.textOutline.widthPt)} w 2 Tr`);
|
|
1635
|
+
const origins = penOrigins(shaped.run);
|
|
1636
|
+
const batch = createGlyphBatch(out, horizontal);
|
|
1637
|
+
const colorGlyphs = [];
|
|
1638
|
+
const outlineGlyphs = [];
|
|
1639
|
+
const rightToLeft = (style.shaping?.level ?? 0) % 2 === 1;
|
|
1640
|
+
let extra = 0;
|
|
1641
|
+
let strokeWidth = 0;
|
|
1642
|
+
let stroked = false;
|
|
1643
|
+
let activeSize = size;
|
|
1644
|
+
let activeFace = face2;
|
|
1645
|
+
let activeIdentity = shaped.font.identity;
|
|
1646
|
+
for (const cluster of [...shaped.run.clusters].sort((a, b) => a.glyphStart - b.glyphStart)) {
|
|
1647
|
+
const identity = shaped.fonts?.[cluster.fontSpan];
|
|
1648
|
+
if (identity && identity.identity !== activeIdentity) {
|
|
1649
|
+
const clusterFace = this.embeddedFace(identity, visit, page);
|
|
1650
|
+
if (!clusterFace) return "";
|
|
1651
|
+
activeFace = clusterFace;
|
|
1652
|
+
activeIdentity = identity.identity;
|
|
1653
|
+
batch.flush();
|
|
1654
|
+
if (!activeFace.colorLayers) out.push(`/${activeFace.name} ${number(activeSize)} Tf`);
|
|
1655
|
+
}
|
|
1656
|
+
const text = shaped.run.text.slice(cluster.textStart, cluster.textEnd);
|
|
1657
|
+
let textGlyph = cluster.glyphStart;
|
|
1658
|
+
for (let i = cluster.glyphStart + 1; i < cluster.glyphEnd; i++)
|
|
1659
|
+
if (shaped.run.glyphs[i].advanceX > shaped.run.glyphs[textGlyph].advanceX) textGlyph = i;
|
|
1660
|
+
for (let i = cluster.glyphStart; i < cluster.glyphEnd; i++) {
|
|
1661
|
+
this.work.tick();
|
|
1662
|
+
const glyph = shaped.run.glyphs[i];
|
|
1663
|
+
const glyphSize = size * (glyph.drawScale ?? 1);
|
|
1664
|
+
if (glyphSize !== activeSize) {
|
|
1665
|
+
batch.flush();
|
|
1666
|
+
if (!activeFace.colorLayers) out.push(`/${activeFace.name} ${number(glyphSize)} Tf`);
|
|
1667
|
+
activeSize = glyphSize;
|
|
1668
|
+
}
|
|
1669
|
+
const origin = origins?.[i] ?? glyph.originX;
|
|
1670
|
+
const gx = x + (origin + glyph.offsetX) * scale * horizontal + extra;
|
|
1671
|
+
const gy = baseline + (glyph.originY + glyph.offsetY) * scale;
|
|
1672
|
+
if (activeFace.colorLayers) {
|
|
1673
|
+
const color2 = colorGlyph(activeFace.font, glyph.id, this.work);
|
|
1674
|
+
if (color2.kind === "refused" || color2.kind === "none" && color2.outlined) {
|
|
1675
|
+
this.work.report(
|
|
1676
|
+
"color-glyph",
|
|
1677
|
+
color2.kind === "refused" ? `A color glyph could not be painted: ${color2.reason}` : "A glyph of a color face has no color layers and the face is not embedded",
|
|
1678
|
+
visit.page.index
|
|
1679
|
+
);
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
if (color2.kind === "layers")
|
|
1683
|
+
colorGlyphs.push({
|
|
1684
|
+
layers: color2.layers,
|
|
1685
|
+
x: gx,
|
|
1686
|
+
y: gy,
|
|
1687
|
+
scale: glyphSize / activeFace.font.unitsPerEm
|
|
1688
|
+
});
|
|
1689
|
+
const host = i === cluster.glyphStart && text ? this.textHost(face2, page) : null;
|
|
1690
|
+
const carrier = host?.carrier(text) ?? null;
|
|
1691
|
+
if (host && carrier) {
|
|
1692
|
+
let advance = 0;
|
|
1693
|
+
for (let j = cluster.glyphStart; j < cluster.glyphEnd; j++)
|
|
1694
|
+
advance += shaped.run.glyphs[j].advanceX * scale;
|
|
1695
|
+
const spaceWidth = host.declaredWidth(carrier) / 1e3 * activeSize;
|
|
1696
|
+
const stretch = spaceWidth > 0 && advance > 0 ? Math.min(1e4, Math.max(1, 100 * advance / spaceWidth)) : 100;
|
|
1697
|
+
batch.flush();
|
|
1698
|
+
out.push(`/${host.name} ${number(activeSize)} Tf 3 Tr ${number(stretch)} Tz`);
|
|
1699
|
+
batch.add(host, activeSize, gx, gy, carrier);
|
|
1700
|
+
batch.flush();
|
|
1701
|
+
out.push("0 Tr 100 Tz");
|
|
1702
|
+
strokeWidth = 0;
|
|
1703
|
+
}
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
const synthesizeBold = style.bold && !style.textOutline && !activeFace.bold;
|
|
1707
|
+
const width = synthesizeBold ? syntheticBoldStrokePt(activeSize) : 0;
|
|
1708
|
+
if (width !== strokeWidth) {
|
|
1709
|
+
batch.flush();
|
|
1710
|
+
if (width > 0 && !stroked) out.push(`${color(foreground)} RG`);
|
|
1711
|
+
stroked ||= width > 0;
|
|
1712
|
+
out.push(width > 0 ? `${number(width)} w 2 Tr` : "0 Tr");
|
|
1713
|
+
strokeWidth = width;
|
|
1714
|
+
}
|
|
1715
|
+
batch.matrix(
|
|
1716
|
+
style.italic && !activeFace.slanted ? SYNTHETIC_ITALIC_SKEW : 0,
|
|
1717
|
+
width > 0 ? SYNTHETIC_BOLD_VERTICAL : 1,
|
|
1718
|
+
width > 0 ? width / 2 * SYNTHETIC_BOLD_VERTICAL : 0
|
|
1719
|
+
);
|
|
1720
|
+
if (rightToLeft && /^ +$/.test(text) && rightToLeftOnly(line)) continue;
|
|
1721
|
+
if (i === textGlyph) {
|
|
1722
|
+
batch.add(activeFace, activeSize, gx, gy, activeFace.encode(glyph.id, text));
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
const outline = glyphOutline(activeFace.font, glyph.id, this.work);
|
|
1726
|
+
if (outline === null) {
|
|
1727
|
+
this.work.report(
|
|
1728
|
+
"glyph-outline",
|
|
1729
|
+
`A glyph outline in ${family} is malformed or too long`,
|
|
1730
|
+
visit.page.index
|
|
1731
|
+
);
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
if (outline) {
|
|
1735
|
+
outlineGlyphs.push({
|
|
1736
|
+
face: activeFace,
|
|
1737
|
+
id: glyph.id,
|
|
1738
|
+
path: outline,
|
|
1739
|
+
x: gx,
|
|
1740
|
+
y: gy + (width > 0 ? width / 2 * SYNTHETIC_BOLD_VERTICAL : 0),
|
|
1741
|
+
scale: glyphSize / activeFace.font.unitsPerEm,
|
|
1742
|
+
skew: style.italic && !activeFace.slanted ? SYNTHETIC_ITALIC_SKEW : 0,
|
|
1743
|
+
vertical: width > 0 ? SYNTHETIC_BOLD_VERTICAL : 1,
|
|
1744
|
+
strokeWidth: width
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
extra += text.length * style.characterSpacingPt + (text.match(/ /g)?.length ?? 0) * (style.shaping?.wordSpacingPt ?? 0);
|
|
1749
|
+
}
|
|
1750
|
+
batch.flush();
|
|
1751
|
+
if (strokeWidth > 0) out.push("0 Tr");
|
|
1752
|
+
out.push("ET");
|
|
1753
|
+
if (style.textOutline) out.push("Q");
|
|
1754
|
+
const placements = [];
|
|
1755
|
+
for (const glyph of outlineGlyphs) {
|
|
1756
|
+
const matrix = [
|
|
1757
|
+
glyph.scale * horizontal,
|
|
1758
|
+
0,
|
|
1759
|
+
glyph.skew * glyph.scale * glyph.vertical,
|
|
1760
|
+
glyph.scale * glyph.vertical,
|
|
1761
|
+
glyph.x,
|
|
1762
|
+
glyph.y
|
|
1763
|
+
];
|
|
1764
|
+
const outlineStroke = style.textOutline ? { color: style.textOutline.color, width: style.textOutline.widthPt } : glyph.strokeWidth > 0 ? { color: foreground, width: glyph.strokeWidth } : null;
|
|
1765
|
+
const stroke = outlineStroke ? `${color(outlineStroke.color)} RG ${number(outlineStroke.width / (glyph.scale * glyph.vertical))} w ` : "";
|
|
1766
|
+
const form = this.outlineForm(
|
|
1767
|
+
glyph.face,
|
|
1768
|
+
glyph.id,
|
|
1769
|
+
glyph.path,
|
|
1770
|
+
outlineStroke ? "B" : "f",
|
|
1771
|
+
page
|
|
1772
|
+
);
|
|
1773
|
+
placements.push({
|
|
1774
|
+
state: `${color(foreground)} rg ${stroke}`,
|
|
1775
|
+
a: matrix[0],
|
|
1776
|
+
c: matrix[2],
|
|
1777
|
+
d: matrix[3],
|
|
1778
|
+
x: glyph.x,
|
|
1779
|
+
y: glyph.y,
|
|
1780
|
+
form
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
if (visit.paragraph.clipToBox) {
|
|
1784
|
+
if (placements.length > 0) out.push(outlineOperators(placements));
|
|
1785
|
+
} else {
|
|
1786
|
+
for (const placement of placements) this.pendingOutlines.push(placement);
|
|
1787
|
+
}
|
|
1788
|
+
for (const { layers, x: gx, y: gy, scale: glyphScale } of colorGlyphs) {
|
|
1789
|
+
out.push(`q ${number(glyphScale * horizontal)} 0 0 ${number(glyphScale)} ${number(gx)} ${number(gy)} cm`);
|
|
1790
|
+
for (const layer of layers) {
|
|
1791
|
+
const alpha = layer.alpha < 1 ? this.alphaState(page, layer.alpha) : null;
|
|
1792
|
+
out.push(
|
|
1793
|
+
alpha ? `q /${alpha} gs ${layer.fill} ${layer.path} f Q` : `${layer.fill} ${layer.path} f`
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
out.push("Q");
|
|
1797
|
+
}
|
|
1798
|
+
const metricScale = size / face2.font.unitsPerEm;
|
|
1799
|
+
const onGrid2 = (value) => Math.round(value / PDF_PAINT_GRID_PT2) * PDF_PAINT_GRID_PT2;
|
|
1800
|
+
const lineRule = (y, thickness, c = foreground, width = absoluteBox.width) => {
|
|
1801
|
+
const left = onGrid2(x);
|
|
1802
|
+
const right = onGrid2(x + width);
|
|
1803
|
+
const painted = right > left ? right - left : width > 0 ? PDF_PAINT_GRID_PT2 : 0;
|
|
1804
|
+
if (painted <= 0) return;
|
|
1805
|
+
out.push(
|
|
1806
|
+
`${color(c)} rg ${number(left)} ${number(y - thickness / 2)} ${number(painted)} ${number(thickness)} re f`
|
|
1807
|
+
);
|
|
1808
|
+
};
|
|
1809
|
+
if (style.underline || insert) {
|
|
1810
|
+
const variant = style.underline?.variant ?? "single";
|
|
1811
|
+
const metricsUsable = face2.font.underlineThickness > 0 && Number.isFinite(face2.font.underlinePosition);
|
|
1812
|
+
if (!metricsUsable)
|
|
1813
|
+
this.work.report(
|
|
1814
|
+
"underline-metrics",
|
|
1815
|
+
"Font has no valid underline metrics; the underline uses a default position and thickness",
|
|
1816
|
+
visit.page.index,
|
|
1817
|
+
"information"
|
|
1818
|
+
);
|
|
1819
|
+
const rawThickness = metricsUsable ? face2.font.underlineThickness * metricScale : size / 20;
|
|
1820
|
+
const rawOffset = metricsUsable ? -face2.font.underlinePosition * metricScale : size / 10;
|
|
1821
|
+
const thickness = Math.max(1, Math.round(rawThickness / PDF_PAINT_GRID_PT2)) * PDF_PAINT_GRID_PT2;
|
|
1822
|
+
const position = baseline - Math.round(rawOffset / PDF_PAINT_GRID_PT2) * PDF_PAINT_GRID_PT2 - thickness / 2;
|
|
1823
|
+
{
|
|
1824
|
+
const c = style.underline?.color ?? foreground;
|
|
1825
|
+
const supported = [
|
|
1826
|
+
"single",
|
|
1827
|
+
"double",
|
|
1828
|
+
"thick",
|
|
1829
|
+
"dotted",
|
|
1830
|
+
"dash",
|
|
1831
|
+
"dashLong",
|
|
1832
|
+
"dotDash",
|
|
1833
|
+
"dotDotDash",
|
|
1834
|
+
"wave",
|
|
1835
|
+
"wavyDouble",
|
|
1836
|
+
"wavyHeavy",
|
|
1837
|
+
"dottedHeavy",
|
|
1838
|
+
"dashedHeavy",
|
|
1839
|
+
"dashLongHeavy",
|
|
1840
|
+
"dashDotHeavy",
|
|
1841
|
+
"dashDotDotHeavy",
|
|
1842
|
+
"words"
|
|
1843
|
+
];
|
|
1844
|
+
if (!supported.includes(variant))
|
|
1845
|
+
this.work.report(
|
|
1846
|
+
"underline-style",
|
|
1847
|
+
`Unsupported underline: ${variant}`,
|
|
1848
|
+
visit.page.index
|
|
1849
|
+
);
|
|
1850
|
+
const heavy = variant === "thick" || variant.endsWith("Heavy");
|
|
1851
|
+
const weight = thickness * (heavy ? 2 : 1);
|
|
1852
|
+
if (variant === "wave" || variant.startsWith("wavy")) {
|
|
1853
|
+
const wave = (y) => {
|
|
1854
|
+
const period = Math.max(2, weight * 6), amplitude = Math.max(0.6, weight);
|
|
1855
|
+
const path = [`${number(x)} ${number(y)} m`];
|
|
1856
|
+
for (let dx = 0; dx < absoluteBox.width; dx += period) {
|
|
1857
|
+
this.work.tick();
|
|
1858
|
+
const end = Math.min(dx + period, absoluteBox.width), length = end - dx;
|
|
1859
|
+
path.push(
|
|
1860
|
+
`${number(x + dx + length / 4)} ${number(y + amplitude)} ${number(x + dx + length / 4)} ${number(y + amplitude)} ${number(x + dx + length / 2)} ${number(y)} c`,
|
|
1861
|
+
`${number(x + dx + 3 * length / 4)} ${number(y - amplitude)} ${number(x + dx + 3 * length / 4)} ${number(y - amplitude)} ${number(x + end)} ${number(y)} c`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
out.push(`q ${color(c)} RG ${number(weight)} w ${path.join(" ")} S Q`);
|
|
1865
|
+
};
|
|
1866
|
+
wave(position);
|
|
1867
|
+
if (variant === "wavyDouble") wave(position - weight * 4);
|
|
1868
|
+
} else if (variant.startsWith("dot") || variant.toLowerCase().includes("dash")) {
|
|
1869
|
+
const dotted = variant.startsWith("dotted");
|
|
1870
|
+
const pattern = dotted ? [weight, weight * 2] : variant.toLowerCase().includes("dotdot") ? [weight * 4, weight * 2, weight, weight * 2, weight, weight * 2] : variant.toLowerCase().includes("dot") ? [weight * 4, weight * 2, weight, weight * 2] : [weight * (variant.includes("Long") ? 8 : 4), weight * 2];
|
|
1871
|
+
out.push(
|
|
1872
|
+
`q ${color(c)} RG ${number(weight)} w [${pattern.map(number).join(" ")}] 0 d ${number(x)} ${number(position)} m ${number(x + absoluteBox.width)} ${number(position)} l S Q`
|
|
1873
|
+
);
|
|
1874
|
+
} else if (variant === "words") {
|
|
1875
|
+
for (const cluster of shaped.run.clusters) {
|
|
1876
|
+
if (/^\s+$/.test(shaped.run.text.slice(cluster.textStart, cluster.textEnd))) continue;
|
|
1877
|
+
const glyph = shaped.run.glyphs[cluster.glyphStart];
|
|
1878
|
+
out.push(
|
|
1879
|
+
`${color(c)} rg ${number(x + glyph.originX * scale * horizontal)} ${number(position - thickness / 2)} ${number(cluster.advance * scale * horizontal)} ${number(thickness)} re f`
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
} else {
|
|
1883
|
+
lineRule(
|
|
1884
|
+
position,
|
|
1885
|
+
weight,
|
|
1886
|
+
c,
|
|
1887
|
+
absoluteBox.width + (variant === "single" ? underlineGap(visit) : 0)
|
|
1888
|
+
);
|
|
1889
|
+
if (variant === "double") lineRule(position - thickness * 2, thickness, c);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
if (style.strike || style.doubleStrike || deleted) {
|
|
1894
|
+
const declared = face2.strike && face2.strike.thickness > 0 ? face2.strike : null;
|
|
1895
|
+
if (!declared)
|
|
1896
|
+
this.work.report(
|
|
1897
|
+
"strike-metrics",
|
|
1898
|
+
"Font has no strike metrics; the strike is placed from the ascent",
|
|
1899
|
+
visit.page.index,
|
|
1900
|
+
"information"
|
|
1901
|
+
);
|
|
1902
|
+
const rawThickness = (declared?.thickness ?? face2.font.underlineThickness) * metricScale;
|
|
1903
|
+
const rawOffset = declared ? declared.position * metricScale : Math.abs(face2.font.ascent) * 0.3 * metricScale;
|
|
1904
|
+
const thickness = Math.max(1, Math.round(rawThickness / PDF_PAINT_GRID_PT2)) * PDF_PAINT_GRID_PT2;
|
|
1905
|
+
const position = baseline + Math.round(rawOffset / PDF_PAINT_GRID_PT2) * PDF_PAINT_GRID_PT2;
|
|
1906
|
+
lineRule(position, thickness);
|
|
1907
|
+
if (style.doubleStrike) lineRule(position + thickness * 2, thickness);
|
|
1908
|
+
}
|
|
1909
|
+
return out.join("\n");
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* The band a highlight or character shading fills: the span's own face box, seated on the
|
|
1913
|
+
* line's baseline.
|
|
1914
|
+
*
|
|
1915
|
+
* Layout publishes every span box at the LINE top with the span's own face height. That is
|
|
1916
|
+
* the selection band, and the screen painter seats it on the baseline with
|
|
1917
|
+
* `vertical-align`. Filling the box where it is published puts a small highlighted run at
|
|
1918
|
+
* the top of a line a larger run made tall, with its glyphs hanging out under the band.
|
|
1919
|
+
* Word draws the band around the run's own face, ascent and external leading above the
|
|
1920
|
+
* baseline and descent below, so a mixed-size line highlights stepped: each run at its own
|
|
1921
|
+
* height, all seated on one baseline. A raised or lowered script carries its band with it.
|
|
1922
|
+
*
|
|
1923
|
+
* The split above and below the baseline comes from the face when it is admitted, and from
|
|
1924
|
+
* the line's own glyph band otherwise, so a span this writer cannot shape (a tab, a face it
|
|
1925
|
+
* refused) still lands on the baseline instead of at the line top.
|
|
1926
|
+
*/
|
|
1927
|
+
bandBox(visit) {
|
|
1928
|
+
const { span, line, absoluteBox } = visit;
|
|
1929
|
+
const shaped = this.session.shapeLaidOutText(span);
|
|
1930
|
+
const face2 = shaped ? this.admittedFace(shaped.font, visit) : void 0;
|
|
1931
|
+
const leading = line.leading ?? 0;
|
|
1932
|
+
const above = face2 ? Math.abs(face2.font.ascent) + Math.max(0, face2.font.lineGap) : line.baseline - leading;
|
|
1933
|
+
const total = face2 ? above + Math.abs(face2.font.descent) : line.box.height - leading - (line.trailingSpacing ?? 0);
|
|
1934
|
+
const faceBaseline = total > 0 ? span.box.height * above / total : span.box.height;
|
|
1935
|
+
const top = line.box.y + line.baseline - faceBaseline - baselineShiftPtOf(styleForFontSlot2(span.style, span.fontSlot));
|
|
1936
|
+
return { ...absoluteBox, y: absoluteBox.y + (top - span.box.y) };
|
|
1937
|
+
}
|
|
1938
|
+
outlineForms = /* @__PURE__ */ new Map();
|
|
1939
|
+
/**
|
|
1940
|
+
* A form XObject that paints one glyph outline in font units, registered on the page.
|
|
1941
|
+
*
|
|
1942
|
+
* The same few dot and mark glyphs repeat across a page, so each outline is written once
|
|
1943
|
+
* per face and paint operator and placed with `Do`. The form sets no colour or width, so
|
|
1944
|
+
* the caller's fill, stroke and line width apply.
|
|
1945
|
+
*/
|
|
1946
|
+
outlineForm(face2, glyphId, path, operator, page) {
|
|
1947
|
+
let forms = this.outlineForms.get(face2);
|
|
1948
|
+
if (!forms) this.outlineForms.set(face2, forms = /* @__PURE__ */ new Map());
|
|
1949
|
+
const key = `${glyphId}:${operator}`;
|
|
1950
|
+
let form = forms.get(key);
|
|
1951
|
+
if (!form) {
|
|
1952
|
+
this.work.reserveContent(path.length + 2);
|
|
1953
|
+
const margin = face2.font.unitsPerEm / 4;
|
|
1954
|
+
const bbox = face2.font.bbox;
|
|
1955
|
+
const ref = this.doc.context.register(
|
|
1956
|
+
flateStream(this.doc.context, `${path} ${operator}`, {
|
|
1957
|
+
Type: "XObject",
|
|
1958
|
+
Subtype: "Form",
|
|
1959
|
+
BBox: [bbox.minX - margin, bbox.minY - margin, bbox.maxX + margin, bbox.maxY + margin]
|
|
1960
|
+
})
|
|
1961
|
+
);
|
|
1962
|
+
form = { name: `${face2.name}G${forms.size + 1}`, ref };
|
|
1963
|
+
forms.set(key, form);
|
|
1964
|
+
}
|
|
1965
|
+
page.node.setXObject(PDFName2.of(form.name), form.ref);
|
|
1966
|
+
return form.name;
|
|
1967
|
+
}
|
|
1968
|
+
alphaStates = /* @__PURE__ */ new Map();
|
|
1969
|
+
/** A registered constant-alpha graphics state, shared across layers of the same alpha. */
|
|
1970
|
+
alphaState(page, alpha) {
|
|
1971
|
+
const key = Math.round(alpha * 1e3);
|
|
1972
|
+
let ref = this.alphaStates.get(key);
|
|
1973
|
+
if (!ref) {
|
|
1974
|
+
ref = this.doc.context.register(
|
|
1975
|
+
this.doc.context.obj({ Type: "ExtGState", ca: key / 1e3, CA: key / 1e3 })
|
|
1976
|
+
);
|
|
1977
|
+
this.alphaStates.set(key, ref);
|
|
1978
|
+
}
|
|
1979
|
+
const name = `LayerAlpha${key}`;
|
|
1980
|
+
page.node.setExtGState(PDFName2.of(name), ref);
|
|
1981
|
+
return name;
|
|
1982
|
+
}
|
|
1983
|
+
/** An embedded text face on this page to carry extractable characters: the run's own, or any. */
|
|
1984
|
+
textHost(face2, page) {
|
|
1985
|
+
const host = face2.colorLayers ? [...this.faces.values()].find((candidate) => !candidate.colorLayers) ?? null : face2;
|
|
1986
|
+
if (host) this.registerFont(page, host);
|
|
1987
|
+
return host;
|
|
1988
|
+
}
|
|
1989
|
+
embeddedFace(font, visit, page) {
|
|
1990
|
+
const face2 = this.admittedFace(font, visit);
|
|
1991
|
+
if (face2 && !face2.colorLayers) this.registerFont(page, face2);
|
|
1992
|
+
return face2;
|
|
1993
|
+
}
|
|
1994
|
+
pageFonts = /* @__PURE__ */ new WeakMap();
|
|
1995
|
+
/**
|
|
1996
|
+
* Name a face in the page's font resources once. pdf-lib re-reads the page's inherited
|
|
1997
|
+
* resources on every call, and nearly every span on a page names a face already there.
|
|
1998
|
+
*/
|
|
1999
|
+
registerFont(page, face2) {
|
|
2000
|
+
let registered = this.pageFonts.get(page);
|
|
2001
|
+
if (!registered) this.pageFonts.set(page, registered = /* @__PURE__ */ new Set());
|
|
2002
|
+
if (registered.has(face2)) return;
|
|
2003
|
+
registered.add(face2);
|
|
2004
|
+
page.node.setFontDictionary(PDFName2.of(face2.name), face2.ref);
|
|
2005
|
+
}
|
|
2006
|
+
/** The embedded face for a shaped font, without registering it on a page. */
|
|
2007
|
+
admittedFace(font, visit) {
|
|
2008
|
+
const report = (code, message) => {
|
|
2009
|
+
this.work.report(code, message, visit.page.index);
|
|
2010
|
+
return void 0;
|
|
2011
|
+
};
|
|
2012
|
+
let face2 = this.faces.get(font.identity);
|
|
2013
|
+
try {
|
|
2014
|
+
if (this.refused.has(font.identity))
|
|
2015
|
+
return report("font-embedding", this.refused.get(font.identity));
|
|
2016
|
+
if (!face2) {
|
|
2017
|
+
const admitted = this.session.admittedFontFace(font.request);
|
|
2018
|
+
if (!admitted || admitted.identity !== font.identity)
|
|
2019
|
+
return report("font-identity", "The shaped face does not match the admitted font");
|
|
2020
|
+
face2 = new EmbeddedFace(this.doc, admitted, this.faces.size + 1);
|
|
2021
|
+
this.faces.set(font.identity, face2);
|
|
2022
|
+
}
|
|
2023
|
+
} catch (error) {
|
|
2024
|
+
const reason = error instanceof Error ? error.message : "Font embedding refused";
|
|
2025
|
+
this.refused.set(font.identity, reason);
|
|
2026
|
+
return report("font-embedding", reason);
|
|
2027
|
+
}
|
|
2028
|
+
return face2;
|
|
2029
|
+
}
|
|
2030
|
+
leader(visit, page) {
|
|
2031
|
+
const glyph = TAB_LEADER_GLYPH.get(visit.span.tabLeader);
|
|
2032
|
+
if (!glyph || visit.absoluteBox.width <= 0) return "";
|
|
2033
|
+
const advance = visit.span.tabLeaderAdvancePt;
|
|
2034
|
+
if (!advance || advance <= 0) {
|
|
2035
|
+
this.work.report(
|
|
2036
|
+
"tab-leader-metrics",
|
|
2037
|
+
"Tab leader has no measured advance",
|
|
2038
|
+
visit.page.index
|
|
2039
|
+
);
|
|
2040
|
+
return "";
|
|
2041
|
+
}
|
|
2042
|
+
const x = visit.absoluteBox.x - visit.page.box.x;
|
|
2043
|
+
const pattern = tabLeaderPattern(
|
|
2044
|
+
visit.textboxDepth ? visit.span.box.x : x,
|
|
2045
|
+
visit.absoluteBox.width,
|
|
2046
|
+
advance
|
|
2047
|
+
);
|
|
2048
|
+
const y = pageHeight(page) - (visit.storyOrigin.y + visit.line.box.y - visit.page.box.y) - visit.line.box.height;
|
|
2049
|
+
const out = [
|
|
2050
|
+
`q ${number(x)} ${number(y)} ${number(visit.absoluteBox.width)} ${number(visit.line.box.height)} re W n`
|
|
2051
|
+
];
|
|
2052
|
+
for (let i = 0; i < pattern.count; i++) {
|
|
2053
|
+
this.work.tick();
|
|
2054
|
+
out.push(
|
|
2055
|
+
this.paint(
|
|
2056
|
+
{
|
|
2057
|
+
...visit,
|
|
2058
|
+
absoluteBox: {
|
|
2059
|
+
...visit.absoluteBox,
|
|
2060
|
+
x: visit.absoluteBox.x + pattern.offsetPt + i * advance,
|
|
2061
|
+
width: advance
|
|
2062
|
+
},
|
|
2063
|
+
span: {
|
|
2064
|
+
...visit.span,
|
|
2065
|
+
text: glyph,
|
|
2066
|
+
tabLeader: void 0,
|
|
2067
|
+
style: {
|
|
2068
|
+
...visit.span.style,
|
|
2069
|
+
characterSpacingPt: 0,
|
|
2070
|
+
underline: null,
|
|
2071
|
+
bold: visit.span.tabLeader === "heavy" || visit.span.style.bold
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
},
|
|
2075
|
+
page
|
|
2076
|
+
)
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
out.push("Q");
|
|
2080
|
+
return out.join("\n");
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
|
|
2084
|
+
// src/annotations.ts
|
|
2085
|
+
import {
|
|
2086
|
+
PDFDict,
|
|
2087
|
+
PDFHexString,
|
|
2088
|
+
PDFName as PDFName3,
|
|
2089
|
+
PDFString as PDFString2
|
|
2090
|
+
} from "pdf-lib";
|
|
2091
|
+
function add(doc, page, dictionary) {
|
|
2092
|
+
const ref = doc.context.register(doc.context.obj(dictionary));
|
|
2093
|
+
page.node.addAnnot(ref);
|
|
2094
|
+
return ref;
|
|
2095
|
+
}
|
|
2096
|
+
function linkAnnotation(doc, page, visit, destinations2) {
|
|
2097
|
+
const link = visit.span.link;
|
|
2098
|
+
if (!link || !link.href) return;
|
|
2099
|
+
const b = visit.absoluteBox;
|
|
2100
|
+
const x = b.x - visit.page.box.x, y = pageHeight(page) - (b.y - visit.page.box.y) - b.height;
|
|
2101
|
+
const base = {
|
|
2102
|
+
Type: "Annot",
|
|
2103
|
+
Subtype: "Link",
|
|
2104
|
+
Rect: [x, y, x + b.width, y + b.height],
|
|
2105
|
+
Border: [0, 0, 0]
|
|
2106
|
+
};
|
|
2107
|
+
if (link.kind === "internal" && link.anchor && destinations2.has(link.anchor))
|
|
2108
|
+
add(doc, page, { ...base, Dest: PDFHexString.fromText(link.anchor) });
|
|
2109
|
+
else if (link.kind === "external" && /^(https?:|mailto:|tel:|ftp:)/i.test(link.href)) {
|
|
2110
|
+
const uri = pdfLiteralUri(link.href);
|
|
2111
|
+
if (uri !== null) add(doc, page, { ...base, A: { S: "URI", URI: PDFString2.of(uri) } });
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
function destinations(doc, pages, layout) {
|
|
2115
|
+
const names = [];
|
|
2116
|
+
const added = /* @__PURE__ */ new Set();
|
|
2117
|
+
for (const dest of [...layout.destinations ?? []].sort(
|
|
2118
|
+
(a, b) => PDFHexString.fromText(a.anchor.name).asString() < PDFHexString.fromText(b.anchor.name).asString() ? -1 : 1
|
|
2119
|
+
)) {
|
|
2120
|
+
if (added.has(dest.anchor.name)) continue;
|
|
2121
|
+
const page = pages[dest.pageIndex], record = layout.pages[dest.pageIndex];
|
|
2122
|
+
if (!page || !record) continue;
|
|
2123
|
+
added.add(dest.anchor.name);
|
|
2124
|
+
names.push(
|
|
2125
|
+
PDFHexString.fromText(dest.anchor.name),
|
|
2126
|
+
doc.context.obj([
|
|
2127
|
+
page.ref,
|
|
2128
|
+
"XYZ",
|
|
2129
|
+
dest.pageStack.x - record.box.x,
|
|
2130
|
+
pageHeight(page) - dest.pageStack.y + record.box.y,
|
|
2131
|
+
null
|
|
2132
|
+
])
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
if (names.length)
|
|
2136
|
+
doc.catalog.set(PDFName3.of("Names"), doc.context.obj({ Dests: { Names: names } }));
|
|
2137
|
+
return added;
|
|
2138
|
+
}
|
|
2139
|
+
function comments(doc, pages, layout, work) {
|
|
2140
|
+
const primary = /* @__PURE__ */ new Map();
|
|
2141
|
+
const replies = [];
|
|
2142
|
+
let count = 0;
|
|
2143
|
+
for (const comment of layout.reviewArtifacts) {
|
|
2144
|
+
if (comment.kind !== "comment") continue;
|
|
2145
|
+
const occurrences = comment.occurrences.filter((o) => o.geometry?.pageStack.length);
|
|
2146
|
+
const targets = occurrences.length ? occurrences : [null];
|
|
2147
|
+
for (const occurrence of targets) {
|
|
2148
|
+
work.tick();
|
|
2149
|
+
if (++count > 5e4) throw new RangeError("PDF annotation limit exceeded");
|
|
2150
|
+
const pageIndex = occurrence?.pageIndex ?? 0;
|
|
2151
|
+
const page = pages[pageIndex], record = layout.pages[pageIndex];
|
|
2152
|
+
if (!page || !record) continue;
|
|
2153
|
+
const boxes = occurrence?.geometry?.pageStack ?? [];
|
|
2154
|
+
const quads = [];
|
|
2155
|
+
for (const b2 of boxes) {
|
|
2156
|
+
const x2 = b2.x - record.box.x, top = pageHeight(page) - b2.y + record.box.y;
|
|
2157
|
+
if (b2.width > 0)
|
|
2158
|
+
quads.push(x2, top, x2 + b2.width, top, x2, top - b2.height, x2 + b2.width, top - b2.height);
|
|
2159
|
+
}
|
|
2160
|
+
const b = boxes[0];
|
|
2161
|
+
const x = b ? b.x - record.box.x : 12;
|
|
2162
|
+
const y = b ? pageHeight(page) - b.y + record.box.y : pageHeight(page) - 12;
|
|
2163
|
+
const xs = quads.filter((_, i) => i % 2 === 0), ys = quads.filter((_, i) => i % 2 === 1);
|
|
2164
|
+
const min = (a) => a.reduce((v, n) => Math.min(v, n), Infinity);
|
|
2165
|
+
const max = (a) => a.reduce((v, n) => Math.max(v, n), -Infinity);
|
|
2166
|
+
const rect2 = quads.length ? [min(xs), min(ys), max(xs), max(ys)] : [x, y - 18, x + 18, y];
|
|
2167
|
+
const ref = add(doc, page, {
|
|
2168
|
+
Type: "Annot",
|
|
2169
|
+
Subtype: quads.length ? "Highlight" : "Text",
|
|
2170
|
+
Rect: rect2,
|
|
2171
|
+
...quads.length ? { QuadPoints: quads } : { Name: "Comment" },
|
|
2172
|
+
T: PDFHexString.fromText(comment.author),
|
|
2173
|
+
Contents: PDFHexString.fromText(
|
|
2174
|
+
`${occurrence ? "" : "[Unanchored comment]\n"}${comment.text}`
|
|
2175
|
+
),
|
|
2176
|
+
NM: PDFHexString.fromText(`comment-${comment.id}-${count}`),
|
|
2177
|
+
C: [1, 0.85, 0.2],
|
|
2178
|
+
CA: 0.25,
|
|
2179
|
+
...comment.date && Number.isFinite(Date.parse(comment.date)) ? {
|
|
2180
|
+
M: PDFHexString.fromText(
|
|
2181
|
+
`D:${new Date(comment.date).toISOString().replace(/[-:T]/g, "").slice(0, 14)}Z`
|
|
2182
|
+
)
|
|
2183
|
+
} : {},
|
|
2184
|
+
...comment.resolved ? { StateModel: "Review", State: "Completed" } : {},
|
|
2185
|
+
F: 4
|
|
2186
|
+
});
|
|
2187
|
+
if (!primary.has(comment.id)) primary.set(comment.id, ref);
|
|
2188
|
+
if (comment.parentId) replies.push({ parent: comment.parentId, ref });
|
|
2189
|
+
if (!occurrence)
|
|
2190
|
+
work.report(
|
|
2191
|
+
"unanchored-comment",
|
|
2192
|
+
"A comment without visible geometry is preserved as a first-page note",
|
|
2193
|
+
0,
|
|
2194
|
+
"information"
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
for (const reply of replies) {
|
|
2199
|
+
const parent = primary.get(reply.parent);
|
|
2200
|
+
if (parent) {
|
|
2201
|
+
const dictionary = doc.context.lookup(reply.ref);
|
|
2202
|
+
if (dictionary instanceof PDFDict) {
|
|
2203
|
+
dictionary.set(PDFName3.of("IRT"), parent);
|
|
2204
|
+
dictionary.set(PDFName3.of("RT"), PDFName3.of("R"));
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// src/images.ts
|
|
2211
|
+
import { PDFName as PDFName5, PDFString as PDFString4 } from "pdf-lib";
|
|
2212
|
+
|
|
2213
|
+
// src/vector-shapes.ts
|
|
2214
|
+
import { PDFName as PDFName4, PDFString as PDFString3 } from "pdf-lib";
|
|
2215
|
+
import { vectorShapeInkClip } from "@docx-editor.dev/core/layout";
|
|
2216
|
+
function paintVectorShape(doc, page, visit, work) {
|
|
2217
|
+
const d = visit.drawing;
|
|
2218
|
+
const shape = d.vectorShape;
|
|
2219
|
+
const painted = visit.absolutePaintBounds;
|
|
2220
|
+
const bounds = vectorShapeInkClip(d, painted);
|
|
2221
|
+
if (bounds.width <= 0 || bounds.height <= 0 || d.accessibility.hidden) return "";
|
|
2222
|
+
const content = d.geometry.contentBounds;
|
|
2223
|
+
const x = visit.drawingOrigin.x - d.x - visit.page.box.x;
|
|
2224
|
+
const y = visit.drawingOrigin.y - d.y - visit.page.box.y;
|
|
2225
|
+
const unit = 12700;
|
|
2226
|
+
const { cx, cy } = shape.extentEmu;
|
|
2227
|
+
const sx = cx > 0 ? content.width / (cx / unit) : content.height / (cy / unit);
|
|
2228
|
+
const sy = cy > 0 ? content.height / (cy / unit) : sx;
|
|
2229
|
+
const out = new Commands(work);
|
|
2230
|
+
out.push(
|
|
2231
|
+
`q ${rect(bounds, -visit.page.box.x, -visit.page.box.y, pageHeight(page))} W n`,
|
|
2232
|
+
`${number(sx)} 0 0 ${number(-sy)} ${number(content.x + x)} ${number(pageHeight(page) - content.y - y)} cm`,
|
|
2233
|
+
"0 J 0 j 4 M [] 0 d"
|
|
2234
|
+
);
|
|
2235
|
+
const path = (points, closed) => {
|
|
2236
|
+
const commands = [];
|
|
2237
|
+
for (const [index, point] of points.entries()) {
|
|
2238
|
+
work.tick();
|
|
2239
|
+
commands.push(`${number(point.x / unit)} ${number(point.y / unit)} ${index === 0 ? "m" : "l"}`);
|
|
2240
|
+
}
|
|
2241
|
+
if (closed && points.length) commands.push("h");
|
|
2242
|
+
return commands.join(" ");
|
|
2243
|
+
};
|
|
2244
|
+
for (const component of shape.components) {
|
|
2245
|
+
work.tick();
|
|
2246
|
+
const state = doc.context.register(
|
|
2247
|
+
doc.context.obj({
|
|
2248
|
+
Type: "ExtGState",
|
|
2249
|
+
ca: component.fillAlpha,
|
|
2250
|
+
CA: component.strokeAlpha
|
|
2251
|
+
})
|
|
2252
|
+
);
|
|
2253
|
+
const stateName = `ShapeAlpha${state.objectNumber}`;
|
|
2254
|
+
page.node.setExtGState(PDFName4.of(stateName), state);
|
|
2255
|
+
out.push(`q /${stateName} gs`);
|
|
2256
|
+
const inset = component.strokeInset === true && component.strokeHex !== null;
|
|
2257
|
+
const width = Math.max(1, component.strokeWidthEmu) * (inset ? 2 : 1);
|
|
2258
|
+
if (component.fillHex !== null) out.push(`${color(component.fillHex)} rg`);
|
|
2259
|
+
if (component.strokeHex !== null)
|
|
2260
|
+
out.push(`${color(component.strokeHex)} RG ${number(width / unit)} w`);
|
|
2261
|
+
const geometry = component.subpathsEmu.map((points, index) => path(points, component.subpathsClosed?.[index] !== false)).join("\n");
|
|
2262
|
+
if (inset) {
|
|
2263
|
+
out.push(`q ${geometry} W* n`, geometry, component.fillHex !== null ? "B* Q" : "S Q");
|
|
2264
|
+
} else {
|
|
2265
|
+
out.push(geometry);
|
|
2266
|
+
out.push(
|
|
2267
|
+
component.fillHex !== null ? component.strokeHex !== null ? "B*" : "f*" : component.strokeHex !== null ? "S" : "n"
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
if (component.strokeHex !== null && component.arrowheadsEmu?.length) {
|
|
2271
|
+
const arrowState = doc.context.register(
|
|
2272
|
+
doc.context.obj({
|
|
2273
|
+
Type: "ExtGState",
|
|
2274
|
+
ca: component.strokeAlpha
|
|
2275
|
+
})
|
|
2276
|
+
);
|
|
2277
|
+
const arrowName = `ShapeAlpha${arrowState.objectNumber}`;
|
|
2278
|
+
page.node.setExtGState(PDFName4.of(arrowName), arrowState);
|
|
2279
|
+
out.push(`/${arrowName} gs ${color(component.strokeHex)} rg`);
|
|
2280
|
+
for (const points of component.arrowheadsEmu) out.push(`${path(points, true)} f`);
|
|
2281
|
+
}
|
|
2282
|
+
out.push("Q");
|
|
2283
|
+
}
|
|
2284
|
+
out.push("Q");
|
|
2285
|
+
const uri = d.hyperlinkHref && /^(https?:|mailto:|tel:|ftp:)/i.test(d.hyperlinkHref) ? pdfLiteralUri(d.hyperlinkHref) : null;
|
|
2286
|
+
if (uri !== null) {
|
|
2287
|
+
const target = painted.width > 0 && painted.height > 0 ? painted : bounds;
|
|
2288
|
+
const left = target.x - visit.page.box.x;
|
|
2289
|
+
const bottom = pageHeight(page) - target.y + visit.page.box.y - target.height;
|
|
2290
|
+
page.node.addAnnot(
|
|
2291
|
+
doc.context.register(
|
|
2292
|
+
doc.context.obj({
|
|
2293
|
+
Type: "Annot",
|
|
2294
|
+
Subtype: "Link",
|
|
2295
|
+
Rect: [left, bottom, left + target.width, bottom + target.height],
|
|
2296
|
+
Border: [0, 0, 0],
|
|
2297
|
+
A: { S: "URI", URI: PDFString3.of(uri) }
|
|
2298
|
+
})
|
|
2299
|
+
)
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
return out.join("\n");
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// src/images.ts
|
|
2306
|
+
var EMBEDDABLE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg"]);
|
|
2307
|
+
function unpaintableBulletReason(picture, box) {
|
|
2308
|
+
const resource = picture.resource;
|
|
2309
|
+
if (resource.kind === "unrenderable") return `image refused: ${resource.reason}`;
|
|
2310
|
+
if (resource.kind !== "ready") return `image ${resource.kind}`;
|
|
2311
|
+
if (!EMBEDDABLE_MIMES.has(resource.mime)) return `format not embeddable: ${resource.mime}`;
|
|
2312
|
+
if (!(box.width > 0 && box.height > 0)) return "no visible area";
|
|
2313
|
+
return null;
|
|
2314
|
+
}
|
|
2315
|
+
var ImageWriter = class {
|
|
2316
|
+
constructor(doc, session, work) {
|
|
2317
|
+
this.doc = doc;
|
|
2318
|
+
this.session = session;
|
|
2319
|
+
this.work = work;
|
|
2320
|
+
}
|
|
2321
|
+
doc;
|
|
2322
|
+
session;
|
|
2323
|
+
work;
|
|
2324
|
+
images = /* @__PURE__ */ new Map();
|
|
2325
|
+
/**
|
|
2326
|
+
* Draw a list marker's picture bullet in the box layout published for it.
|
|
2327
|
+
*
|
|
2328
|
+
* Returns '' for anything not drawable — missing, external, still decoding, refused, or a
|
|
2329
|
+
* format this writer cannot embed — and the caller then paints the level's `w:lvlText`.
|
|
2330
|
+
*
|
|
2331
|
+
* That fall back is FOLLOWING the document, not approximating it: the same `w:lvl` authors
|
|
2332
|
+
* `w:lvlText` as this level's marker and `w:lvlPicBulletId` as a picture to use in its
|
|
2333
|
+
* place (ECMA-376 §17.9.12), so the text is the document's own alternative rather than
|
|
2334
|
+
* something this writer invented. It is reported all the same, at `information`: nothing
|
|
2335
|
+
* the document authors is lost from the page, so a strict export is still faithful, while
|
|
2336
|
+
* the reader is still told which of the two markers was drawn. An inline `w:drawing` whose
|
|
2337
|
+
* format cannot be embedded keeps reporting `unsupported`, because there the document
|
|
2338
|
+
* offers no alternative and the page really does lose content.
|
|
2339
|
+
*
|
|
2340
|
+
* No scaling decision is taken here: the box is the extent layout already placed on the
|
|
2341
|
+
* baseline, after the marker font scale.
|
|
2342
|
+
*/
|
|
2343
|
+
async paintListMarkerPicture(picture, box, page, pageIndex) {
|
|
2344
|
+
await this.work.yield();
|
|
2345
|
+
const unpaintable = unpaintableBulletReason(picture, box);
|
|
2346
|
+
if (unpaintable !== null) {
|
|
2347
|
+
this.work.report(
|
|
2348
|
+
"list-picture-bullet",
|
|
2349
|
+
`Picture bullet drawn as its level text (${unpaintable})`,
|
|
2350
|
+
pageIndex,
|
|
2351
|
+
"information"
|
|
2352
|
+
);
|
|
2353
|
+
return "";
|
|
2354
|
+
}
|
|
2355
|
+
if (picture.resource.kind !== "ready") return "";
|
|
2356
|
+
const mime = picture.resource.mime;
|
|
2357
|
+
const bytes = this.session.validatedImageBytes(picture);
|
|
2358
|
+
if (!bytes) return "";
|
|
2359
|
+
let image = this.images.get(picture.resource.resourceKey);
|
|
2360
|
+
if (!image) {
|
|
2361
|
+
image = mime === "image/png" ? await this.doc.embedPng(bytes) : await this.doc.embedJpg(bytes);
|
|
2362
|
+
this.images.set(picture.resource.resourceKey, image);
|
|
2363
|
+
}
|
|
2364
|
+
const key = `Im${image.ref.objectNumber}`;
|
|
2365
|
+
page.node.setXObject(PDFName5.of(key), image.ref);
|
|
2366
|
+
const x = box.x;
|
|
2367
|
+
const y = pageHeight(page) - box.y - box.height;
|
|
2368
|
+
return `q ${number(box.width)} 0 0 ${number(box.height)} ${number(x)} ${number(y)} cm /${key} Do Q`;
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* A textbox: its fill and outline at the extent, then `body` (the story's decorations and
|
|
2372
|
+
* the text its owner collected) clipped to the content box, all clipped to the published
|
|
2373
|
+
* paint bounds — the same three boxes the screen painter draws, in the same order.
|
|
2374
|
+
*
|
|
2375
|
+
* The outline is stroked inside the extent, as a CSS border inside a `border-box` is, so a
|
|
2376
|
+
* bordered textbox is exactly as wide as Word laid it out. Word clips overflowing content
|
|
2377
|
+
* to the box as well, so a height clip is information: the page shows what Word shows.
|
|
2378
|
+
*/
|
|
2379
|
+
paintTextbox(visit, page, body) {
|
|
2380
|
+
const d = visit.drawing;
|
|
2381
|
+
if (d.kind !== "anchoredDrawing" || !d.textboxStory) return "";
|
|
2382
|
+
const story = d.textboxStory;
|
|
2383
|
+
const bounds = visit.absolutePaintBounds;
|
|
2384
|
+
if (bounds.width <= 0 || bounds.height <= 0 || d.accessibility.hidden) return "";
|
|
2385
|
+
if (story.fallbackReason === "textbox-height-clip")
|
|
2386
|
+
this.work.report(
|
|
2387
|
+
"textbox-clip",
|
|
2388
|
+
"Textbox content taller than its box is clipped",
|
|
2389
|
+
visit.page.index,
|
|
2390
|
+
"information"
|
|
2391
|
+
);
|
|
2392
|
+
else if (story.fallbackReason) {
|
|
2393
|
+
this.work.report(
|
|
2394
|
+
"textbox",
|
|
2395
|
+
`Textbox story not laid out: ${story.fallbackReason}`,
|
|
2396
|
+
visit.page.index
|
|
2397
|
+
);
|
|
2398
|
+
return "";
|
|
2399
|
+
}
|
|
2400
|
+
const height = pageHeight(page);
|
|
2401
|
+
const x = visit.drawingOrigin.x - visit.page.box.x;
|
|
2402
|
+
const y = visit.drawingOrigin.y - visit.page.box.y;
|
|
2403
|
+
const extent = { x, y, width: d.width, height: d.height };
|
|
2404
|
+
const out = new Commands(this.work);
|
|
2405
|
+
out.push(`q ${rect(bounds, -visit.page.box.x, -visit.page.box.y, height)} W n`);
|
|
2406
|
+
if (story.fillHex !== null)
|
|
2407
|
+
out.push(`${color(story.fillHex)} rg ${rect(extent, 0, 0, height)} f`);
|
|
2408
|
+
if (story.strokeHex !== null) {
|
|
2409
|
+
const width = Math.max(0.5, story.strokeWidthPt);
|
|
2410
|
+
const inner = {
|
|
2411
|
+
x: x + width / 2,
|
|
2412
|
+
y: y + width / 2,
|
|
2413
|
+
width: Math.max(0, d.width - width),
|
|
2414
|
+
height: Math.max(0, d.height - width)
|
|
2415
|
+
};
|
|
2416
|
+
out.push(`${color(story.strokeHex)} RG ${number(width)} w ${rect(inner, 0, 0, height)} S`);
|
|
2417
|
+
}
|
|
2418
|
+
const content = {
|
|
2419
|
+
x: x + story.contentOffset.x,
|
|
2420
|
+
y: y + story.contentOffset.y,
|
|
2421
|
+
width: story.contentWidth,
|
|
2422
|
+
height: Math.max(0, story.contentHeight)
|
|
2423
|
+
};
|
|
2424
|
+
out.push(`q ${rect(content, 0, 0, height)} W n`, ...body, "Q", "Q");
|
|
2425
|
+
return out.join("\n");
|
|
2426
|
+
}
|
|
2427
|
+
async paint(visit, page) {
|
|
2428
|
+
await this.work.yield();
|
|
2429
|
+
const d = visit.drawing;
|
|
2430
|
+
if (d.accessibility.hidden) return "";
|
|
2431
|
+
const report = (message) => {
|
|
2432
|
+
this.work.report("drawing", message, visit.page.index);
|
|
2433
|
+
return "";
|
|
2434
|
+
};
|
|
2435
|
+
if (d.groupPicture) return this.paintGroupPicture(visit, page, report);
|
|
2436
|
+
if (d.vectorShape) return paintVectorShape(this.doc, page, visit, this.work);
|
|
2437
|
+
if (d.kind === "anchoredDrawing" && d.textboxStory)
|
|
2438
|
+
return report("Textbox story not routed through paintTextbox");
|
|
2439
|
+
if (d.placeholderGraphicKind) return report(`Unsupported drawing: ${d.placeholderGraphicKind}`);
|
|
2440
|
+
return this.paintPicture(visit, page, report);
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* A shape group with a picture member: the picture, then the group's vector members over it.
|
|
2444
|
+
*
|
|
2445
|
+
* The group paints whole or not at all, as it does on screen. Layout already drops an
|
|
2446
|
+
* `mc:AlternateContent` group whose picture resource fails, following the rule for MC
|
|
2447
|
+
* payloads the engine cannot draw. A ready picture in a format this writer cannot embed
|
|
2448
|
+
* gets the same rule here: the MC group is left out and reported at `information`, because
|
|
2449
|
+
* the editor draws it and only this output cannot. A bare group reports every refusal as
|
|
2450
|
+
* `unsupported`, as it reports an unsupported group.
|
|
2451
|
+
*/
|
|
2452
|
+
async paintGroupPicture(visit, page, report) {
|
|
2453
|
+
const d = visit.drawing;
|
|
2454
|
+
const { resource } = d;
|
|
2455
|
+
if (d.groupPicture?.alternateContent && resource.kind === "ready" && !EMBEDDABLE_MIMES.has(resource.mime)) {
|
|
2456
|
+
this.work.report(
|
|
2457
|
+
"drawing",
|
|
2458
|
+
`Shape group left out: picture format not embeddable: ${resource.mime}`,
|
|
2459
|
+
visit.page.index,
|
|
2460
|
+
"information"
|
|
2461
|
+
);
|
|
2462
|
+
return "";
|
|
2463
|
+
}
|
|
2464
|
+
let refused = false;
|
|
2465
|
+
const picture = await this.paintPicture(visit, page, (message) => {
|
|
2466
|
+
refused = true;
|
|
2467
|
+
return report(message);
|
|
2468
|
+
});
|
|
2469
|
+
if (refused) return "";
|
|
2470
|
+
const vector = d.vectorShape ? paintVectorShape(this.doc, page, visit, this.work) : "";
|
|
2471
|
+
return [picture, vector].filter((commands) => commands !== "").join("\n");
|
|
2472
|
+
}
|
|
2473
|
+
async paintPicture(visit, page, report) {
|
|
2474
|
+
const d = visit.drawing;
|
|
2475
|
+
const bytes = this.session.validatedImageBytes(d);
|
|
2476
|
+
if (!bytes || d.resource.kind !== "ready") return report("Image has no validated raster bytes");
|
|
2477
|
+
const mime = d.resource.mime;
|
|
2478
|
+
if (!EMBEDDABLE_MIMES.has(mime)) return report(`Unsupported PDF image format: ${mime}`);
|
|
2479
|
+
if (d.effects.grayscale || d.effects.brightness || d.effects.contrast || d.effects.bilevel !== void 0)
|
|
2480
|
+
this.work.report(
|
|
2481
|
+
"image-effects",
|
|
2482
|
+
"Image color adjustments are not encoded",
|
|
2483
|
+
visit.page.index
|
|
2484
|
+
);
|
|
2485
|
+
let image = this.images.get(d.resource.resourceKey);
|
|
2486
|
+
if (!image) {
|
|
2487
|
+
image = mime === "image/png" ? await this.doc.embedPng(bytes) : await this.doc.embedJpg(bytes);
|
|
2488
|
+
this.images.set(d.resource.resourceKey, image);
|
|
2489
|
+
}
|
|
2490
|
+
let opacityCommand = "";
|
|
2491
|
+
if (d.effects.opacity !== void 0 && d.effects.opacity !== 1) {
|
|
2492
|
+
const opacity = d.effects.opacity;
|
|
2493
|
+
if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1)
|
|
2494
|
+
return report("Invalid image opacity");
|
|
2495
|
+
const name = `Alpha${Math.round(opacity * 1e5)}`;
|
|
2496
|
+
const ref = this.doc.context.register(
|
|
2497
|
+
this.doc.context.obj({ Type: "ExtGState", ca: opacity, CA: opacity })
|
|
2498
|
+
);
|
|
2499
|
+
page.node.setExtGState(PDFName5.of(name), ref);
|
|
2500
|
+
opacityCommand = `/${name} gs `;
|
|
2501
|
+
}
|
|
2502
|
+
const key = `Im${image.ref.objectNumber}`;
|
|
2503
|
+
page.node.setXObject(PDFName5.of(key), image.ref);
|
|
2504
|
+
const points = d.geometry.imageTransformCorners ?? d.geometry.transformedCorners;
|
|
2505
|
+
if (points.length !== 4) return report("Image has no affine rectangle geometry");
|
|
2506
|
+
if (d.geometry.clipFallback !== "none")
|
|
2507
|
+
this.work.report("image-clip", "Image clipping uses a Core fallback", visit.page.index);
|
|
2508
|
+
const offsetX = visit.drawingOrigin.x - d.x - visit.page.box.x;
|
|
2509
|
+
const offsetY = visit.drawingOrigin.y - d.y - visit.page.box.y;
|
|
2510
|
+
const p = points.map((v) => ({ x: v.x + offsetX, y: pageHeight(page) - v.y - offsetY }));
|
|
2511
|
+
const topLeft = p[0], topRight = p[1], bottomLeft = p[3];
|
|
2512
|
+
const ax = topRight.x - topLeft.x, ay = topRight.y - topLeft.y;
|
|
2513
|
+
const bx = topLeft.x - bottomLeft.x, by = topLeft.y - bottomLeft.y;
|
|
2514
|
+
const crop = d.crop;
|
|
2515
|
+
const width = 1 - crop.left - crop.right, height = 1 - crop.top - crop.bottom;
|
|
2516
|
+
if (!(width > 0 && height > 0)) return report("Image crop has no visible area");
|
|
2517
|
+
if (d.geometry.clipPolygon && d.geometry.clipPolygon.length === 0) return "";
|
|
2518
|
+
const clip = d.geometry.clipPolygon?.length ? d.geometry.clipPolygon.map((v) => ({
|
|
2519
|
+
x: v.x + offsetX,
|
|
2520
|
+
y: pageHeight(page) - v.y - offsetY
|
|
2521
|
+
})) : p;
|
|
2522
|
+
const uri = d.hyperlinkHref && /^(https?:|mailto:|tel:|ftp:)/i.test(d.hyperlinkHref) ? pdfLiteralUri(d.hyperlinkHref) : null;
|
|
2523
|
+
if (uri !== null) {
|
|
2524
|
+
const bounds = [Infinity, Infinity, -Infinity, -Infinity];
|
|
2525
|
+
for (const point of clip) {
|
|
2526
|
+
bounds[0] = Math.min(bounds[0], point.x);
|
|
2527
|
+
bounds[1] = Math.min(bounds[1], point.y);
|
|
2528
|
+
bounds[2] = Math.max(bounds[2], point.x);
|
|
2529
|
+
bounds[3] = Math.max(bounds[3], point.y);
|
|
2530
|
+
}
|
|
2531
|
+
page.node.addAnnot(
|
|
2532
|
+
this.doc.context.register(
|
|
2533
|
+
this.doc.context.obj({
|
|
2534
|
+
Type: "Annot",
|
|
2535
|
+
Subtype: "Link",
|
|
2536
|
+
Rect: bounds,
|
|
2537
|
+
Border: [0, 0, 0],
|
|
2538
|
+
A: { S: "URI", URI: PDFString4.of(uri) }
|
|
2539
|
+
})
|
|
2540
|
+
)
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
const path = clip.map((v, i) => `${number(v.x)} ${number(v.y)} ${i === 0 ? "m" : "l"}`).join(" ");
|
|
2544
|
+
return `q ${opacityCommand}${path} h W n ${number(ax / width)} ${number(ay / width)} ${number(bx / height)} ${number(by / height)} ${number(bottomLeft.x - ax * crop.left / width - bx * crop.bottom / height)} ${number(bottomLeft.y - ay * crop.left / width - by * crop.bottom / height)} cm /${key} Do Q`;
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
|
|
2548
|
+
// src/equations.ts
|
|
2549
|
+
function paintEquation(visit, page, text, work) {
|
|
2550
|
+
const equation = visit.span.equation;
|
|
2551
|
+
if (equation.truncated)
|
|
2552
|
+
work.report("equation-truncated", "Core truncated an equation", visit.page.index);
|
|
2553
|
+
const top = visit.storyOrigin.y + visit.line.box.y + visit.line.baseline - equation.geometry.baseline;
|
|
2554
|
+
const out = [];
|
|
2555
|
+
function node(g, parentX, parentY) {
|
|
2556
|
+
work.tick();
|
|
2557
|
+
const x = parentX + g.box.x, y = parentY + g.box.y;
|
|
2558
|
+
if (g.kind === "text" || g.kind === "fallback") {
|
|
2559
|
+
if (g.kind === "fallback")
|
|
2560
|
+
work.report("equation-fallback", "Core uses fallback equation text", visit.page.index);
|
|
2561
|
+
out.push(
|
|
2562
|
+
text.paint(
|
|
2563
|
+
{
|
|
2564
|
+
...visit,
|
|
2565
|
+
storyOrigin: { x: 0, y: 0 },
|
|
2566
|
+
line: { ...visit.line, box: { ...visit.line.box, y }, baseline: g.baseline },
|
|
2567
|
+
absoluteBox: { ...g.box, x, y },
|
|
2568
|
+
span: {
|
|
2569
|
+
...visit.span,
|
|
2570
|
+
text: g.text,
|
|
2571
|
+
equation: void 0,
|
|
2572
|
+
glyphOffsetPt: 0,
|
|
2573
|
+
style: {
|
|
2574
|
+
...visit.span.style,
|
|
2575
|
+
fontFamily: "Cambria Math",
|
|
2576
|
+
fontFamilyEastAsia: "Cambria Math",
|
|
2577
|
+
fontSizePt: g.fontSizePt,
|
|
2578
|
+
verticalAlign: "baseline",
|
|
2579
|
+
baselineShiftPt: 0
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
},
|
|
2583
|
+
page
|
|
2584
|
+
)
|
|
2585
|
+
);
|
|
2586
|
+
} else if (g.kind === "row") for (const child of g.items) node(child, x, y);
|
|
2587
|
+
else if (g.kind === "fraction" || g.kind === "radical") {
|
|
2588
|
+
out.push(
|
|
2589
|
+
`${color(visit.span.style.color)} rg ${rect(g.bar, x - visit.page.box.x, y - visit.page.box.y, pageHeight(page), true)} f`
|
|
2590
|
+
);
|
|
2591
|
+
if (g.kind === "fraction") {
|
|
2592
|
+
node(g.numerator, x, y);
|
|
2593
|
+
node(g.denominator, x, y);
|
|
2594
|
+
} else {
|
|
2595
|
+
node(g.sign, x, y);
|
|
2596
|
+
node(g.radicand, x, y);
|
|
2597
|
+
if (g.degree) node(g.degree, x, y);
|
|
2598
|
+
}
|
|
2599
|
+
} else if (g.kind === "script") {
|
|
2600
|
+
node(g.base, x, y);
|
|
2601
|
+
if (g.subscript) node(g.subscript, x, y);
|
|
2602
|
+
if (g.superscript) node(g.superscript, x, y);
|
|
2603
|
+
} else if (g.kind === "nary") {
|
|
2604
|
+
node(g.operator, x, y);
|
|
2605
|
+
node(g.body, x, y);
|
|
2606
|
+
if (g.lowerLimit) node(g.lowerLimit, x, y);
|
|
2607
|
+
if (g.upperLimit) node(g.upperLimit, x, y);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
node(equation.geometry, visit.absoluteBox.x, top);
|
|
2611
|
+
return out.join("\n");
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// src/paint.ts
|
|
2615
|
+
function rule(box, colorHex, style, x, y, height) {
|
|
2616
|
+
if (style === "double") {
|
|
2617
|
+
const horizontal2 = box.width >= box.height;
|
|
2618
|
+
const third = (horizontal2 ? box.height : box.width) / 3;
|
|
2619
|
+
return [0, 2 * third].map(
|
|
2620
|
+
(offset) => rule(
|
|
2621
|
+
{
|
|
2622
|
+
...box,
|
|
2623
|
+
x: box.x + (horizontal2 ? 0 : offset),
|
|
2624
|
+
y: box.y + (horizontal2 ? offset : 0),
|
|
2625
|
+
width: horizontal2 ? box.width : third,
|
|
2626
|
+
height: horizontal2 ? third : box.height
|
|
2627
|
+
},
|
|
2628
|
+
colorHex,
|
|
2629
|
+
"single",
|
|
2630
|
+
x,
|
|
2631
|
+
y,
|
|
2632
|
+
height
|
|
2633
|
+
)
|
|
2634
|
+
).join("\n");
|
|
2635
|
+
}
|
|
2636
|
+
if (style === "solid" || style === "single" || style === "thick")
|
|
2637
|
+
return `${color(colorHex)} rg ${rect(box, x, y, height, true)} f`;
|
|
2638
|
+
const horizontal = box.width >= box.height;
|
|
2639
|
+
const width = horizontal ? box.height : box.width;
|
|
2640
|
+
const sx = box.x + x + (horizontal ? 0 : width / 2), sy = height - box.y - y - (horizontal ? width / 2 : 0);
|
|
2641
|
+
return `${color(colorHex)} RG ${number(width)} w [${number(style === "dotted" ? width : width * 3)} ${number(width * 2)}] 0 d ${number(sx)} ${number(sy)} m ${number(sx + (horizontal ? box.width : 0))} ${number(sy - (horizontal ? 0 : box.height))} l S [] 0 d`;
|
|
2642
|
+
}
|
|
2643
|
+
function rotatedCellMatrix(cell, x, y, height) {
|
|
2644
|
+
const left = x + cell.x;
|
|
2645
|
+
const top = y + cell.y;
|
|
2646
|
+
return `0 1 -1 0 ${number(left - top + height)} ${number(height - top - cell.height - left)} cm`;
|
|
2647
|
+
}
|
|
2648
|
+
function decorations(blocks, x, y, page, work, pageIndex) {
|
|
2649
|
+
const out = [];
|
|
2650
|
+
for (const block of blocks) {
|
|
2651
|
+
work.tick();
|
|
2652
|
+
if (block.kind === "paragraph") {
|
|
2653
|
+
if (block.shading && block.shadingBox)
|
|
2654
|
+
out.push(
|
|
2655
|
+
`${color(block.shading)} rg ${rect(block.shadingBox, x, y, pageHeight(page), true)} f`
|
|
2656
|
+
);
|
|
2657
|
+
for (const border of block.borders ?? (block.bottomBorder ? [{ ...block.bottomBorder, side: "bottom" }] : [])) {
|
|
2658
|
+
const style = border.edge.val;
|
|
2659
|
+
if (border.edge.shadow)
|
|
2660
|
+
work.report("border-shadow", "Paragraph border shadow is not encoded", pageIndex);
|
|
2661
|
+
if (!["single", "thick", "dashed", "dotted", "double"].includes(style))
|
|
2662
|
+
work.report(
|
|
2663
|
+
"paragraph-border-style",
|
|
2664
|
+
`Unsupported paragraph border: ${style}`,
|
|
2665
|
+
pageIndex
|
|
2666
|
+
);
|
|
2667
|
+
out.push(rule(border.box, border.edge.color, style, x, y, pageHeight(page)));
|
|
2668
|
+
}
|
|
2669
|
+
} else {
|
|
2670
|
+
for (const row of block.rows)
|
|
2671
|
+
for (const cell of row.cells) {
|
|
2672
|
+
if (cell.paintInert || cell.vMergeContinue) continue;
|
|
2673
|
+
if (cell.shading)
|
|
2674
|
+
out.push(`${color(cell.shading)} rg ${rect(cell.box, x, y, pageHeight(page), true)} f`);
|
|
2675
|
+
const inner = decorations(cell.blocks, x, y, page, work, pageIndex);
|
|
2676
|
+
if (cell.textDirection && inner.length)
|
|
2677
|
+
out.push("q", rotatedCellMatrix(cell.box, x, y, pageHeight(page)), ...inner, "Q");
|
|
2678
|
+
else out.push(...inner);
|
|
2679
|
+
}
|
|
2680
|
+
for (const row of block.rows)
|
|
2681
|
+
for (const cell of row.cells) {
|
|
2682
|
+
if (cell.paintInert || cell.vMergeContinue) continue;
|
|
2683
|
+
for (const stroke of cell.borders?.strokes ?? [])
|
|
2684
|
+
out.push(
|
|
2685
|
+
rule(
|
|
2686
|
+
stroke,
|
|
2687
|
+
stroke.color,
|
|
2688
|
+
stroke.cssStyle,
|
|
2689
|
+
x + cell.box.x,
|
|
2690
|
+
y + cell.box.y,
|
|
2691
|
+
pageHeight(page)
|
|
2692
|
+
)
|
|
2693
|
+
);
|
|
2694
|
+
const publishedSides = new Set(
|
|
2695
|
+
(cell.borders?.strokes ?? []).map((stroke) => stroke.side)
|
|
2696
|
+
);
|
|
2697
|
+
for (const side of ["top", "right", "bottom", "left"]) {
|
|
2698
|
+
const edge = cell.borders?.[side];
|
|
2699
|
+
if (!edge || publishedSides.has(side)) continue;
|
|
2700
|
+
const b = cell.box, width = edge.widthPt;
|
|
2701
|
+
const box = {
|
|
2702
|
+
x: b.x + (side === "right" ? b.width - width : 0),
|
|
2703
|
+
y: b.y + (side === "bottom" ? b.height - width : 0),
|
|
2704
|
+
width: side === "left" || side === "right" ? width : b.width,
|
|
2705
|
+
height: side === "top" || side === "bottom" ? width : b.height
|
|
2706
|
+
};
|
|
2707
|
+
out.push(rule(box, edge.color, edge.style, x, y, pageHeight(page)));
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return out;
|
|
2713
|
+
}
|
|
2714
|
+
var HIGHLIGHTS = {
|
|
2715
|
+
yellow: "FFFF00",
|
|
2716
|
+
green: "00FF00",
|
|
2717
|
+
cyan: "00FFFF",
|
|
2718
|
+
magenta: "FF00FF",
|
|
2719
|
+
blue: "0000FF",
|
|
2720
|
+
red: "FF0000",
|
|
2721
|
+
darkBlue: "000080",
|
|
2722
|
+
darkCyan: "008080",
|
|
2723
|
+
darkGreen: "008000",
|
|
2724
|
+
darkMagenta: "800080",
|
|
2725
|
+
darkRed: "800000",
|
|
2726
|
+
darkYellow: "808000",
|
|
2727
|
+
darkGray: "808080",
|
|
2728
|
+
lightGray: "C0C0C0",
|
|
2729
|
+
black: "000000",
|
|
2730
|
+
white: "FFFFFF"
|
|
2731
|
+
};
|
|
2732
|
+
var PAGE_GRID_PT = 0.24;
|
|
2733
|
+
function onDeviceGrid(value) {
|
|
2734
|
+
return Number((Math.round(value / PAGE_GRID_PT) * PAGE_GRID_PT).toFixed(6));
|
|
2735
|
+
}
|
|
2736
|
+
async function paint(doc, session, layout, work, includeComments) {
|
|
2737
|
+
const pages = layout.pages.map((p) => {
|
|
2738
|
+
if (p.box.width <= 0 || p.box.height <= 0 || p.box.width > 14400 || p.box.height > 14400)
|
|
2739
|
+
throw new RangeError("Invalid PDF page dimensions");
|
|
2740
|
+
return doc.addPage([onDeviceGrid(p.box.width), onDeviceGrid(p.box.height)]);
|
|
2741
|
+
});
|
|
2742
|
+
for (const artifact of layout.reviewArtifacts) {
|
|
2743
|
+
if (layout.displayMode === "all-markup" && artifact.kind === "tracked-change" && !["insert", "delete", "replace"].includes(artifact.change))
|
|
2744
|
+
work.report("review-presentation", `Unsupported revision presentation: ${artifact.change}`);
|
|
2745
|
+
}
|
|
2746
|
+
const text = new TextWriter(doc, session, work, layout.displayMode === "all-markup");
|
|
2747
|
+
const images = new ImageWriter(doc, session, work);
|
|
2748
|
+
const names = destinations(doc, pages, layout);
|
|
2749
|
+
const behindStreams = pages.map(() => new Commands(work));
|
|
2750
|
+
const streams = pages.map(() => new Commands(work));
|
|
2751
|
+
const frontBorders = pages.map(() => new Commands(work));
|
|
2752
|
+
for (const record of layout.pages) {
|
|
2753
|
+
const out = streams[record.index];
|
|
2754
|
+
const height = pageHeight(pages[record.index]);
|
|
2755
|
+
if (record.pageBorders) {
|
|
2756
|
+
const borderOut = record.pageBorders.zOrder === "front" ? frontBorders[record.index] : out;
|
|
2757
|
+
for (const border of record.pageBorders.strokes) {
|
|
2758
|
+
if (!["single", "thick", "dashed", "dotted", "double"].includes(border.edge.val) || border.edge.shadow)
|
|
2759
|
+
work.report(
|
|
2760
|
+
"page-border-style",
|
|
2761
|
+
`Unsupported page border: ${border.edge.val}`,
|
|
2762
|
+
record.index
|
|
2763
|
+
);
|
|
2764
|
+
borderOut.push(rule(border.box, border.edge.color, border.edge.val, 0, 0, height));
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
for (const separator of record.columnSeparators ?? [])
|
|
2768
|
+
out.push(
|
|
2769
|
+
`0 0 0 rg ${rect(separator, record.contentBox.x - record.box.x, record.contentBox.y - record.box.y, height, true)} f`
|
|
2770
|
+
);
|
|
2771
|
+
for (const area of [record.footnotes, record.endnotes]) {
|
|
2772
|
+
if (area?.fallbackReason === "note-continuation-notice-height-cap")
|
|
2773
|
+
work.report(
|
|
2774
|
+
area.fallbackReason,
|
|
2775
|
+
"Continuation notice exceeds the available page height",
|
|
2776
|
+
record.index
|
|
2777
|
+
);
|
|
2778
|
+
const sep = area?.separator;
|
|
2779
|
+
if (!sep || !(sep.ruleStyle || sep.synthetic)) continue;
|
|
2780
|
+
for (const offset of sep.ruleStyle === "double" ? [0, 2] : [0])
|
|
2781
|
+
out.push(
|
|
2782
|
+
`${color(sep.ruleColor)} rg ${rect({ ...sep.box, y: sep.box.y + offset, height: sep.ruleStyle === "double" ? 0.75 : sep.box.height }, -record.box.x, -record.box.y, height, true)} f`
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
const spans = [];
|
|
2787
|
+
const drawings = [];
|
|
2788
|
+
for (const warning of layout.contentWarnings ?? [])
|
|
2789
|
+
work.report(`core-${warning.code}`, warning.code);
|
|
2790
|
+
forEachSemanticSpan(layout, (visit) => {
|
|
2791
|
+
work.tick();
|
|
2792
|
+
spans.push(visit);
|
|
2793
|
+
});
|
|
2794
|
+
forEachSemanticDrawing(layout, (visit) => {
|
|
2795
|
+
work.tick();
|
|
2796
|
+
drawings.push(visit);
|
|
2797
|
+
});
|
|
2798
|
+
forEachSemanticStory(layout, (root) => {
|
|
2799
|
+
streams[root.page.index].push(
|
|
2800
|
+
...decorations(
|
|
2801
|
+
root.host.fragments,
|
|
2802
|
+
root.origin.x - root.page.box.x,
|
|
2803
|
+
root.origin.y - root.page.box.y,
|
|
2804
|
+
pages[root.page.index],
|
|
2805
|
+
work,
|
|
2806
|
+
root.page.index
|
|
2807
|
+
)
|
|
2808
|
+
);
|
|
2809
|
+
});
|
|
2810
|
+
drawings.sort(
|
|
2811
|
+
(a, b) => (a.drawing.kind === "anchoredDrawing" ? a.drawing.relativeHeight : 0) - (b.drawing.kind === "anchoredDrawing" ? b.drawing.relativeHeight : 0)
|
|
2812
|
+
);
|
|
2813
|
+
const textboxBuffers = /* @__PURE__ */ new Map();
|
|
2814
|
+
const rotatedCellOf = /* @__PURE__ */ new Map();
|
|
2815
|
+
const markRotated = (blocks, cell) => {
|
|
2816
|
+
for (const block of blocks) {
|
|
2817
|
+
work.tick();
|
|
2818
|
+
if (block.kind === "paragraph") {
|
|
2819
|
+
if (cell) rotatedCellOf.set(block, cell);
|
|
2820
|
+
continue;
|
|
2821
|
+
}
|
|
2822
|
+
for (const row of block.rows)
|
|
2823
|
+
for (const inner of row.cells)
|
|
2824
|
+
markRotated(inner.blocks, cell ?? (inner.textDirection ? inner : void 0));
|
|
2825
|
+
}
|
|
2826
|
+
};
|
|
2827
|
+
forEachSemanticStory(layout, (root) => markRotated(root.host.fragments));
|
|
2828
|
+
const rotatedBuffers = /* @__PURE__ */ new Map();
|
|
2829
|
+
const outFor = (visit) => {
|
|
2830
|
+
const cell = visit.paragraph && rotatedCellOf.get(visit.paragraph);
|
|
2831
|
+
if (cell && visit.storyOrigin) {
|
|
2832
|
+
const key = `${visit.page.index}:${cell.id}`;
|
|
2833
|
+
let entry = rotatedBuffers.get(key);
|
|
2834
|
+
if (!entry) {
|
|
2835
|
+
entry = {
|
|
2836
|
+
cell,
|
|
2837
|
+
x: visit.storyOrigin.x - visit.page.box.x,
|
|
2838
|
+
y: visit.storyOrigin.y - visit.page.box.y,
|
|
2839
|
+
page: visit.page.index,
|
|
2840
|
+
commands: new Commands(work),
|
|
2841
|
+
behind: new Commands(work)
|
|
2842
|
+
};
|
|
2843
|
+
rotatedBuffers.set(key, entry);
|
|
2844
|
+
}
|
|
2845
|
+
return entry.commands;
|
|
2846
|
+
}
|
|
2847
|
+
if (visit.story !== "textbox" || !visit.textboxOwner) return streams[visit.page.index];
|
|
2848
|
+
let owners = textboxBuffers.get(visit.page.index);
|
|
2849
|
+
if (!owners) {
|
|
2850
|
+
owners = /* @__PURE__ */ new Map();
|
|
2851
|
+
textboxBuffers.set(visit.page.index, owners);
|
|
2852
|
+
}
|
|
2853
|
+
let buffer = owners.get(visit.textboxOwner);
|
|
2854
|
+
if (!buffer) {
|
|
2855
|
+
buffer = new Commands(work);
|
|
2856
|
+
owners.set(visit.textboxOwner, buffer);
|
|
2857
|
+
}
|
|
2858
|
+
return buffer;
|
|
2859
|
+
};
|
|
2860
|
+
const pageGroups = /* @__PURE__ */ new Map();
|
|
2861
|
+
for (const visit of spans) {
|
|
2862
|
+
let lines = pageGroups.get(visit.page.index);
|
|
2863
|
+
if (!lines) {
|
|
2864
|
+
lines = /* @__PURE__ */ new Map();
|
|
2865
|
+
pageGroups.set(visit.page.index, lines);
|
|
2866
|
+
}
|
|
2867
|
+
let group = lines.get(visit.line);
|
|
2868
|
+
if (!group) {
|
|
2869
|
+
group = [];
|
|
2870
|
+
lines.set(visit.line, group);
|
|
2871
|
+
}
|
|
2872
|
+
group.push(visit);
|
|
2873
|
+
}
|
|
2874
|
+
const lineStarts = /* @__PURE__ */ new Map();
|
|
2875
|
+
spans.length = 0;
|
|
2876
|
+
for (const lines of pageGroups.values())
|
|
2877
|
+
for (const group of lines.values()) {
|
|
2878
|
+
const first = group[0];
|
|
2879
|
+
const markerText = first.paragraph.lines[0] === first.line ? first.paragraph.marker?.text : void 0;
|
|
2880
|
+
const logical = (markerText ? markerText + " " : "") + group.map((v) => v.span.equation?.fallbackText ?? v.span.text).join("");
|
|
2881
|
+
const rightToLeft = group.some((v) => (v.span.style.shaping?.level ?? 0) % 2 === 1);
|
|
2882
|
+
group.sort((a, b) => a.absoluteBox.x - b.absoluteBox.x);
|
|
2883
|
+
lineStarts.set(group[0], rightToLeft ? null : logical);
|
|
2884
|
+
for (const visit of group) spans.push(visit);
|
|
2885
|
+
}
|
|
2886
|
+
let activeOut;
|
|
2887
|
+
let lineOut;
|
|
2888
|
+
const placeOutlines = () => {
|
|
2889
|
+
const outlines2 = text.takeOutlines();
|
|
2890
|
+
if (outlines2 && lineOut) lineOut.push(outlines2);
|
|
2891
|
+
};
|
|
2892
|
+
const markersByPage = /* @__PURE__ */ new Map();
|
|
2893
|
+
for (let i = 0; i < spans.length; i++) {
|
|
2894
|
+
if (i % 128 === 0) await work.yield();
|
|
2895
|
+
const visit = spans[i], page = pages[visit.page.index], out = outFor(visit);
|
|
2896
|
+
if (lineStarts.has(visit)) {
|
|
2897
|
+
placeOutlines();
|
|
2898
|
+
lineOut = out;
|
|
2899
|
+
if (activeOut) activeOut.push("EMC");
|
|
2900
|
+
const logical = lineStarts.get(visit);
|
|
2901
|
+
activeOut = logical === null ? void 0 : out;
|
|
2902
|
+
if (logical !== null)
|
|
2903
|
+
out.push(`/Span << /ActualText <FEFF${unicodeHex(logical ?? "")}> >> BDC`);
|
|
2904
|
+
}
|
|
2905
|
+
let markers = markersByPage.get(visit.page.index);
|
|
2906
|
+
if (!markers) {
|
|
2907
|
+
markers = /* @__PURE__ */ new Set();
|
|
2908
|
+
markersByPage.set(visit.page.index, markers);
|
|
2909
|
+
}
|
|
2910
|
+
const marker = visit.paragraph.marker;
|
|
2911
|
+
if (marker && !markers.has(visit.paragraph)) {
|
|
2912
|
+
markers.add(visit.paragraph);
|
|
2913
|
+
const picture = marker.picture ? await images.paintListMarkerPicture(
|
|
2914
|
+
marker.picture,
|
|
2915
|
+
{
|
|
2916
|
+
...marker.picture.box,
|
|
2917
|
+
x: marker.picture.box.x + visit.storyOrigin.x - visit.page.box.x,
|
|
2918
|
+
y: marker.picture.box.y + visit.storyOrigin.y - visit.page.box.y
|
|
2919
|
+
},
|
|
2920
|
+
page,
|
|
2921
|
+
visit.page.index
|
|
2922
|
+
) : "";
|
|
2923
|
+
if (picture) out.push(picture);
|
|
2924
|
+
else
|
|
2925
|
+
for (const piece of marker.pieces ?? [marker])
|
|
2926
|
+
out.push(
|
|
2927
|
+
text.paint(
|
|
2928
|
+
{
|
|
2929
|
+
...visit,
|
|
2930
|
+
span: {
|
|
2931
|
+
...visit.span,
|
|
2932
|
+
text: piece.text,
|
|
2933
|
+
style: piece.style,
|
|
2934
|
+
box: piece.box,
|
|
2935
|
+
link: void 0,
|
|
2936
|
+
revisions: void 0
|
|
2937
|
+
},
|
|
2938
|
+
absoluteBox: {
|
|
2939
|
+
...piece.box,
|
|
2940
|
+
x: piece.box.x + visit.storyOrigin.x,
|
|
2941
|
+
y: piece.box.y + visit.storyOrigin.y
|
|
2942
|
+
}
|
|
2943
|
+
},
|
|
2944
|
+
page
|
|
2945
|
+
)
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
const fill = HIGHLIGHTS[visit.span.style.highlight ?? ""] ?? visit.span.style.shading;
|
|
2949
|
+
if (fill)
|
|
2950
|
+
out.push(
|
|
2951
|
+
`${color(fill)} rg ${rect(text.bandBox(visit), -visit.page.box.x, -visit.page.box.y, pageHeight(page), true)} f`
|
|
2952
|
+
);
|
|
2953
|
+
const clipping = visit.paragraph.clipToBox;
|
|
2954
|
+
if (clipping)
|
|
2955
|
+
out.push(
|
|
2956
|
+
`q ${rect(visit.paragraph.box, visit.storyOrigin.x - visit.page.box.x, visit.storyOrigin.y - visit.page.box.y, pageHeight(page))} W n`
|
|
2957
|
+
);
|
|
2958
|
+
out.push(
|
|
2959
|
+
visit.span.equation ? paintEquation(visit, page, text, work) : text.paint(visit, page)
|
|
2960
|
+
);
|
|
2961
|
+
if (clipping) out.push("Q");
|
|
2962
|
+
linkAnnotation(doc, page, visit, names);
|
|
2963
|
+
}
|
|
2964
|
+
placeOutlines();
|
|
2965
|
+
if (activeOut) activeOut.push("EMC");
|
|
2966
|
+
for (const lines of pageGroups.values()) {
|
|
2967
|
+
for (const group of lines.values()) {
|
|
2968
|
+
const visit = group[0];
|
|
2969
|
+
const strokes = runBorderStrokesForLine(visit.line);
|
|
2970
|
+
if (strokes.length === 0) continue;
|
|
2971
|
+
work.tick();
|
|
2972
|
+
const out = outFor(visit);
|
|
2973
|
+
const height = pageHeight(pages[visit.page.index]);
|
|
2974
|
+
const x = visit.storyOrigin.x - visit.page.box.x;
|
|
2975
|
+
const y = visit.storyOrigin.y - visit.page.box.y;
|
|
2976
|
+
if (visit.paragraph.clipToBox) out.push(`q ${rect(visit.paragraph.box, x, y, height)} W n`);
|
|
2977
|
+
for (const { box, edge } of strokes) {
|
|
2978
|
+
if (edge.shadow || !["single", "thick", "dashed", "dotted", "double"].includes(edge.val))
|
|
2979
|
+
work.report(
|
|
2980
|
+
"run-border-style",
|
|
2981
|
+
`Unsupported character border: ${edge.val}${edge.shadow ? " with shadow" : ""}`,
|
|
2982
|
+
visit.page.index
|
|
2983
|
+
);
|
|
2984
|
+
out.push(rule(box, edge.color, edge.val, x, y, height));
|
|
2985
|
+
}
|
|
2986
|
+
if (visit.paragraph.clipToBox) out.push("Q");
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
const paintDrawing = async (visit) => {
|
|
2990
|
+
const page = pages[visit.page.index];
|
|
2991
|
+
const d = visit.drawing;
|
|
2992
|
+
if (d.kind !== "anchoredDrawing" || !d.textboxStory) return images.paint(visit, page);
|
|
2993
|
+
const story = d.textboxStory;
|
|
2994
|
+
const origin = {
|
|
2995
|
+
x: visit.drawingOrigin.x + story.contentOffset.x - visit.page.box.x,
|
|
2996
|
+
y: visit.drawingOrigin.y + story.contentOffset.y - visit.page.box.y
|
|
2997
|
+
};
|
|
2998
|
+
return images.paintTextbox(visit, page, [
|
|
2999
|
+
...decorations(story.fragments, origin.x, origin.y, page, work, visit.page.index),
|
|
3000
|
+
...textboxBuffers.get(visit.page.index)?.get(d) ?? []
|
|
3001
|
+
]);
|
|
3002
|
+
};
|
|
3003
|
+
const nested = drawings.filter((visit) => visit.story === "textbox" && visit.textboxOwner).sort((a, b) => b.textboxDepth - a.textboxDepth);
|
|
3004
|
+
for (const visit of nested) {
|
|
3005
|
+
const commands = await paintDrawing(visit);
|
|
3006
|
+
if (!commands) continue;
|
|
3007
|
+
const buffer = outFor(visit);
|
|
3008
|
+
if (visit.paintLayer === "behind-text") buffer.unshift(commands);
|
|
3009
|
+
else buffer.push(commands);
|
|
3010
|
+
}
|
|
3011
|
+
for (const visit of drawings) {
|
|
3012
|
+
if (visit.story === "textbox" && visit.textboxOwner) continue;
|
|
3013
|
+
const commands = await paintDrawing(visit);
|
|
3014
|
+
if (visit.paragraph && rotatedCellOf.has(visit.paragraph)) {
|
|
3015
|
+
const cell = rotatedCellOf.get(visit.paragraph);
|
|
3016
|
+
outFor(visit);
|
|
3017
|
+
const entry = rotatedBuffers.get(`${visit.page.index}:${cell.id}`);
|
|
3018
|
+
(visit.paintLayer === "behind-text" ? entry.behind : entry.commands).push(commands);
|
|
3019
|
+
continue;
|
|
3020
|
+
}
|
|
3021
|
+
(visit.paintLayer === "behind-text" ? behindStreams : streams)[visit.page.index].push(
|
|
3022
|
+
commands
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
for (const { cell, x, y, page, commands, behind } of rotatedBuffers.values()) {
|
|
3026
|
+
const matrix = rotatedCellMatrix(cell.box, x, y, pageHeight(pages[page]));
|
|
3027
|
+
if (behind.length) behindStreams[page].push("q", matrix, ...behind, "Q");
|
|
3028
|
+
if (commands.length) streams[page].push("q", matrix, ...commands, "Q");
|
|
3029
|
+
}
|
|
3030
|
+
for (let i = 0; i < streams.length; i++) streams[i].push(...frontBorders[i]);
|
|
3031
|
+
for (let i = 0; i < pages.length; i++) {
|
|
3032
|
+
work.check();
|
|
3033
|
+
pages[i].node.addContentStream(
|
|
3034
|
+
doc.context.register(
|
|
3035
|
+
flateStream(doc.context, behindStreams[i].join("\n") + "\n" + streams[i].join("\n"))
|
|
3036
|
+
)
|
|
3037
|
+
);
|
|
3038
|
+
}
|
|
3039
|
+
for (const face2 of text.faces.values()) await face2.finish(work);
|
|
3040
|
+
if (includeComments) comments(doc, pages, layout, work);
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
// src/pdf-work.ts
|
|
3044
|
+
import { ExportResourceError as ExportResourceError3 } from "@docx-editor.dev/core/export";
|
|
3045
|
+
|
|
3046
|
+
// src/types.ts
|
|
3047
|
+
var PdfFidelityError = class extends Error {
|
|
3048
|
+
constructor(diagnostics) {
|
|
3049
|
+
super("PDF export cannot faithfully represent all requested content");
|
|
3050
|
+
this.diagnostics = diagnostics;
|
|
3051
|
+
this.name = "PdfFidelityError";
|
|
3052
|
+
}
|
|
3053
|
+
diagnostics;
|
|
3054
|
+
code = "fidelityUnsupported";
|
|
3055
|
+
};
|
|
3056
|
+
var PdfDocumentOpenError = class extends Error {
|
|
3057
|
+
constructor(reason, detail) {
|
|
3058
|
+
super(`Cannot open DOCX: ${reason}${detail ? `: ${detail}` : ""}`);
|
|
3059
|
+
this.reason = reason;
|
|
3060
|
+
this.detail = detail;
|
|
3061
|
+
this.name = "PdfDocumentOpenError";
|
|
3062
|
+
}
|
|
3063
|
+
reason;
|
|
3064
|
+
detail;
|
|
3065
|
+
code = "documentOpenFailed";
|
|
3066
|
+
};
|
|
3067
|
+
var PdfEncodingError = class extends Error {
|
|
3068
|
+
code = "encodingFailed";
|
|
3069
|
+
constructor(message, options) {
|
|
3070
|
+
super(message, options);
|
|
3071
|
+
this.name = "PdfEncodingError";
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
var PdfOutputLimitError = class extends PdfEncodingError {
|
|
3075
|
+
constructor(limit, actual) {
|
|
3076
|
+
super(`PDF exceeds maxOutputBytes (${limit}); encoded ${actual} bytes`);
|
|
3077
|
+
this.limit = limit;
|
|
3078
|
+
this.actual = actual;
|
|
3079
|
+
this.name = "PdfOutputLimitError";
|
|
3080
|
+
}
|
|
3081
|
+
limit;
|
|
3082
|
+
actual;
|
|
3083
|
+
code = "outputTooLarge";
|
|
3084
|
+
};
|
|
3085
|
+
var PdfPageLimitError = class extends RangeError {
|
|
3086
|
+
constructor(limit, actual) {
|
|
3087
|
+
super(`PDF exceeds maxPages (${limit}); laid out ${actual} pages`);
|
|
3088
|
+
this.limit = limit;
|
|
3089
|
+
this.actual = actual;
|
|
3090
|
+
this.name = "PdfPageLimitError";
|
|
3091
|
+
}
|
|
3092
|
+
limit;
|
|
3093
|
+
actual;
|
|
3094
|
+
code = "pageLimitExceeded";
|
|
3095
|
+
};
|
|
3096
|
+
|
|
3097
|
+
// src/pdf-work.ts
|
|
3098
|
+
async function withPdfWork(options, run) {
|
|
3099
|
+
const { timeoutMs } = validateOptions(options);
|
|
3100
|
+
const controller = new AbortController();
|
|
3101
|
+
const signal = options.signal;
|
|
3102
|
+
const abort = () => controller.abort(
|
|
3103
|
+
new ExportResourceError3("aborted", "PDF conversion aborted", { cause: signal?.reason })
|
|
3104
|
+
);
|
|
3105
|
+
if (signal?.aborted) abort();
|
|
3106
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
3107
|
+
const timer = setTimeout(
|
|
3108
|
+
() => controller.abort(new ExportResourceError3("timedOut", "PDF conversion timed out")),
|
|
3109
|
+
timeoutMs
|
|
3110
|
+
);
|
|
3111
|
+
const work = new Work(controller.signal, Date.now() + timeoutMs);
|
|
3112
|
+
const started = performance.now();
|
|
3113
|
+
try {
|
|
3114
|
+
work.check();
|
|
3115
|
+
return await run(work, () => Math.round(performance.now() - started));
|
|
3116
|
+
} catch (error) {
|
|
3117
|
+
if (error instanceof PdfDocumentOpenError && error.reason === "aborted" || error instanceof ExportResourceError3 && error.code === "aborted")
|
|
3118
|
+
work.check();
|
|
3119
|
+
if (error instanceof PdfFidelityError || error instanceof PdfDocumentOpenError || error instanceof ExportResourceError3 || error instanceof RangeError || error instanceof PdfEncodingError || error instanceof PdfWorkLimitError)
|
|
3120
|
+
throw error;
|
|
3121
|
+
work.check();
|
|
3122
|
+
throw new PdfEncodingError("PDF encoding failed", { cause: error });
|
|
3123
|
+
} finally {
|
|
3124
|
+
clearTimeout(timer);
|
|
3125
|
+
signal?.removeEventListener("abort", abort);
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
function waitForPdfWork(run, work) {
|
|
3129
|
+
work.check();
|
|
3130
|
+
return new Promise((resolve, reject) => {
|
|
3131
|
+
const abort = () => {
|
|
3132
|
+
try {
|
|
3133
|
+
work.check();
|
|
3134
|
+
} catch (error) {
|
|
3135
|
+
reject(error);
|
|
3136
|
+
}
|
|
3137
|
+
};
|
|
3138
|
+
work.signal.addEventListener("abort", abort, { once: true });
|
|
3139
|
+
const clear = () => work.signal.removeEventListener("abort", abort);
|
|
3140
|
+
Promise.resolve().then(() => {
|
|
3141
|
+
work.check();
|
|
3142
|
+
return run();
|
|
3143
|
+
}).then(
|
|
3144
|
+
(value) => {
|
|
3145
|
+
clear();
|
|
3146
|
+
resolve(value);
|
|
3147
|
+
},
|
|
3148
|
+
(error) => {
|
|
3149
|
+
clear();
|
|
3150
|
+
reject(error);
|
|
3151
|
+
}
|
|
3152
|
+
);
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// src/export-pdf.ts
|
|
3157
|
+
async function exportPdf(source, options = {}) {
|
|
3158
|
+
if (!(source instanceof Uint8Array)) throw new TypeError("source must be a Uint8Array");
|
|
3159
|
+
validateOptions(options);
|
|
3160
|
+
return withPdfWork(options, async (work, phase) => {
|
|
3161
|
+
const {
|
|
3162
|
+
comments: _comments,
|
|
3163
|
+
fidelityPolicy: _fidelity,
|
|
3164
|
+
timeoutMs: _timeout,
|
|
3165
|
+
maxOutputBytes: _bytes,
|
|
3166
|
+
maxPages: _pages,
|
|
3167
|
+
...open
|
|
3168
|
+
} = options;
|
|
3169
|
+
const opened = await openExportSession(source, open, work.signal);
|
|
3170
|
+
if (!opened.ok) throw new PdfDocumentOpenError(opened.reason, opened.detail);
|
|
3171
|
+
try {
|
|
3172
|
+
return await renderSession(opened.session, options, work, phase, phase());
|
|
3173
|
+
} finally {
|
|
3174
|
+
opened.session.dispose();
|
|
3175
|
+
}
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
async function exportPdfFrom(session, options = {}) {
|
|
3179
|
+
validateOptions(options);
|
|
3180
|
+
for (const key of Object.keys(options)) {
|
|
3181
|
+
if (![
|
|
3182
|
+
"comments",
|
|
3183
|
+
"fidelityPolicy",
|
|
3184
|
+
"timeoutMs",
|
|
3185
|
+
"maxOutputBytes",
|
|
3186
|
+
"maxPages",
|
|
3187
|
+
"signal",
|
|
3188
|
+
"displayMode"
|
|
3189
|
+
].includes(key))
|
|
3190
|
+
throw new TypeError(`${key} must be configured when opening the PDF session`);
|
|
3191
|
+
}
|
|
3192
|
+
if (!session || typeof session !== "object" || !hasFontBackedExportCapabilities(session) || typeof session.layout !== "function" || typeof session.layoutFor !== "function" || !session.fontResolution)
|
|
3193
|
+
throw new TypeError("exportPdfFrom requires a font-backed export session");
|
|
3194
|
+
return withPdfWork(options, (work, phase) => renderSession(session, options, work, phase, 0));
|
|
3195
|
+
}
|
|
3196
|
+
async function renderSession(session, options, work, phase, openMs) {
|
|
3197
|
+
const { maxBytes, maxPages } = validateOptions(options);
|
|
3198
|
+
const { comments: comments2 = true, fidelityPolicy = "strict" } = options;
|
|
3199
|
+
work.check();
|
|
3200
|
+
const layout = await waitForPdfWork(
|
|
3201
|
+
() => options.displayMode ? session.layoutFor(options.displayMode) : session.layout(),
|
|
3202
|
+
work
|
|
3203
|
+
);
|
|
3204
|
+
const layoutMs = phase() - openMs;
|
|
3205
|
+
if (layout.pages.length > maxPages) throw new PdfPageLimitError(maxPages, layout.pages.length);
|
|
3206
|
+
reportFontDiagnostics(session.fontResolution, work);
|
|
3207
|
+
const doc = await PDFDocument5.create({ updateMetadata: false });
|
|
3208
|
+
doc.setProducer("docx-editor.dev");
|
|
3209
|
+
doc.setCreator("docx-editor.dev");
|
|
3210
|
+
const date = /* @__PURE__ */ new Date("2020-01-01T00:00:00Z");
|
|
3211
|
+
doc.setCreationDate(date);
|
|
3212
|
+
doc.setModificationDate(date);
|
|
3213
|
+
const metadata = layout.documentMetadata;
|
|
3214
|
+
if (metadata?.title) doc.setTitle(metadata.title);
|
|
3215
|
+
if (metadata?.creator) doc.setAuthor(metadata.creator);
|
|
3216
|
+
if (metadata?.subject) doc.setSubject(metadata.subject);
|
|
3217
|
+
if (metadata?.keywords) doc.setKeywords([metadata.keywords]);
|
|
3218
|
+
await paint(doc, session, layout, work, comments2);
|
|
3219
|
+
const paintMs = phase() - openMs - layoutMs;
|
|
3220
|
+
const diagnostics = Object.freeze([...work.diagnostics]);
|
|
3221
|
+
if (fidelityPolicy === "strict" && diagnostics.some((d) => d.severity !== "information"))
|
|
3222
|
+
throw new PdfFidelityError(diagnostics);
|
|
3223
|
+
work.check();
|
|
3224
|
+
const bytes = await doc.save({ useObjectStreams: false, objectsPerTick: 1e3 });
|
|
3225
|
+
work.check();
|
|
3226
|
+
if (bytes.byteLength > maxBytes) throw new PdfOutputLimitError(maxBytes, bytes.byteLength);
|
|
3227
|
+
return Object.freeze({
|
|
3228
|
+
bytes,
|
|
3229
|
+
pageCount: layout.pages.length,
|
|
3230
|
+
layoutRevision: layout.revision,
|
|
3231
|
+
displayMode: layout.displayMode ?? "proposed",
|
|
3232
|
+
fontResolution: session.fontResolution,
|
|
3233
|
+
diagnostics,
|
|
3234
|
+
timings: Object.freeze({
|
|
3235
|
+
openMs,
|
|
3236
|
+
layoutMs,
|
|
3237
|
+
paintMs,
|
|
3238
|
+
saveMs: phase() - openMs - layoutMs - paintMs
|
|
3239
|
+
})
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
// src/pdf-session.ts
|
|
3244
|
+
async function openDocumentForExport(source, options = {}) {
|
|
3245
|
+
if (!(source instanceof Uint8Array)) throw new TypeError("source must be a Uint8Array");
|
|
3246
|
+
validateOptions(options);
|
|
3247
|
+
for (const key of ["comments", "fidelityPolicy", "timeoutMs", "maxOutputBytes", "maxPages"]) {
|
|
3248
|
+
if (options[key] !== void 0)
|
|
3249
|
+
throw new TypeError(`${key} must be configured in exportPdfFrom`);
|
|
3250
|
+
}
|
|
3251
|
+
return openExportSession(source, options, options.signal);
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
// src/index.ts
|
|
3255
|
+
import { createFontSource as createFontSource2, defineFontResolver as defineFontResolver2 } from "@docx-editor.dev/core/editor";
|
|
3256
|
+
import { ExportResourceError as ExportResourceError4 } from "@docx-editor.dev/core/export";
|
|
3257
|
+
export {
|
|
3258
|
+
ExportResourceError4 as ExportResourceError,
|
|
3259
|
+
PdfDocumentOpenError,
|
|
3260
|
+
PdfEncodingError,
|
|
3261
|
+
PdfFidelityError,
|
|
3262
|
+
PdfOutputLimitError,
|
|
3263
|
+
PdfPageLimitError,
|
|
3264
|
+
PdfWorkLimitError,
|
|
3265
|
+
createFontSource2 as createFontSource,
|
|
3266
|
+
defineFontResolver2 as defineFontResolver,
|
|
3267
|
+
exportPdf,
|
|
3268
|
+
exportPdfFrom,
|
|
3269
|
+
openDocumentForExport
|
|
3270
|
+
};
|