@docx-editor.dev/docx-to-pdf 2.22.0 → 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/README.md +10 -1
- package/THIRD_PARTY_NOTICES.md +0 -1
- package/assets/sources.json +0 -9
- package/dist/index.cjs +630 -135
- package/dist/index.js +612 -127
- package/docs/fonts.md +60 -2
- package/docs/integrations.md +2 -1
- package/package.json +11 -4
- package/assets/NotoSansCJKjp-Regular.otf +0 -0
- package/licenses/NotoSansCJK-OFL.txt +0 -92
package/dist/index.js
CHANGED
|
@@ -208,11 +208,11 @@ var latinSubstitute = (family, prefix) => [
|
|
|
208
208
|
weight,
|
|
209
209
|
style
|
|
210
210
|
]);
|
|
211
|
+
var CJK_FAMILY = "Noto Sans CJK JP";
|
|
211
212
|
var supplemental = [
|
|
212
213
|
["Noto Sans Symbols 2", new NodeURL("NotoSansSymbols2-Regular.ttf", assetRoot), 400, "normal"],
|
|
213
214
|
["Noto Sans Math", new NodeURL("NotoSansMath-Regular.ttf", assetRoot), 400, "normal"],
|
|
214
215
|
["Noto Sans Arabic", new NodeURL("NotoSansArabic-Regular.ttf", assetRoot), 400, "normal"],
|
|
215
|
-
["Noto Sans CJK JP", new NodeURL("NotoSansCJKjp-Regular.otf", assetRoot), 400, "normal"],
|
|
216
216
|
["Twemoji Mozilla", new NodeURL("TwemojiMozilla.ttf", assetRoot), 400, "normal"],
|
|
217
217
|
["Noto Emoji", new NodeURL("NotoEmoji-Regular.ttf", assetRoot), 400, "normal"],
|
|
218
218
|
...latinSubstitute("Liberation Sans", "LiberationSans"),
|
|
@@ -226,10 +226,16 @@ var PDF_GLYPH_FALLBACKS = [
|
|
|
226
226
|
"Noto Sans Symbols 2",
|
|
227
227
|
"Noto Sans Math",
|
|
228
228
|
"Noto Sans Arabic",
|
|
229
|
-
//
|
|
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.
|
|
230
236
|
"SimSun",
|
|
231
237
|
"Batang",
|
|
232
|
-
|
|
238
|
+
CJK_FAMILY,
|
|
233
239
|
// Color first: a COLR face paints its layers in the writer. The monochrome face stays
|
|
234
240
|
// behind it for the few symbols the color set leaves out.
|
|
235
241
|
"Twemoji Mozilla",
|
|
@@ -265,7 +271,7 @@ var substitutes = {
|
|
|
265
271
|
"Gulim",
|
|
266
272
|
"Dotum",
|
|
267
273
|
"Malgun Gothic"
|
|
268
|
-
].map((family) => [family,
|
|
274
|
+
].map((family) => [family, CJK_FAMILY])
|
|
269
275
|
)
|
|
270
276
|
};
|
|
271
277
|
var localizedFamilies = {
|
|
@@ -334,6 +340,7 @@ var substituteFor = (family) => {
|
|
|
334
340
|
function genericSubstituteFor(family) {
|
|
335
341
|
const name = canonicalFamily(family).family;
|
|
336
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";
|
|
337
344
|
if (/sans/i.test(name)) return "Liberation Sans";
|
|
338
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(
|
|
339
346
|
name
|
|
@@ -343,6 +350,7 @@ function genericSubstituteFor(family) {
|
|
|
343
350
|
}
|
|
344
351
|
var knownFamilies = /* @__PURE__ */ new Set([
|
|
345
352
|
...supplemental.map(([family]) => family),
|
|
353
|
+
CJK_FAMILY,
|
|
346
354
|
...Object.keys(substitutes),
|
|
347
355
|
...Object.keys(packagedSubstitutes),
|
|
348
356
|
...Object.values(packagedSubstitutes),
|
|
@@ -353,89 +361,184 @@ var knownFamilies = /* @__PURE__ */ new Set([
|
|
|
353
361
|
"Webdings",
|
|
354
362
|
"MT Extra"
|
|
355
363
|
]);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
var
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
|
|
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]) => {
|
|
369
424
|
const bytes = new Uint8Array(await readFontFile(file, signal));
|
|
370
425
|
const result = createFontSource(bytes, face(family, weight, style));
|
|
371
|
-
if ("failure" in result) throw new Error(`Invalid packaged PDF font: ${file
|
|
426
|
+
if ("failure" in result) throw new Error(`Invalid packaged PDF font: ${String(file)}`);
|
|
372
427
|
return result.source;
|
|
373
428
|
})
|
|
374
429
|
);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
430
|
+
const supplementalFonts2 = defineFontResolver(async ({ families, signal }) => {
|
|
431
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
432
|
+
for (const family of families) {
|
|
433
|
+
wanted.add(family);
|
|
378
434
|
const target = substituteFor(family);
|
|
379
|
-
if (
|
|
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);
|
|
380
493
|
const { bold, italic } = canonicalFamily(family);
|
|
381
|
-
|
|
382
|
-
|
|
494
|
+
for (const [weight, style] of faces)
|
|
495
|
+
substitutions.push({
|
|
383
496
|
from: face(family, weight, style),
|
|
384
|
-
|
|
385
|
-
to: target.startsWith("Noto") ? face(target) : face(
|
|
497
|
+
to: face(
|
|
386
498
|
target,
|
|
387
499
|
bold || weight === 700 ? 700 : 400,
|
|
388
500
|
italic || style === "italic" ? "italic" : "normal"
|
|
389
501
|
)
|
|
390
|
-
})
|
|
391
|
-
);
|
|
392
|
-
})
|
|
393
|
-
};
|
|
394
|
-
});
|
|
395
|
-
var standInFonts = defineFontResolver(async ({ families, signal, resolvedFaces }) => {
|
|
396
|
-
const covered = new Set(
|
|
397
|
-
(resolvedFaces ?? []).map((face2) => `${face2.family.toLowerCase()}/${face2.weight}/${face2.style}`)
|
|
398
|
-
);
|
|
399
|
-
const has = (family, weight, style) => covered.has(`${family.toLowerCase()}/${weight}/${style}`);
|
|
400
|
-
const faces = [
|
|
401
|
-
[400, "normal"],
|
|
402
|
-
[700, "normal"],
|
|
403
|
-
[400, "italic"],
|
|
404
|
-
[700, "italic"]
|
|
405
|
-
];
|
|
406
|
-
const substitutions = [];
|
|
407
|
-
const wanted = /* @__PURE__ */ new Set();
|
|
408
|
-
for (const family of families) {
|
|
409
|
-
if (has(family, 400, "normal")) {
|
|
410
|
-
for (const [weight, style] of faces)
|
|
411
|
-
if (!has(family, weight, style))
|
|
412
|
-
substitutions.push({ from: face(family, weight, style), to: face(family) });
|
|
413
|
-
continue;
|
|
502
|
+
});
|
|
414
503
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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();
|
|
439
542
|
var wordFontRoots = process.platform === "darwin" ? [
|
|
440
543
|
"/System/Library/Fonts/Supplemental",
|
|
441
544
|
"/Library/Fonts",
|
|
@@ -603,7 +706,8 @@ function openExportSession(source, options, signal) {
|
|
|
603
706
|
...useSystemFonts ? [installedWordFonts] : [],
|
|
604
707
|
bundledFonts,
|
|
605
708
|
...fallbackFonts ? Array.isArray(fallbackFonts) ? fallbackFonts : [fallbackFonts] : [],
|
|
606
|
-
supplementalFonts
|
|
709
|
+
supplementalFonts,
|
|
710
|
+
cjkFonts
|
|
607
711
|
];
|
|
608
712
|
return openFontBackedDocumentForExport(source, {
|
|
609
713
|
...core,
|
|
@@ -952,9 +1056,17 @@ var EmbeddedFace = class {
|
|
|
952
1056
|
throw new Error("Variable fonts require an exact static instance");
|
|
953
1057
|
this.font = font;
|
|
954
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;
|
|
955
1064
|
this.family = admitted.request.family;
|
|
956
1065
|
this.strike = strikeMetrics(admitted);
|
|
957
1066
|
this.subset = font.createSubset();
|
|
1067
|
+
this.trueType = Boolean(
|
|
1068
|
+
font.directory?.tables?.glyf
|
|
1069
|
+
);
|
|
958
1070
|
this.ref = doc.context.nextRef();
|
|
959
1071
|
this.name = `F${index}`;
|
|
960
1072
|
}
|
|
@@ -969,9 +1081,18 @@ var EmbeddedFace = class {
|
|
|
969
1081
|
* glyph, and the page carries the text in its line's `ActualText` instead.
|
|
970
1082
|
*/
|
|
971
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;
|
|
972
1091
|
/** The family this face was admitted under, for legacy symbol-encoding extraction. */
|
|
973
1092
|
family;
|
|
974
1093
|
subset;
|
|
1094
|
+
/** A `glyf` face, whose subset has a `loca` table; see {@link encodeSubset}. */
|
|
1095
|
+
trueType;
|
|
975
1096
|
codes = /* @__PURE__ */ new Map();
|
|
976
1097
|
rows = [];
|
|
977
1098
|
widths = /* @__PURE__ */ new Map();
|
|
@@ -993,7 +1114,7 @@ var EmbeddedFace = class {
|
|
|
993
1114
|
let code = this.codes.get(key);
|
|
994
1115
|
if (code === void 0) {
|
|
995
1116
|
if (this.rows.length >= 65534) throw new Error("Font character code limit exceeded");
|
|
996
|
-
const cid = this.
|
|
1117
|
+
const cid = this.cidFor(glyph, text);
|
|
997
1118
|
if (!Number.isInteger(cid) || cid < 0 || cid > 65535)
|
|
998
1119
|
throw new Error("Invalid subset glyph identifier");
|
|
999
1120
|
code = this.rows.length + 1;
|
|
@@ -1003,6 +1124,60 @@ var EmbeddedFace = class {
|
|
|
1003
1124
|
}
|
|
1004
1125
|
return hex(code);
|
|
1005
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
|
+
}
|
|
1006
1181
|
/**
|
|
1007
1182
|
* A code that draws this face's space and extracts as `text`: the carrier for characters
|
|
1008
1183
|
* painted some other way, such as the palette layers of a color emoji. Drawn invisibly, it
|
|
@@ -1029,7 +1204,7 @@ var EmbeddedFace = class {
|
|
|
1029
1204
|
async finish(work) {
|
|
1030
1205
|
if (this.colorLayers) return;
|
|
1031
1206
|
await work.yield();
|
|
1032
|
-
const bytes = this.
|
|
1207
|
+
const bytes = this.encodeSubset();
|
|
1033
1208
|
work.check();
|
|
1034
1209
|
const cff = bytes[0] === 1 && bytes[1] === 0;
|
|
1035
1210
|
const ctx = this.doc.context;
|
|
@@ -1108,8 +1283,54 @@ var EmbeddedFace = class {
|
|
|
1108
1283
|
};
|
|
1109
1284
|
|
|
1110
1285
|
// src/color-glyphs.ts
|
|
1286
|
+
import { ExportResourceError as ExportResourceError2 } from "@docx-editor.dev/core/export";
|
|
1111
1287
|
var MAX_LAYERS = 256;
|
|
1112
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
|
+
}
|
|
1113
1334
|
var cache = /* @__PURE__ */ new WeakMap();
|
|
1114
1335
|
function colorGlyph(font, glyphId, work) {
|
|
1115
1336
|
let byGlyph = cache.get(font);
|
|
@@ -1119,21 +1340,30 @@ function colorGlyph(font, glyphId, work) {
|
|
|
1119
1340
|
}
|
|
1120
1341
|
const known = byGlyph.get(glyphId);
|
|
1121
1342
|
if (known) return known;
|
|
1122
|
-
|
|
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
|
+
}
|
|
1123
1350
|
byGlyph.set(glyphId, result);
|
|
1124
1351
|
return result;
|
|
1125
1352
|
}
|
|
1126
1353
|
function readColorGlyph(font, glyphId, work) {
|
|
1127
1354
|
const glyph = font.getGlyph(glyphId);
|
|
1128
1355
|
const layers = glyph.layers;
|
|
1129
|
-
if (!layers || layers.length === 0)
|
|
1130
|
-
|
|
1356
|
+
if (!layers || layers.length === 0) {
|
|
1357
|
+
const commands = boundedPathCommands(font, glyph);
|
|
1358
|
+
return { kind: "none", outlined: commands === null || commands.length > 0 };
|
|
1359
|
+
}
|
|
1131
1360
|
if (layers.length > MAX_LAYERS)
|
|
1132
1361
|
return { kind: "refused", reason: `${layers.length} layers exceed the ${MAX_LAYERS} allowed` };
|
|
1133
1362
|
const out = [];
|
|
1134
1363
|
for (const layer of layers) {
|
|
1135
1364
|
work.tick();
|
|
1136
|
-
const
|
|
1365
|
+
const commands = boundedPathCommands(font, layer.glyph);
|
|
1366
|
+
const path = commands && pathOperators(commands, work);
|
|
1137
1367
|
if (path === null)
|
|
1138
1368
|
return { kind: "refused", reason: "a layer outline is malformed or too long" };
|
|
1139
1369
|
if (!path) continue;
|
|
@@ -1146,6 +1376,19 @@ function readColorGlyph(font, glyphId, work) {
|
|
|
1146
1376
|
}
|
|
1147
1377
|
return { kind: "layers", layers: out };
|
|
1148
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
|
+
}
|
|
1149
1392
|
function clampByte(value) {
|
|
1150
1393
|
return Number.isFinite(value) ? Math.min(255, Math.max(0, Math.round(value))) : 0;
|
|
1151
1394
|
}
|
|
@@ -1193,6 +1436,35 @@ function pathOperators(commands, work) {
|
|
|
1193
1436
|
return out.join(" ");
|
|
1194
1437
|
}
|
|
1195
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
|
+
|
|
1196
1468
|
// src/text.ts
|
|
1197
1469
|
var PDF_PAINT_GRID_PT2 = 0.24;
|
|
1198
1470
|
function reducedHalfPoints(fontSizePt, factor) {
|
|
@@ -1213,6 +1485,20 @@ function griddedBaselineInLine(line, face2, paragraph) {
|
|
|
1213
1485
|
return units(line.baseline) * PDF_PAINT_GRID_PT2;
|
|
1214
1486
|
return (units(naturalBox) - units(descent)) * PDF_PAINT_GRID_PT2;
|
|
1215
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
|
+
}
|
|
1216
1502
|
var MAX_TJ_ADJUSTMENT = 1e6;
|
|
1217
1503
|
function penOrigins(run) {
|
|
1218
1504
|
const advances = run.exactAdvancesX;
|
|
@@ -1232,15 +1518,28 @@ function createGlyphBatch(out, horizontal) {
|
|
|
1232
1518
|
let startX = 0;
|
|
1233
1519
|
let penX = 0;
|
|
1234
1520
|
let parts = [];
|
|
1521
|
+
let skew = 0;
|
|
1522
|
+
let vertical = 1;
|
|
1523
|
+
let rise = 0;
|
|
1235
1524
|
const flush = () => {
|
|
1236
1525
|
if (parts.length === 0) return;
|
|
1237
1526
|
const show = parts.length === 1 && parts[0].startsWith("<") ? `${parts[0]} Tj` : `[${parts.join("")}] TJ`;
|
|
1238
|
-
out.push(
|
|
1527
|
+
out.push(
|
|
1528
|
+
`${number(horizontal)} 0 ${number(skew * vertical)} ${number(vertical)} ${number(startX)} ${number(baselineY + rise)} Tm ${show}`
|
|
1529
|
+
);
|
|
1239
1530
|
parts = [];
|
|
1240
1531
|
face2 = null;
|
|
1241
1532
|
};
|
|
1242
1533
|
return {
|
|
1243
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
|
+
},
|
|
1244
1543
|
add(nextFace, nextSize, gx, gy, code) {
|
|
1245
1544
|
const unit = nextSize * horizontal;
|
|
1246
1545
|
const continues = face2 === nextFace && size === nextSize && baselineY === gy && unit !== 0;
|
|
@@ -1274,6 +1573,14 @@ var TextWriter = class {
|
|
|
1274
1573
|
work;
|
|
1275
1574
|
showRevisionMarkup;
|
|
1276
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
|
+
}
|
|
1277
1584
|
refused = /* @__PURE__ */ new Map();
|
|
1278
1585
|
paint(visit, page) {
|
|
1279
1586
|
const { span, line, storyOrigin, absoluteBox } = visit;
|
|
@@ -1295,7 +1602,10 @@ var TextWriter = class {
|
|
|
1295
1602
|
`Core could not provide exact shaping for a visible span in ${family}`
|
|
1296
1603
|
);
|
|
1297
1604
|
if (shaped.run.glyphs.some((g) => g.id === 0))
|
|
1298
|
-
return report(
|
|
1605
|
+
return report(
|
|
1606
|
+
"missing-glyph",
|
|
1607
|
+
`A visible span in ${family} contains missing glyphs${missingGlyphHint(span.text)}`
|
|
1608
|
+
);
|
|
1299
1609
|
const face2 = this.embeddedFace(shaped.font, visit, page);
|
|
1300
1610
|
if (!face2) return "";
|
|
1301
1611
|
const style = styleForFontSlot2(span.style, span.fontSlot);
|
|
@@ -1325,7 +1635,11 @@ var TextWriter = class {
|
|
|
1325
1635
|
const origins = penOrigins(shaped.run);
|
|
1326
1636
|
const batch = createGlyphBatch(out, horizontal);
|
|
1327
1637
|
const colorGlyphs = [];
|
|
1638
|
+
const outlineGlyphs = [];
|
|
1639
|
+
const rightToLeft = (style.shaping?.level ?? 0) % 2 === 1;
|
|
1328
1640
|
let extra = 0;
|
|
1641
|
+
let strokeWidth = 0;
|
|
1642
|
+
let stroked = false;
|
|
1329
1643
|
let activeSize = size;
|
|
1330
1644
|
let activeFace = face2;
|
|
1331
1645
|
let activeIdentity = shaped.font.identity;
|
|
@@ -1340,6 +1654,9 @@ var TextWriter = class {
|
|
|
1340
1654
|
if (!activeFace.colorLayers) out.push(`/${activeFace.name} ${number(activeSize)} Tf`);
|
|
1341
1655
|
}
|
|
1342
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;
|
|
1343
1660
|
for (let i = cluster.glyphStart; i < cluster.glyphEnd; i++) {
|
|
1344
1661
|
this.work.tick();
|
|
1345
1662
|
const glyph = shaped.run.glyphs[i];
|
|
@@ -1382,17 +1699,92 @@ var TextWriter = class {
|
|
|
1382
1699
|
batch.add(host, activeSize, gx, gy, carrier);
|
|
1383
1700
|
batch.flush();
|
|
1384
1701
|
out.push("0 Tr 100 Tz");
|
|
1702
|
+
strokeWidth = 0;
|
|
1385
1703
|
}
|
|
1386
1704
|
continue;
|
|
1387
1705
|
}
|
|
1388
|
-
const
|
|
1389
|
-
|
|
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
|
+
}
|
|
1390
1747
|
}
|
|
1391
1748
|
extra += text.length * style.characterSpacingPt + (text.match(/ /g)?.length ?? 0) * (style.shaping?.wordSpacingPt ?? 0);
|
|
1392
1749
|
}
|
|
1393
1750
|
batch.flush();
|
|
1751
|
+
if (strokeWidth > 0) out.push("0 Tr");
|
|
1394
1752
|
out.push("ET");
|
|
1395
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
|
+
}
|
|
1396
1788
|
for (const { layers, x: gx, y: gy, scale: glyphScale } of colorGlyphs) {
|
|
1397
1789
|
out.push(`q ${number(glyphScale * horizontal)} 0 0 ${number(glyphScale)} ${number(gx)} ${number(gy)} cm`);
|
|
1398
1790
|
for (const layer of layers) {
|
|
@@ -1543,6 +1935,36 @@ var TextWriter = class {
|
|
|
1543
1935
|
const top = line.box.y + line.baseline - faceBaseline - baselineShiftPtOf(styleForFontSlot2(span.style, span.fontSlot));
|
|
1544
1936
|
return { ...absoluteBox, y: absoluteBox.y + (top - span.box.y) };
|
|
1545
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
|
+
}
|
|
1546
1968
|
alphaStates = /* @__PURE__ */ new Map();
|
|
1547
1969
|
/** A registered constant-alpha graphics state, shared across layers of the same alpha. */
|
|
1548
1970
|
alphaState(page, alpha) {
|
|
@@ -1790,17 +2212,20 @@ import { PDFName as PDFName5, PDFString as PDFString4 } from "pdf-lib";
|
|
|
1790
2212
|
|
|
1791
2213
|
// src/vector-shapes.ts
|
|
1792
2214
|
import { PDFName as PDFName4, PDFString as PDFString3 } from "pdf-lib";
|
|
2215
|
+
import { vectorShapeInkClip } from "@docx-editor.dev/core/layout";
|
|
1793
2216
|
function paintVectorShape(doc, page, visit, work) {
|
|
1794
2217
|
const d = visit.drawing;
|
|
1795
2218
|
const shape = d.vectorShape;
|
|
1796
|
-
const
|
|
2219
|
+
const painted = visit.absolutePaintBounds;
|
|
2220
|
+
const bounds = vectorShapeInkClip(d, painted);
|
|
1797
2221
|
if (bounds.width <= 0 || bounds.height <= 0 || d.accessibility.hidden) return "";
|
|
1798
2222
|
const content = d.geometry.contentBounds;
|
|
1799
2223
|
const x = visit.drawingOrigin.x - d.x - visit.page.box.x;
|
|
1800
2224
|
const y = visit.drawingOrigin.y - d.y - visit.page.box.y;
|
|
1801
2225
|
const unit = 12700;
|
|
1802
|
-
const
|
|
1803
|
-
const
|
|
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;
|
|
1804
2229
|
const out = new Commands(work);
|
|
1805
2230
|
out.push(
|
|
1806
2231
|
`q ${rect(bounds, -visit.page.box.x, -visit.page.box.y, pageHeight(page))} W n`,
|
|
@@ -1828,16 +2253,20 @@ function paintVectorShape(doc, page, visit, work) {
|
|
|
1828
2253
|
const stateName = `ShapeAlpha${state.objectNumber}`;
|
|
1829
2254
|
page.node.setExtGState(PDFName4.of(stateName), state);
|
|
1830
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);
|
|
1831
2258
|
if (component.fillHex !== null) out.push(`${color(component.fillHex)} rg`);
|
|
1832
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);
|
|
1833
2266
|
out.push(
|
|
1834
|
-
|
|
2267
|
+
component.fillHex !== null ? component.strokeHex !== null ? "B*" : "f*" : component.strokeHex !== null ? "S" : "n"
|
|
1835
2268
|
);
|
|
1836
|
-
|
|
1837
|
-
out.push(path(points, component.subpathsClosed?.[index] !== false));
|
|
1838
|
-
out.push(
|
|
1839
|
-
component.fillHex !== null ? component.strokeHex !== null ? "B*" : "f*" : component.strokeHex !== null ? "S" : "n"
|
|
1840
|
-
);
|
|
2269
|
+
}
|
|
1841
2270
|
if (component.strokeHex !== null && component.arrowheadsEmu?.length) {
|
|
1842
2271
|
const arrowState = doc.context.register(
|
|
1843
2272
|
doc.context.obj({
|
|
@@ -1855,14 +2284,15 @@ function paintVectorShape(doc, page, visit, work) {
|
|
|
1855
2284
|
out.push("Q");
|
|
1856
2285
|
const uri = d.hyperlinkHref && /^(https?:|mailto:|tel:|ftp:)/i.test(d.hyperlinkHref) ? pdfLiteralUri(d.hyperlinkHref) : null;
|
|
1857
2286
|
if (uri !== null) {
|
|
1858
|
-
const
|
|
1859
|
-
const
|
|
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;
|
|
1860
2290
|
page.node.addAnnot(
|
|
1861
2291
|
doc.context.register(
|
|
1862
2292
|
doc.context.obj({
|
|
1863
2293
|
Type: "Annot",
|
|
1864
2294
|
Subtype: "Link",
|
|
1865
|
-
Rect: [left, bottom, left +
|
|
2295
|
+
Rect: [left, bottom, left + target.width, bottom + target.height],
|
|
1866
2296
|
Border: [0, 0, 0],
|
|
1867
2297
|
A: { S: "URI", URI: PDFString3.of(uri) }
|
|
1868
2298
|
})
|
|
@@ -1997,14 +2427,51 @@ var ImageWriter = class {
|
|
|
1997
2427
|
async paint(visit, page) {
|
|
1998
2428
|
await this.work.yield();
|
|
1999
2429
|
const d = visit.drawing;
|
|
2430
|
+
if (d.accessibility.hidden) return "";
|
|
2000
2431
|
const report = (message) => {
|
|
2001
2432
|
this.work.report("drawing", message, visit.page.index);
|
|
2002
2433
|
return "";
|
|
2003
2434
|
};
|
|
2435
|
+
if (d.groupPicture) return this.paintGroupPicture(visit, page, report);
|
|
2004
2436
|
if (d.vectorShape) return paintVectorShape(this.doc, page, visit, this.work);
|
|
2005
2437
|
if (d.kind === "anchoredDrawing" && d.textboxStory)
|
|
2006
2438
|
return report("Textbox story not routed through paintTextbox");
|
|
2007
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;
|
|
2008
2475
|
const bytes = this.session.validatedImageBytes(d);
|
|
2009
2476
|
if (!bytes || d.resource.kind !== "ready") return report("Image has no validated raster bytes");
|
|
2010
2477
|
const mime = d.resource.mime;
|
|
@@ -2302,6 +2769,12 @@ async function paint(doc, session, layout, work, includeComments) {
|
|
|
2302
2769
|
`0 0 0 rg ${rect(separator, record.contentBox.x - record.box.x, record.contentBox.y - record.box.y, height, true)} f`
|
|
2303
2770
|
);
|
|
2304
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
|
+
);
|
|
2305
2778
|
const sep = area?.separator;
|
|
2306
2779
|
if (!sep || !(sep.ruleStyle || sep.synthetic)) continue;
|
|
2307
2780
|
for (const offset of sep.ruleStyle === "double" ? [0, 2] : [0])
|
|
@@ -2405,19 +2878,29 @@ async function paint(doc, session, layout, work, includeComments) {
|
|
|
2405
2878
|
const first = group[0];
|
|
2406
2879
|
const markerText = first.paragraph.lines[0] === first.line ? first.paragraph.marker?.text : void 0;
|
|
2407
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);
|
|
2408
2882
|
group.sort((a, b) => a.absoluteBox.x - b.absoluteBox.x);
|
|
2409
|
-
lineStarts.set(group[0], logical);
|
|
2883
|
+
lineStarts.set(group[0], rightToLeft ? null : logical);
|
|
2410
2884
|
for (const visit of group) spans.push(visit);
|
|
2411
2885
|
}
|
|
2412
2886
|
let activeOut;
|
|
2887
|
+
let lineOut;
|
|
2888
|
+
const placeOutlines = () => {
|
|
2889
|
+
const outlines2 = text.takeOutlines();
|
|
2890
|
+
if (outlines2 && lineOut) lineOut.push(outlines2);
|
|
2891
|
+
};
|
|
2413
2892
|
const markersByPage = /* @__PURE__ */ new Map();
|
|
2414
2893
|
for (let i = 0; i < spans.length; i++) {
|
|
2415
2894
|
if (i % 128 === 0) await work.yield();
|
|
2416
2895
|
const visit = spans[i], page = pages[visit.page.index], out = outFor(visit);
|
|
2417
2896
|
if (lineStarts.has(visit)) {
|
|
2897
|
+
placeOutlines();
|
|
2898
|
+
lineOut = out;
|
|
2418
2899
|
if (activeOut) activeOut.push("EMC");
|
|
2419
|
-
|
|
2420
|
-
|
|
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`);
|
|
2421
2904
|
}
|
|
2422
2905
|
let markers = markersByPage.get(visit.page.index);
|
|
2423
2906
|
if (!markers) {
|
|
@@ -2439,27 +2922,28 @@ async function paint(doc, session, layout, work, includeComments) {
|
|
|
2439
2922
|
) : "";
|
|
2440
2923
|
if (picture) out.push(picture);
|
|
2441
2924
|
else
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
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
|
+
}
|
|
2453
2943
|
},
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
y: marker.box.y + visit.storyOrigin.y
|
|
2458
|
-
}
|
|
2459
|
-
},
|
|
2460
|
-
page
|
|
2461
|
-
)
|
|
2462
|
-
);
|
|
2944
|
+
page
|
|
2945
|
+
)
|
|
2946
|
+
);
|
|
2463
2947
|
}
|
|
2464
2948
|
const fill = HIGHLIGHTS[visit.span.style.highlight ?? ""] ?? visit.span.style.shading;
|
|
2465
2949
|
if (fill)
|
|
@@ -2477,6 +2961,7 @@ async function paint(doc, session, layout, work, includeComments) {
|
|
|
2477
2961
|
if (clipping) out.push("Q");
|
|
2478
2962
|
linkAnnotation(doc, page, visit, names);
|
|
2479
2963
|
}
|
|
2964
|
+
placeOutlines();
|
|
2480
2965
|
if (activeOut) activeOut.push("EMC");
|
|
2481
2966
|
for (const lines of pageGroups.values()) {
|
|
2482
2967
|
for (const group of lines.values()) {
|
|
@@ -2556,7 +3041,7 @@ async function paint(doc, session, layout, work, includeComments) {
|
|
|
2556
3041
|
}
|
|
2557
3042
|
|
|
2558
3043
|
// src/pdf-work.ts
|
|
2559
|
-
import { ExportResourceError as
|
|
3044
|
+
import { ExportResourceError as ExportResourceError3 } from "@docx-editor.dev/core/export";
|
|
2560
3045
|
|
|
2561
3046
|
// src/types.ts
|
|
2562
3047
|
var PdfFidelityError = class extends Error {
|
|
@@ -2615,12 +3100,12 @@ async function withPdfWork(options, run) {
|
|
|
2615
3100
|
const controller = new AbortController();
|
|
2616
3101
|
const signal = options.signal;
|
|
2617
3102
|
const abort = () => controller.abort(
|
|
2618
|
-
new
|
|
3103
|
+
new ExportResourceError3("aborted", "PDF conversion aborted", { cause: signal?.reason })
|
|
2619
3104
|
);
|
|
2620
3105
|
if (signal?.aborted) abort();
|
|
2621
3106
|
signal?.addEventListener("abort", abort, { once: true });
|
|
2622
3107
|
const timer = setTimeout(
|
|
2623
|
-
() => controller.abort(new
|
|
3108
|
+
() => controller.abort(new ExportResourceError3("timedOut", "PDF conversion timed out")),
|
|
2624
3109
|
timeoutMs
|
|
2625
3110
|
);
|
|
2626
3111
|
const work = new Work(controller.signal, Date.now() + timeoutMs);
|
|
@@ -2629,9 +3114,9 @@ async function withPdfWork(options, run) {
|
|
|
2629
3114
|
work.check();
|
|
2630
3115
|
return await run(work, () => Math.round(performance.now() - started));
|
|
2631
3116
|
} catch (error) {
|
|
2632
|
-
if (error instanceof PdfDocumentOpenError && error.reason === "aborted" || error instanceof
|
|
3117
|
+
if (error instanceof PdfDocumentOpenError && error.reason === "aborted" || error instanceof ExportResourceError3 && error.code === "aborted")
|
|
2633
3118
|
work.check();
|
|
2634
|
-
if (error instanceof PdfFidelityError || error instanceof PdfDocumentOpenError || error instanceof
|
|
3119
|
+
if (error instanceof PdfFidelityError || error instanceof PdfDocumentOpenError || error instanceof ExportResourceError3 || error instanceof RangeError || error instanceof PdfEncodingError || error instanceof PdfWorkLimitError)
|
|
2635
3120
|
throw error;
|
|
2636
3121
|
work.check();
|
|
2637
3122
|
throw new PdfEncodingError("PDF encoding failed", { cause: error });
|
|
@@ -2768,9 +3253,9 @@ async function openDocumentForExport(source, options = {}) {
|
|
|
2768
3253
|
|
|
2769
3254
|
// src/index.ts
|
|
2770
3255
|
import { createFontSource as createFontSource2, defineFontResolver as defineFontResolver2 } from "@docx-editor.dev/core/editor";
|
|
2771
|
-
import { ExportResourceError as
|
|
3256
|
+
import { ExportResourceError as ExportResourceError4 } from "@docx-editor.dev/core/export";
|
|
2772
3257
|
export {
|
|
2773
|
-
|
|
3258
|
+
ExportResourceError4 as ExportResourceError,
|
|
2774
3259
|
PdfDocumentOpenError,
|
|
2775
3260
|
PdfEncodingError,
|
|
2776
3261
|
PdfFidelityError,
|