@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/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
+ };