@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/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,12 +17,20 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
23
- ExportResourceError: () => import_export5.ExportResourceError,
33
+ ExportResourceError: () => import_export6.ExportResourceError,
24
34
  PdfDocumentOpenError: () => PdfDocumentOpenError,
25
35
  PdfEncodingError: () => PdfEncodingError,
26
36
  PdfFidelityError: () => PdfFidelityError,
@@ -37,7 +47,7 @@ module.exports = __toCommonJS(index_exports);
37
47
 
38
48
  // src/export-pdf.ts
39
49
  var import_pdf_lib6 = require("pdf-lib");
40
- var import_export4 = require("@docx-editor.dev/core/export");
50
+ var import_export5 = require("@docx-editor.dev/core/export");
41
51
 
42
52
  // src/context.ts
43
53
  var import_export = require("@docx-editor.dev/core/export");
@@ -243,11 +253,11 @@ var latinSubstitute = (family, prefix) => [
243
253
  weight,
244
254
  style
245
255
  ]);
256
+ var CJK_FAMILY = "Noto Sans CJK JP";
246
257
  var supplemental = [
247
258
  ["Noto Sans Symbols 2", new import_node_url.URL("NotoSansSymbols2-Regular.ttf", assetRoot), 400, "normal"],
248
259
  ["Noto Sans Math", new import_node_url.URL("NotoSansMath-Regular.ttf", assetRoot), 400, "normal"],
249
260
  ["Noto Sans Arabic", new import_node_url.URL("NotoSansArabic-Regular.ttf", assetRoot), 400, "normal"],
250
- ["Noto Sans CJK JP", new import_node_url.URL("NotoSansCJKjp-Regular.otf", assetRoot), 400, "normal"],
251
261
  ["Twemoji Mozilla", new import_node_url.URL("TwemojiMozilla.ttf", assetRoot), 400, "normal"],
252
262
  ["Noto Emoji", new import_node_url.URL("NotoEmoji-Regular.ttf", assetRoot), 400, "normal"],
253
263
  ...latinSubstitute("Liberation Sans", "LiberationSans"),
@@ -261,10 +271,16 @@ var PDF_GLYPH_FALLBACKS = [
261
271
  "Noto Sans Symbols 2",
262
272
  "Noto Sans Math",
263
273
  "Noto Sans Arabic",
264
- // Prefer available Word CJK faces; packaged Noto remains the portable fallback.
274
+ // Hebrew, and Latin, Greek or Cyrillic letters the faces above lack. A missing glyph falls
275
+ // back to Times New Roman in the reference renderer. A host without it resolves the packaged
276
+ // Liberation Serif, whose Latin letters share its widths but whose Hebrew runs about 12%
277
+ // wider, so Hebrew in that face can wrap earlier.
278
+ "Times New Roman",
279
+ // Prefer available Word CJK faces; Noto Sans CJK JP from `@docx-editor.dev/fonts-cjk` is
280
+ // the portable fallback when that package is installed.
265
281
  "SimSun",
266
282
  "Batang",
267
- "Noto Sans CJK JP",
283
+ CJK_FAMILY,
268
284
  // Color first: a COLR face paints its layers in the writer. The monochrome face stays
269
285
  // behind it for the few symbols the color set leaves out.
270
286
  "Twemoji Mozilla",
@@ -300,7 +316,7 @@ var substitutes = {
300
316
  "Gulim",
301
317
  "Dotum",
302
318
  "Malgun Gothic"
303
- ].map((family) => [family, "Noto Sans CJK JP"])
319
+ ].map((family) => [family, CJK_FAMILY])
304
320
  )
305
321
  };
306
322
  var localizedFamilies = {
@@ -369,6 +385,7 @@ var substituteFor = (family) => {
369
385
  function genericSubstituteFor(family) {
370
386
  const name = canonicalFamily(family).family;
371
387
  if (/mono|courier|consol|menlo|typewriter|\bcode\b|fixed/i.test(name)) return "Liberation Mono";
388
+ if (/symbol/i.test(name)) return "Noto Sans Symbols 2";
372
389
  if (/sans/i.test(name)) return "Liberation Sans";
373
390
  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(
374
391
  name
@@ -378,6 +395,7 @@ function genericSubstituteFor(family) {
378
395
  }
379
396
  var knownFamilies = /* @__PURE__ */ new Set([
380
397
  ...supplemental.map(([family]) => family),
398
+ CJK_FAMILY,
381
399
  ...Object.keys(substitutes),
382
400
  ...Object.keys(packagedSubstitutes),
383
401
  ...Object.values(packagedSubstitutes),
@@ -388,89 +406,184 @@ var knownFamilies = /* @__PURE__ */ new Set([
388
406
  "Webdings",
389
407
  "MT Extra"
390
408
  ]);
391
- function isGenericSubstitution(family, sourceFamily) {
392
- if (family === sourceFamily) return false;
393
- return !knownFamilies.has(canonicalFamily(family).family);
394
- }
395
- var supplementalFonts = (0, import_editor.defineFontResolver)(async ({ families, signal }) => {
396
- const wanted = /* @__PURE__ */ new Set();
397
- for (const family of families) {
398
- wanted.add(family);
399
- const target = substituteFor(family);
400
- if (target) wanted.add(target);
401
- }
402
- const sources = await Promise.all(
403
- supplemental.filter(([family]) => wanted.has(family)).map(async ([family, file, weight, style]) => {
409
+ var cjkWordFamilies = /* @__PURE__ */ new Set([
410
+ CJK_FAMILY,
411
+ ...Object.keys(substitutes).filter((family) => substitutes[family] === CJK_FAMILY)
412
+ ]);
413
+ var CJK_PACKAGE = "@docx-editor.dev/fonts-cjk";
414
+ function isPackageNotFound(error, specifier) {
415
+ if (typeof error !== "object" || error === null) return false;
416
+ const { code, message } = error;
417
+ if (code !== "ERR_MODULE_NOT_FOUND" && code !== "MODULE_NOT_FOUND") return false;
418
+ if (typeof message !== "string") return false;
419
+ return [`Cannot find package '${specifier}'`, `Cannot find module '${specifier}'`].some(
420
+ (prefix) => message.startsWith(prefix)
421
+ );
422
+ }
423
+ async function importCjkFace() {
424
+ let module2;
425
+ try {
426
+ module2 = await import(
427
+ /* webpackIgnore: true */
428
+ "@docx-editor.dev/fonts-cjk"
429
+ );
430
+ } catch (error) {
431
+ if (isPackageNotFound(error, CJK_PACKAGE)) return null;
432
+ throw error;
433
+ }
434
+ const file = module2.NOTO_SANS_CJK_JP_URL;
435
+ if (typeof file?.href !== "string")
436
+ throw new TypeError(`${CJK_PACKAGE} does not export NOTO_SANS_CJK_JP_URL`);
437
+ return new import_node_url.URL(file.href);
438
+ }
439
+ var CJK_TEXT = /[\u1100-\u11ff\u2e80-\u2fdf\u3000-\u9fff\ua960-\ua97f\uac00-\ud7ff\uf900-\ufaff\ufe30-\ufe4f\uff00-\uffef]|[\u{20000}-\u{3ffff}]/u;
440
+ var substitutionsTo = (family, target) => {
441
+ const { bold, italic } = canonicalFamily(family);
442
+ return [400, 700].flatMap(
443
+ (weight) => ["normal", "italic"].map((style) => ({
444
+ from: face(family, weight, style),
445
+ // The packaged CJK, symbol and mathematics faces come in one weight and one style.
446
+ to: target.startsWith("Noto") ? face(target) : face(
447
+ target,
448
+ bold || weight === 700 ? 700 : 400,
449
+ italic || style === "italic" ? "italic" : "normal"
450
+ )
451
+ }))
452
+ );
453
+ };
454
+ function packagedFontResolvers(locateCjkFace = importCjkFace) {
455
+ let located;
456
+ let cjkMissing = false;
457
+ const cjkFace = () => located ??= locateCjkFace().then(
458
+ (file) => {
459
+ cjkMissing = file === null;
460
+ return file;
461
+ },
462
+ (error) => {
463
+ cjkMissing = true;
464
+ throw error;
465
+ }
466
+ );
467
+ const readFaces = (faces, signal) => Promise.all(
468
+ faces.map(async ([family, file, weight, style]) => {
404
469
  const bytes = new Uint8Array(await readFontFile(file, signal));
405
470
  const result = (0, import_editor.createFontSource)(bytes, face(family, weight, style));
406
- if ("failure" in result) throw new Error(`Invalid packaged PDF font: ${file.pathname}`);
471
+ if ("failure" in result) throw new Error(`Invalid packaged PDF font: ${String(file)}`);
407
472
  return result.source;
408
473
  })
409
474
  );
410
- return {
411
- sources,
412
- substitutions: families.flatMap((family) => {
475
+ const supplementalFonts2 = (0, import_editor.defineFontResolver)(async ({ families, signal }) => {
476
+ const wanted = /* @__PURE__ */ new Set();
477
+ for (const family of families) {
478
+ wanted.add(family);
413
479
  const target = substituteFor(family);
414
- if (!target) return [];
480
+ if (target) wanted.add(target);
481
+ }
482
+ const sources = await readFaces(
483
+ supplemental.filter(([family]) => wanted.has(family)),
484
+ signal
485
+ );
486
+ return {
487
+ sources,
488
+ substitutions: families.flatMap((family) => {
489
+ const target = substituteFor(family);
490
+ return target && target !== CJK_FAMILY ? substitutionsTo(family, target) : [];
491
+ })
492
+ };
493
+ });
494
+ const cjkFonts2 = (0, import_editor.defineFontResolver)(async ({ families, signal }) => {
495
+ const planned = families.filter((family) => substituteFor(family) === CJK_FAMILY);
496
+ if (!families.includes(CJK_FAMILY) && planned.length === 0) return { sources: [] };
497
+ let sources;
498
+ try {
499
+ const file = await cjkFace();
500
+ if (!file) return { sources: [] };
501
+ sources = await readFaces([[CJK_FAMILY, file, 400, "normal"]], signal);
502
+ } catch (error) {
503
+ if (signal?.aborted) throw error;
504
+ const detail = error instanceof Error ? error.message : String(error);
505
+ throw new Error(`${CJK_PACKAGE} could not supply its face: ${detail}`, { cause: error });
506
+ }
507
+ return {
508
+ sources,
509
+ substitutions: planned.flatMap((family) => substitutionsTo(family, CJK_FAMILY))
510
+ };
511
+ });
512
+ const standInFonts2 = (0, import_editor.defineFontResolver)(async ({ families, signal, resolvedFaces }) => {
513
+ const covered = new Set(
514
+ (resolvedFaces ?? []).map(
515
+ (face2) => `${face2.family.toLowerCase()}/${face2.weight}/${face2.style}`
516
+ )
517
+ );
518
+ const has = (family, weight, style) => covered.has(`${family.toLowerCase()}/${weight}/${style}`);
519
+ const faces = [
520
+ [400, "normal"],
521
+ [700, "normal"],
522
+ [400, "italic"],
523
+ [700, "italic"]
524
+ ];
525
+ const substitutions = [];
526
+ const wanted = /* @__PURE__ */ new Set();
527
+ for (const family of families) {
528
+ if (has(family, 400, "normal")) {
529
+ for (const [weight, style] of faces)
530
+ if (!has(family, weight, style))
531
+ substitutions.push({ from: face(family, weight, style), to: face(family) });
532
+ continue;
533
+ }
534
+ const canonical = canonicalFamily(family).family;
535
+ if (knownFamilies.has(canonical) && !cjkWordFamilies.has(canonical)) continue;
536
+ const target = genericSubstituteFor(family);
537
+ wanted.add(target);
415
538
  const { bold, italic } = canonicalFamily(family);
416
- return [400, 700].flatMap(
417
- (weight) => ["normal", "italic"].map((style) => ({
539
+ for (const [weight, style] of faces)
540
+ substitutions.push({
418
541
  from: face(family, weight, style),
419
- // The packaged CJK, symbol and mathematics faces come in one weight and one style.
420
- to: target.startsWith("Noto") ? face(target) : face(
542
+ to: face(
421
543
  target,
422
544
  bold || weight === 700 ? 700 : 400,
423
545
  italic || style === "italic" ? "italic" : "normal"
424
546
  )
425
- }))
426
- );
427
- })
428
- };
429
- });
430
- var standInFonts = (0, import_editor.defineFontResolver)(async ({ families, signal, resolvedFaces }) => {
431
- const covered = new Set(
432
- (resolvedFaces ?? []).map((face2) => `${face2.family.toLowerCase()}/${face2.weight}/${face2.style}`)
433
- );
434
- const has = (family, weight, style) => covered.has(`${family.toLowerCase()}/${weight}/${style}`);
435
- const faces = [
436
- [400, "normal"],
437
- [700, "normal"],
438
- [400, "italic"],
439
- [700, "italic"]
440
- ];
441
- const substitutions = [];
442
- const wanted = /* @__PURE__ */ new Set();
443
- for (const family of families) {
444
- if (has(family, 400, "normal")) {
445
- for (const [weight, style] of faces)
446
- if (!has(family, weight, style))
447
- substitutions.push({ from: face(family, weight, style), to: face(family) });
448
- continue;
547
+ });
449
548
  }
450
- if (knownFamilies.has(canonicalFamily(family).family)) continue;
451
- const target = genericSubstituteFor(family);
452
- wanted.add(target);
453
- const { bold, italic } = canonicalFamily(family);
454
- for (const [weight, style] of faces)
455
- substitutions.push({
456
- from: face(family, weight, style),
457
- to: face(
458
- target,
459
- bold || weight === 700 ? 700 : 400,
460
- italic || style === "italic" ? "italic" : "normal"
461
- )
462
- });
463
- }
464
- const sources = await Promise.all(
465
- supplemental.filter(([family]) => wanted.has(family)).map(async ([family, file, weight, style]) => {
466
- const bytes = new Uint8Array(await readFontFile(file, signal));
467
- const result = (0, import_editor.createFontSource)(bytes, face(family, weight, style));
468
- if ("failure" in result) throw new Error(`Invalid packaged PDF font: ${file.pathname}`);
469
- return result.source;
470
- })
471
- );
472
- return { sources, substitutions };
473
- });
549
+ const sources = await readFaces(
550
+ supplemental.filter(([family]) => wanted.has(family)),
551
+ signal
552
+ );
553
+ return { sources, substitutions };
554
+ });
555
+ return {
556
+ supplementalFonts: supplementalFonts2,
557
+ cjkFonts: cjkFonts2,
558
+ standInFonts: standInFonts2,
559
+ /**
560
+ * Whether a face resolved through the generic stand-in rather than a metric-compatible
561
+ * plan: a substitution whose family this package does not answer for, or a Word CJK
562
+ * family in its stand-in rather than the CJK face. The writer reports these, since the
563
+ * page carries the text in another font's metrics.
564
+ */
565
+ isGenericSubstitution(family, sourceFamily) {
566
+ if (family === sourceFamily) return false;
567
+ const canonical = canonicalFamily(family).family;
568
+ if (cjkWordFamilies.has(canonical)) return sourceFamily === genericSubstituteFor(family);
569
+ return !knownFamilies.has(canonical);
570
+ },
571
+ /**
572
+ * What a missing-glyph report adds for `text`: the package to install, when the text is
573
+ * CJK and the lookup found no usable package. Empty otherwise.
574
+ */
575
+ missingGlyphHint(text) {
576
+ return cjkMissing && CJK_TEXT.test(text) ? `; install ${CJK_PACKAGE} for Chinese, Japanese, and Korean text` : "";
577
+ }
578
+ };
579
+ }
580
+ var {
581
+ supplementalFonts,
582
+ cjkFonts,
583
+ standInFonts,
584
+ isGenericSubstitution,
585
+ missingGlyphHint
586
+ } = packagedFontResolvers();
474
587
  var wordFontRoots = process.platform === "darwin" ? [
475
588
  "/System/Library/Fonts/Supplemental",
476
589
  "/Library/Fonts",
@@ -638,7 +751,8 @@ function openExportSession(source, options, signal) {
638
751
  ...useSystemFonts ? [installedWordFonts] : [],
639
752
  bundledFonts,
640
753
  ...fallbackFonts ? Array.isArray(fallbackFonts) ? fallbackFonts : [fallbackFonts] : [],
641
- supplementalFonts
754
+ supplementalFonts,
755
+ cjkFonts
642
756
  ];
643
757
  return (0, import_export2.openFontBackedDocumentForExport)(source, {
644
758
  ...core,
@@ -694,7 +808,7 @@ function reportFontDiagnostics(resolution, work) {
694
808
  }
695
809
 
696
810
  // src/paint.ts
697
- var import_layout5 = require("@docx-editor.dev/core/layout");
811
+ var import_layout6 = require("@docx-editor.dev/core/layout");
698
812
 
699
813
  // src/text.ts
700
814
  var import_pdf_lib2 = require("pdf-lib");
@@ -975,9 +1089,17 @@ var EmbeddedFace = class {
975
1089
  throw new Error("Variable fonts require an exact static instance");
976
1090
  this.font = font;
977
1091
  this.colorLayers = Boolean(font.COLR && font.CPAL);
1092
+ const tables = font;
1093
+ const os2 = tables["OS/2"];
1094
+ const macStyle = tables.head?.macStyle;
1095
+ this.bold = (os2?.usWeightClass ?? 400) >= 600 || Boolean(os2?.fsSelection?.bold || macStyle?.bold);
1096
+ this.slanted = Boolean(os2?.fsSelection?.italic || os2?.fsSelection?.oblique || macStyle?.italic) || Number.isFinite(font.italicAngle) && font.italicAngle !== 0;
978
1097
  this.family = admitted.request.family;
979
1098
  this.strike = strikeMetrics(admitted);
980
1099
  this.subset = font.createSubset();
1100
+ this.trueType = Boolean(
1101
+ font.directory?.tables?.glyf
1102
+ );
981
1103
  this.ref = doc.context.nextRef();
982
1104
  this.name = `F${index}`;
983
1105
  }
@@ -992,9 +1114,18 @@ var EmbeddedFace = class {
992
1114
  * glyph, and the page carries the text in its line's `ActualText` instead.
993
1115
  */
994
1116
  colorLayers;
1117
+ /**
1118
+ * Whether the face's own outlines are bold or slanted. A bold or italic run drawn in a
1119
+ * face that is not (a single-weight face, or a substituted regular) gets Word's synthetic
1120
+ * style at paint; a real bold or italic face draws as it is.
1121
+ */
1122
+ bold;
1123
+ slanted;
995
1124
  /** The family this face was admitted under, for legacy symbol-encoding extraction. */
996
1125
  family;
997
1126
  subset;
1127
+ /** A `glyf` face, whose subset has a `loca` table; see {@link encodeSubset}. */
1128
+ trueType;
998
1129
  codes = /* @__PURE__ */ new Map();
999
1130
  rows = [];
1000
1131
  widths = /* @__PURE__ */ new Map();
@@ -1016,7 +1147,7 @@ var EmbeddedFace = class {
1016
1147
  let code = this.codes.get(key);
1017
1148
  if (code === void 0) {
1018
1149
  if (this.rows.length >= 65534) throw new Error("Font character code limit exceeded");
1019
- const cid = this.subset.includeGlyph(glyph);
1150
+ const cid = this.cidFor(glyph, text);
1020
1151
  if (!Number.isInteger(cid) || cid < 0 || cid > 65535)
1021
1152
  throw new Error("Invalid subset glyph identifier");
1022
1153
  code = this.rows.length + 1;
@@ -1026,6 +1157,60 @@ var EmbeddedFace = class {
1026
1157
  }
1027
1158
  return hex(code);
1028
1159
  }
1160
+ /** The text each subset glyph first extracted as; see {@link cidFor}. */
1161
+ cidText = /* @__PURE__ */ new Map();
1162
+ /**
1163
+ * The subset glyph for one (glyph, text) pair.
1164
+ *
1165
+ * An Arabic face draws BEH, TEH, NOON and YEH on one dotless base glyph and adds the dots
1166
+ * as separate glyphs, so one glyph extracts as several letters. Codes already differ per
1167
+ * letter, but MuPDF resolves `ToUnicode` per CID, so one shared CID read every such letter
1168
+ * as the last one mapped. The subset takes the glyph again under a new CID, the way
1169
+ * LibreOffice's writer does. Both fontkit subsets write one entry per listed glyph: a
1170
+ * TrueType subset one `glyf` record, a CFF subset one charstring under a single charset
1171
+ * range, so the CID is the subset index either way.
1172
+ *
1173
+ * Bounded: no copy once the copies and every glyph composites could still pull in could
1174
+ * reach 65535, where the glyph count and component ids wrap. A TrueType subset with copies
1175
+ * is written with long `loca` offsets (see `encodeSubset`).
1176
+ */
1177
+ cidFor(glyph, text) {
1178
+ const known = this.subset.mapping[glyph];
1179
+ if (known === void 0) {
1180
+ const cid2 = this.subset.includeGlyph(glyph);
1181
+ this.cidText.set(cid2, text);
1182
+ return cid2;
1183
+ }
1184
+ const glyphs = this.subset.glyphs;
1185
+ if (this.cidText.get(known) === text || !Array.isArray(glyphs)) return known;
1186
+ if (glyphs.length + this.font.numGlyphs >= 65535) return known;
1187
+ glyphs.push(glyph);
1188
+ this.copies += 1;
1189
+ const cid = glyphs.length - 1;
1190
+ this.cidText.set(cid, text);
1191
+ return cid;
1192
+ }
1193
+ /** Glyphs the subset lists more than once; see {@link cidFor}. */
1194
+ copies = 0;
1195
+ /**
1196
+ * Encode the subset. A TrueType face with short `loca` offsets can address 131070 bytes of
1197
+ * `glyf`, and fontkit copies the face's format into the subset; copies can pass that, and
1198
+ * the offsets then wrap and later glyphs draw other glyphs' outlines. fontkit reads the
1199
+ * format from the face's parsed `loca` (whose offsets are already decoded, so the format
1200
+ * no longer affects reads), so it says long while the subset encodes.
1201
+ */
1202
+ encodeSubset() {
1203
+ const loca = this.font.loca;
1204
+ if (!this.trueType || this.copies === 0 || !loca || loca.version !== 0) {
1205
+ return this.subset.encode();
1206
+ }
1207
+ loca.version = 1;
1208
+ try {
1209
+ return this.subset.encode();
1210
+ } finally {
1211
+ loca.version = 0;
1212
+ }
1213
+ }
1029
1214
  /**
1030
1215
  * A code that draws this face's space and extracts as `text`: the carrier for characters
1031
1216
  * painted some other way, such as the palette layers of a color emoji. Drawn invisibly, it
@@ -1052,7 +1237,7 @@ var EmbeddedFace = class {
1052
1237
  async finish(work) {
1053
1238
  if (this.colorLayers) return;
1054
1239
  await work.yield();
1055
- const bytes = this.subset.encode();
1240
+ const bytes = this.encodeSubset();
1056
1241
  work.check();
1057
1242
  const cff = bytes[0] === 1 && bytes[1] === 0;
1058
1243
  const ctx = this.doc.context;
@@ -1131,8 +1316,54 @@ var EmbeddedFace = class {
1131
1316
  };
1132
1317
 
1133
1318
  // src/color-glyphs.ts
1319
+ var import_export3 = require("@docx-editor.dev/core/export");
1134
1320
  var MAX_LAYERS = 256;
1135
1321
  var MAX_COMMANDS = 8192;
1322
+ var MAX_COMPOSITE_DEPTH = 8;
1323
+ var MAX_EXPANDED_POINTS = MAX_COMMANDS;
1324
+ var pathSafety = /* @__PURE__ */ new WeakMap();
1325
+ function pathIsSafe(font, glyph) {
1326
+ const cached = pathSafety.get(glyph);
1327
+ if (cached !== void 0) return cached;
1328
+ let safe;
1329
+ try {
1330
+ const { points, height } = expanded(font, glyph, 0, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
1331
+ safe = points <= MAX_EXPANDED_POINTS && height <= MAX_COMPOSITE_DEPTH;
1332
+ } catch {
1333
+ safe = false;
1334
+ }
1335
+ pathSafety.set(glyph, safe);
1336
+ return safe;
1337
+ }
1338
+ function expanded(font, glyph, depth, memo, open) {
1339
+ const cached = memo.get(glyph);
1340
+ if (cached) return cached;
1341
+ if (depth > MAX_COMPOSITE_DEPTH || open.has(glyph)) return { points: Infinity, height: Infinity };
1342
+ const decode = glyph._decode;
1343
+ if (typeof decode !== "function") return { points: 0, height: 0 };
1344
+ const decoded = decode.call(glyph);
1345
+ let points = (decoded?.points?.length ?? 0) + 1;
1346
+ let height = 0;
1347
+ open.add(glyph);
1348
+ for (const component of decoded?.components ?? []) {
1349
+ const child = expanded(font, font.getGlyph(component.glyphID), depth + 1, memo, open);
1350
+ points += child.points;
1351
+ height = Math.max(height, child.height + 1);
1352
+ if (points > MAX_EXPANDED_POINTS || height > MAX_COMPOSITE_DEPTH) break;
1353
+ }
1354
+ open.delete(glyph);
1355
+ const result = { points, height };
1356
+ memo.set(glyph, result);
1357
+ return result;
1358
+ }
1359
+ function boundedPathCommands(font, glyph) {
1360
+ if (!pathIsSafe(font, glyph)) return null;
1361
+ try {
1362
+ return glyph.path.commands;
1363
+ } catch {
1364
+ return null;
1365
+ }
1366
+ }
1136
1367
  var cache = /* @__PURE__ */ new WeakMap();
1137
1368
  function colorGlyph(font, glyphId, work) {
1138
1369
  let byGlyph = cache.get(font);
@@ -1142,21 +1373,30 @@ function colorGlyph(font, glyphId, work) {
1142
1373
  }
1143
1374
  const known = byGlyph.get(glyphId);
1144
1375
  if (known) return known;
1145
- const result = readColorGlyph(font, glyphId, work);
1376
+ let result;
1377
+ try {
1378
+ result = readColorGlyph(font, glyphId, work);
1379
+ } catch (error) {
1380
+ if (error instanceof PdfWorkLimitError || error instanceof import_export3.ExportResourceError) throw error;
1381
+ result = { kind: "refused", reason: "the color glyph could not be read" };
1382
+ }
1146
1383
  byGlyph.set(glyphId, result);
1147
1384
  return result;
1148
1385
  }
1149
1386
  function readColorGlyph(font, glyphId, work) {
1150
1387
  const glyph = font.getGlyph(glyphId);
1151
1388
  const layers = glyph.layers;
1152
- if (!layers || layers.length === 0)
1153
- return { kind: "none", outlined: glyph.path.commands.length > 0 };
1389
+ if (!layers || layers.length === 0) {
1390
+ const commands = boundedPathCommands(font, glyph);
1391
+ return { kind: "none", outlined: commands === null || commands.length > 0 };
1392
+ }
1154
1393
  if (layers.length > MAX_LAYERS)
1155
1394
  return { kind: "refused", reason: `${layers.length} layers exceed the ${MAX_LAYERS} allowed` };
1156
1395
  const out = [];
1157
1396
  for (const layer of layers) {
1158
1397
  work.tick();
1159
- const path = pathOperators(layer.glyph.path.commands, work);
1398
+ const commands = boundedPathCommands(font, layer.glyph);
1399
+ const path = commands && pathOperators(commands, work);
1160
1400
  if (path === null)
1161
1401
  return { kind: "refused", reason: "a layer outline is malformed or too long" };
1162
1402
  if (!path) continue;
@@ -1169,6 +1409,19 @@ function readColorGlyph(font, glyphId, work) {
1169
1409
  }
1170
1410
  return { kind: "layers", layers: out };
1171
1411
  }
1412
+ var outlines = /* @__PURE__ */ new WeakMap();
1413
+ function glyphOutline(font, glyphId, work) {
1414
+ let byGlyph = outlines.get(font);
1415
+ if (!byGlyph) {
1416
+ byGlyph = /* @__PURE__ */ new Map();
1417
+ outlines.set(font, byGlyph);
1418
+ }
1419
+ if (byGlyph.has(glyphId)) return byGlyph.get(glyphId);
1420
+ const commands = boundedPathCommands(font, font.getGlyph(glyphId));
1421
+ const path = commands && pathOperators(commands, work);
1422
+ byGlyph.set(glyphId, path);
1423
+ return path;
1424
+ }
1172
1425
  function clampByte(value) {
1173
1426
  return Number.isFinite(value) ? Math.min(255, Math.max(0, Math.round(value))) : 0;
1174
1427
  }
@@ -1216,6 +1469,35 @@ function pathOperators(commands, work) {
1216
1469
  return out.join(" ");
1217
1470
  }
1218
1471
 
1472
+ // src/outline-placement.ts
1473
+ var MAX_OUTLINE_MOVE = 1e5;
1474
+ var movable = (value) => Number.isFinite(value) && Math.abs(value) <= MAX_OUTLINE_MOVE;
1475
+ function outlineOperators(placements) {
1476
+ const out = [];
1477
+ let group = null;
1478
+ let [ra, rc, rd, px, py] = [1, 0, 1, 0, 0];
1479
+ for (const placement of placements) {
1480
+ const same = group !== null && group.state === placement.state && group.a === placement.a && group.c === placement.c && group.d === placement.d;
1481
+ const rawV = same && rd !== 0 ? (placement.y - py) / rd : NaN;
1482
+ const v = movable(rawV) ? Number(number(rawV)) : NaN;
1483
+ const rawU = movable(v) && ra !== 0 ? (placement.x - px - rc * v) / ra : NaN;
1484
+ const u = movable(rawU) ? Number(number(rawU)) : NaN;
1485
+ if (movable(u) && movable(v)) {
1486
+ out.push(`1 0 0 1 ${number(u)} ${number(v)} cm /${placement.form} Do`);
1487
+ px += ra * u + rc * v;
1488
+ py += rd * v;
1489
+ continue;
1490
+ }
1491
+ if (group) out.push("Q");
1492
+ group = placement;
1493
+ const { a, c, d, x, y } = placement;
1494
+ out.push(`q ${placement.state}${[a, 0, c, d, x, y].map(number).join(" ")} cm /${placement.form} Do`);
1495
+ [ra, rc, rd, px, py] = [a, c, d, x, y].map((value) => Number(number(value)));
1496
+ }
1497
+ if (group) out.push("Q");
1498
+ return out.join("\n");
1499
+ }
1500
+
1219
1501
  // src/text.ts
1220
1502
  var PDF_PAINT_GRID_PT2 = 0.24;
1221
1503
  function reducedHalfPoints(fontSizePt, factor) {
@@ -1236,6 +1518,20 @@ function griddedBaselineInLine(line, face2, paragraph) {
1236
1518
  return units(line.baseline) * PDF_PAINT_GRID_PT2;
1237
1519
  return (units(naturalBox) - units(descent)) * PDF_PAINT_GRID_PT2;
1238
1520
  }
1521
+ var SYNTHETIC_ITALIC_SKEW = 87 / 256;
1522
+ var SYNTHETIC_BOLD_VERTICAL = 1 / 1.02;
1523
+ var syntheticBoldStrokePt = (size) => size / 50 + 0.12;
1524
+ var rightToLeftLines = /* @__PURE__ */ new WeakMap();
1525
+ function rightToLeftOnly(line) {
1526
+ let known = rightToLeftLines.get(line);
1527
+ if (known === void 0) {
1528
+ known = line.spans.every(
1529
+ (span) => !/\S/u.test(span.text) || (span.style.shaping?.level ?? 0) % 2 === 1
1530
+ );
1531
+ rightToLeftLines.set(line, known);
1532
+ }
1533
+ return known;
1534
+ }
1239
1535
  var MAX_TJ_ADJUSTMENT = 1e6;
1240
1536
  function penOrigins(run) {
1241
1537
  const advances = run.exactAdvancesX;
@@ -1255,15 +1551,28 @@ function createGlyphBatch(out, horizontal) {
1255
1551
  let startX = 0;
1256
1552
  let penX = 0;
1257
1553
  let parts = [];
1554
+ let skew = 0;
1555
+ let vertical = 1;
1556
+ let rise = 0;
1258
1557
  const flush = () => {
1259
1558
  if (parts.length === 0) return;
1260
1559
  const show = parts.length === 1 && parts[0].startsWith("<") ? `${parts[0]} Tj` : `[${parts.join("")}] TJ`;
1261
- out.push(`${number(horizontal)} 0 0 1 ${number(startX)} ${number(baselineY)} Tm ${show}`);
1560
+ out.push(
1561
+ `${number(horizontal)} 0 ${number(skew * vertical)} ${number(vertical)} ${number(startX)} ${number(baselineY + rise)} Tm ${show}`
1562
+ );
1262
1563
  parts = [];
1263
1564
  face2 = null;
1264
1565
  };
1265
1566
  return {
1266
1567
  flush,
1568
+ /** Change the synthetic matrix; the next glyph starts a new batch when it changes. */
1569
+ matrix(nextSkew, nextVertical, nextRise) {
1570
+ if (nextSkew === skew && nextVertical === vertical && nextRise === rise) return;
1571
+ flush();
1572
+ skew = nextSkew;
1573
+ vertical = nextVertical;
1574
+ rise = nextRise;
1575
+ },
1267
1576
  add(nextFace, nextSize, gx, gy, code) {
1268
1577
  const unit = nextSize * horizontal;
1269
1578
  const continues = face2 === nextFace && size === nextSize && baselineY === gy && unit !== 0;
@@ -1297,6 +1606,14 @@ var TextWriter = class {
1297
1606
  work;
1298
1607
  showRevisionMarkup;
1299
1608
  faces = /* @__PURE__ */ new Map();
1609
+ /** Outline glyphs of the current line, for {@link takeOutlines}. */
1610
+ pendingOutlines = [];
1611
+ /** The outline glyphs painted since the last call; the caller places them after a line. */
1612
+ takeOutlines() {
1613
+ const taken = this.pendingOutlines.length > 0 ? outlineOperators(this.pendingOutlines) : "";
1614
+ this.pendingOutlines = [];
1615
+ return taken;
1616
+ }
1300
1617
  refused = /* @__PURE__ */ new Map();
1301
1618
  paint(visit, page) {
1302
1619
  const { span, line, storyOrigin, absoluteBox } = visit;
@@ -1318,7 +1635,10 @@ var TextWriter = class {
1318
1635
  `Core could not provide exact shaping for a visible span in ${family}`
1319
1636
  );
1320
1637
  if (shaped.run.glyphs.some((g) => g.id === 0))
1321
- return report("missing-glyph", `A visible span in ${family} contains missing glyphs`);
1638
+ return report(
1639
+ "missing-glyph",
1640
+ `A visible span in ${family} contains missing glyphs${missingGlyphHint(span.text)}`
1641
+ );
1322
1642
  const face2 = this.embeddedFace(shaped.font, visit, page);
1323
1643
  if (!face2) return "";
1324
1644
  const style = (0, import_layout4.styleForFontSlot)(span.style, span.fontSlot);
@@ -1348,7 +1668,11 @@ var TextWriter = class {
1348
1668
  const origins = penOrigins(shaped.run);
1349
1669
  const batch = createGlyphBatch(out, horizontal);
1350
1670
  const colorGlyphs = [];
1671
+ const outlineGlyphs = [];
1672
+ const rightToLeft = (style.shaping?.level ?? 0) % 2 === 1;
1351
1673
  let extra = 0;
1674
+ let strokeWidth = 0;
1675
+ let stroked = false;
1352
1676
  let activeSize = size;
1353
1677
  let activeFace = face2;
1354
1678
  let activeIdentity = shaped.font.identity;
@@ -1363,6 +1687,9 @@ var TextWriter = class {
1363
1687
  if (!activeFace.colorLayers) out.push(`/${activeFace.name} ${number(activeSize)} Tf`);
1364
1688
  }
1365
1689
  const text = shaped.run.text.slice(cluster.textStart, cluster.textEnd);
1690
+ let textGlyph = cluster.glyphStart;
1691
+ for (let i = cluster.glyphStart + 1; i < cluster.glyphEnd; i++)
1692
+ if (shaped.run.glyphs[i].advanceX > shaped.run.glyphs[textGlyph].advanceX) textGlyph = i;
1366
1693
  for (let i = cluster.glyphStart; i < cluster.glyphEnd; i++) {
1367
1694
  this.work.tick();
1368
1695
  const glyph = shaped.run.glyphs[i];
@@ -1405,17 +1732,92 @@ var TextWriter = class {
1405
1732
  batch.add(host, activeSize, gx, gy, carrier);
1406
1733
  batch.flush();
1407
1734
  out.push("0 Tr 100 Tz");
1735
+ strokeWidth = 0;
1408
1736
  }
1409
1737
  continue;
1410
1738
  }
1411
- const code = activeFace.encode(glyph.id, i === cluster.glyphStart ? text : "");
1412
- batch.add(activeFace, activeSize, gx, gy, code);
1739
+ const synthesizeBold = style.bold && !style.textOutline && !activeFace.bold;
1740
+ const width = synthesizeBold ? syntheticBoldStrokePt(activeSize) : 0;
1741
+ if (width !== strokeWidth) {
1742
+ batch.flush();
1743
+ if (width > 0 && !stroked) out.push(`${color(foreground)} RG`);
1744
+ stroked ||= width > 0;
1745
+ out.push(width > 0 ? `${number(width)} w 2 Tr` : "0 Tr");
1746
+ strokeWidth = width;
1747
+ }
1748
+ batch.matrix(
1749
+ style.italic && !activeFace.slanted ? SYNTHETIC_ITALIC_SKEW : 0,
1750
+ width > 0 ? SYNTHETIC_BOLD_VERTICAL : 1,
1751
+ width > 0 ? width / 2 * SYNTHETIC_BOLD_VERTICAL : 0
1752
+ );
1753
+ if (rightToLeft && /^ +$/.test(text) && rightToLeftOnly(line)) continue;
1754
+ if (i === textGlyph) {
1755
+ batch.add(activeFace, activeSize, gx, gy, activeFace.encode(glyph.id, text));
1756
+ continue;
1757
+ }
1758
+ const outline = glyphOutline(activeFace.font, glyph.id, this.work);
1759
+ if (outline === null) {
1760
+ this.work.report(
1761
+ "glyph-outline",
1762
+ `A glyph outline in ${family} is malformed or too long`,
1763
+ visit.page.index
1764
+ );
1765
+ continue;
1766
+ }
1767
+ if (outline) {
1768
+ outlineGlyphs.push({
1769
+ face: activeFace,
1770
+ id: glyph.id,
1771
+ path: outline,
1772
+ x: gx,
1773
+ y: gy + (width > 0 ? width / 2 * SYNTHETIC_BOLD_VERTICAL : 0),
1774
+ scale: glyphSize / activeFace.font.unitsPerEm,
1775
+ skew: style.italic && !activeFace.slanted ? SYNTHETIC_ITALIC_SKEW : 0,
1776
+ vertical: width > 0 ? SYNTHETIC_BOLD_VERTICAL : 1,
1777
+ strokeWidth: width
1778
+ });
1779
+ }
1413
1780
  }
1414
1781
  extra += text.length * style.characterSpacingPt + (text.match(/ /g)?.length ?? 0) * (style.shaping?.wordSpacingPt ?? 0);
1415
1782
  }
1416
1783
  batch.flush();
1784
+ if (strokeWidth > 0) out.push("0 Tr");
1417
1785
  out.push("ET");
1418
1786
  if (style.textOutline) out.push("Q");
1787
+ const placements = [];
1788
+ for (const glyph of outlineGlyphs) {
1789
+ const matrix = [
1790
+ glyph.scale * horizontal,
1791
+ 0,
1792
+ glyph.skew * glyph.scale * glyph.vertical,
1793
+ glyph.scale * glyph.vertical,
1794
+ glyph.x,
1795
+ glyph.y
1796
+ ];
1797
+ const outlineStroke = style.textOutline ? { color: style.textOutline.color, width: style.textOutline.widthPt } : glyph.strokeWidth > 0 ? { color: foreground, width: glyph.strokeWidth } : null;
1798
+ const stroke = outlineStroke ? `${color(outlineStroke.color)} RG ${number(outlineStroke.width / (glyph.scale * glyph.vertical))} w ` : "";
1799
+ const form = this.outlineForm(
1800
+ glyph.face,
1801
+ glyph.id,
1802
+ glyph.path,
1803
+ outlineStroke ? "B" : "f",
1804
+ page
1805
+ );
1806
+ placements.push({
1807
+ state: `${color(foreground)} rg ${stroke}`,
1808
+ a: matrix[0],
1809
+ c: matrix[2],
1810
+ d: matrix[3],
1811
+ x: glyph.x,
1812
+ y: glyph.y,
1813
+ form
1814
+ });
1815
+ }
1816
+ if (visit.paragraph.clipToBox) {
1817
+ if (placements.length > 0) out.push(outlineOperators(placements));
1818
+ } else {
1819
+ for (const placement of placements) this.pendingOutlines.push(placement);
1820
+ }
1419
1821
  for (const { layers, x: gx, y: gy, scale: glyphScale } of colorGlyphs) {
1420
1822
  out.push(`q ${number(glyphScale * horizontal)} 0 0 ${number(glyphScale)} ${number(gx)} ${number(gy)} cm`);
1421
1823
  for (const layer of layers) {
@@ -1566,6 +1968,36 @@ var TextWriter = class {
1566
1968
  const top = line.box.y + line.baseline - faceBaseline - (0, import_layout4.baselineShiftPtOf)((0, import_layout4.styleForFontSlot)(span.style, span.fontSlot));
1567
1969
  return { ...absoluteBox, y: absoluteBox.y + (top - span.box.y) };
1568
1970
  }
1971
+ outlineForms = /* @__PURE__ */ new Map();
1972
+ /**
1973
+ * A form XObject that paints one glyph outline in font units, registered on the page.
1974
+ *
1975
+ * The same few dot and mark glyphs repeat across a page, so each outline is written once
1976
+ * per face and paint operator and placed with `Do`. The form sets no colour or width, so
1977
+ * the caller's fill, stroke and line width apply.
1978
+ */
1979
+ outlineForm(face2, glyphId, path, operator, page) {
1980
+ let forms = this.outlineForms.get(face2);
1981
+ if (!forms) this.outlineForms.set(face2, forms = /* @__PURE__ */ new Map());
1982
+ const key = `${glyphId}:${operator}`;
1983
+ let form = forms.get(key);
1984
+ if (!form) {
1985
+ this.work.reserveContent(path.length + 2);
1986
+ const margin = face2.font.unitsPerEm / 4;
1987
+ const bbox = face2.font.bbox;
1988
+ const ref = this.doc.context.register(
1989
+ flateStream(this.doc.context, `${path} ${operator}`, {
1990
+ Type: "XObject",
1991
+ Subtype: "Form",
1992
+ BBox: [bbox.minX - margin, bbox.minY - margin, bbox.maxX + margin, bbox.maxY + margin]
1993
+ })
1994
+ );
1995
+ form = { name: `${face2.name}G${forms.size + 1}`, ref };
1996
+ forms.set(key, form);
1997
+ }
1998
+ page.node.setXObject(import_pdf_lib2.PDFName.of(form.name), form.ref);
1999
+ return form.name;
2000
+ }
1569
2001
  alphaStates = /* @__PURE__ */ new Map();
1570
2002
  /** A registered constant-alpha graphics state, shared across layers of the same alpha. */
1571
2003
  alphaState(page, alpha) {
@@ -1808,17 +2240,20 @@ var import_pdf_lib5 = require("pdf-lib");
1808
2240
 
1809
2241
  // src/vector-shapes.ts
1810
2242
  var import_pdf_lib4 = require("pdf-lib");
2243
+ var import_layout5 = require("@docx-editor.dev/core/layout");
1811
2244
  function paintVectorShape(doc, page, visit, work) {
1812
2245
  const d = visit.drawing;
1813
2246
  const shape = d.vectorShape;
1814
- const bounds = visit.absolutePaintBounds;
2247
+ const painted = visit.absolutePaintBounds;
2248
+ const bounds = (0, import_layout5.vectorShapeInkClip)(d, painted);
1815
2249
  if (bounds.width <= 0 || bounds.height <= 0 || d.accessibility.hidden) return "";
1816
2250
  const content = d.geometry.contentBounds;
1817
2251
  const x = visit.drawingOrigin.x - d.x - visit.page.box.x;
1818
2252
  const y = visit.drawingOrigin.y - d.y - visit.page.box.y;
1819
2253
  const unit = 12700;
1820
- const sx = content.width / (shape.extentEmu.cx / unit);
1821
- const sy = content.height / (shape.extentEmu.cy / unit);
2254
+ const { cx, cy } = shape.extentEmu;
2255
+ const sx = cx > 0 ? content.width / (cx / unit) : content.height / (cy / unit);
2256
+ const sy = cy > 0 ? content.height / (cy / unit) : sx;
1822
2257
  const out = new Commands(work);
1823
2258
  out.push(
1824
2259
  `q ${rect(bounds, -visit.page.box.x, -visit.page.box.y, pageHeight(page))} W n`,
@@ -1846,16 +2281,20 @@ function paintVectorShape(doc, page, visit, work) {
1846
2281
  const stateName = `ShapeAlpha${state.objectNumber}`;
1847
2282
  page.node.setExtGState(import_pdf_lib4.PDFName.of(stateName), state);
1848
2283
  out.push(`q /${stateName} gs`);
2284
+ const inset = component.strokeInset === true && component.strokeHex !== null;
2285
+ const width = Math.max(1, component.strokeWidthEmu) * (inset ? 2 : 1);
1849
2286
  if (component.fillHex !== null) out.push(`${color(component.fillHex)} rg`);
1850
2287
  if (component.strokeHex !== null)
2288
+ out.push(`${color(component.strokeHex)} RG ${number(width / unit)} w`);
2289
+ const geometry = component.subpathsEmu.map((points, index) => path(points, component.subpathsClosed?.[index] !== false)).join("\n");
2290
+ if (inset) {
2291
+ out.push(`q ${geometry} W* n`, geometry, component.fillHex !== null ? "B* Q" : "S Q");
2292
+ } else {
2293
+ out.push(geometry);
1851
2294
  out.push(
1852
- `${color(component.strokeHex)} RG ${number(Math.max(1, component.strokeWidthEmu) / unit)} w`
2295
+ component.fillHex !== null ? component.strokeHex !== null ? "B*" : "f*" : component.strokeHex !== null ? "S" : "n"
1853
2296
  );
1854
- for (const [index, points] of component.subpathsEmu.entries())
1855
- out.push(path(points, component.subpathsClosed?.[index] !== false));
1856
- out.push(
1857
- component.fillHex !== null ? component.strokeHex !== null ? "B*" : "f*" : component.strokeHex !== null ? "S" : "n"
1858
- );
2297
+ }
1859
2298
  if (component.strokeHex !== null && component.arrowheadsEmu?.length) {
1860
2299
  const arrowState = doc.context.register(
1861
2300
  doc.context.obj({
@@ -1873,14 +2312,15 @@ function paintVectorShape(doc, page, visit, work) {
1873
2312
  out.push("Q");
1874
2313
  const uri = d.hyperlinkHref && /^(https?:|mailto:|tel:|ftp:)/i.test(d.hyperlinkHref) ? pdfLiteralUri(d.hyperlinkHref) : null;
1875
2314
  if (uri !== null) {
1876
- const left = bounds.x - visit.page.box.x;
1877
- const bottom = pageHeight(page) - bounds.y + visit.page.box.y - bounds.height;
2315
+ const target = painted.width > 0 && painted.height > 0 ? painted : bounds;
2316
+ const left = target.x - visit.page.box.x;
2317
+ const bottom = pageHeight(page) - target.y + visit.page.box.y - target.height;
1878
2318
  page.node.addAnnot(
1879
2319
  doc.context.register(
1880
2320
  doc.context.obj({
1881
2321
  Type: "Annot",
1882
2322
  Subtype: "Link",
1883
- Rect: [left, bottom, left + bounds.width, bottom + bounds.height],
2323
+ Rect: [left, bottom, left + target.width, bottom + target.height],
1884
2324
  Border: [0, 0, 0],
1885
2325
  A: { S: "URI", URI: import_pdf_lib4.PDFString.of(uri) }
1886
2326
  })
@@ -2015,14 +2455,51 @@ var ImageWriter = class {
2015
2455
  async paint(visit, page) {
2016
2456
  await this.work.yield();
2017
2457
  const d = visit.drawing;
2458
+ if (d.accessibility.hidden) return "";
2018
2459
  const report = (message) => {
2019
2460
  this.work.report("drawing", message, visit.page.index);
2020
2461
  return "";
2021
2462
  };
2463
+ if (d.groupPicture) return this.paintGroupPicture(visit, page, report);
2022
2464
  if (d.vectorShape) return paintVectorShape(this.doc, page, visit, this.work);
2023
2465
  if (d.kind === "anchoredDrawing" && d.textboxStory)
2024
2466
  return report("Textbox story not routed through paintTextbox");
2025
2467
  if (d.placeholderGraphicKind) return report(`Unsupported drawing: ${d.placeholderGraphicKind}`);
2468
+ return this.paintPicture(visit, page, report);
2469
+ }
2470
+ /**
2471
+ * A shape group with a picture member: the picture, then the group's vector members over it.
2472
+ *
2473
+ * The group paints whole or not at all, as it does on screen. Layout already drops an
2474
+ * `mc:AlternateContent` group whose picture resource fails, following the rule for MC
2475
+ * payloads the engine cannot draw. A ready picture in a format this writer cannot embed
2476
+ * gets the same rule here: the MC group is left out and reported at `information`, because
2477
+ * the editor draws it and only this output cannot. A bare group reports every refusal as
2478
+ * `unsupported`, as it reports an unsupported group.
2479
+ */
2480
+ async paintGroupPicture(visit, page, report) {
2481
+ const d = visit.drawing;
2482
+ const { resource } = d;
2483
+ if (d.groupPicture?.alternateContent && resource.kind === "ready" && !EMBEDDABLE_MIMES.has(resource.mime)) {
2484
+ this.work.report(
2485
+ "drawing",
2486
+ `Shape group left out: picture format not embeddable: ${resource.mime}`,
2487
+ visit.page.index,
2488
+ "information"
2489
+ );
2490
+ return "";
2491
+ }
2492
+ let refused = false;
2493
+ const picture = await this.paintPicture(visit, page, (message) => {
2494
+ refused = true;
2495
+ return report(message);
2496
+ });
2497
+ if (refused) return "";
2498
+ const vector = d.vectorShape ? paintVectorShape(this.doc, page, visit, this.work) : "";
2499
+ return [picture, vector].filter((commands) => commands !== "").join("\n");
2500
+ }
2501
+ async paintPicture(visit, page, report) {
2502
+ const d = visit.drawing;
2026
2503
  const bytes = this.session.validatedImageBytes(d);
2027
2504
  if (!bytes || d.resource.kind !== "ready") return report("Image has no validated raster bytes");
2028
2505
  const mime = d.resource.mime;
@@ -2320,6 +2797,12 @@ async function paint(doc, session, layout, work, includeComments) {
2320
2797
  `0 0 0 rg ${rect(separator, record.contentBox.x - record.box.x, record.contentBox.y - record.box.y, height, true)} f`
2321
2798
  );
2322
2799
  for (const area of [record.footnotes, record.endnotes]) {
2800
+ if (area?.fallbackReason === "note-continuation-notice-height-cap")
2801
+ work.report(
2802
+ area.fallbackReason,
2803
+ "Continuation notice exceeds the available page height",
2804
+ record.index
2805
+ );
2323
2806
  const sep = area?.separator;
2324
2807
  if (!sep || !(sep.ruleStyle || sep.synthetic)) continue;
2325
2808
  for (const offset of sep.ruleStyle === "double" ? [0, 2] : [0])
@@ -2332,15 +2815,15 @@ async function paint(doc, session, layout, work, includeComments) {
2332
2815
  const drawings = [];
2333
2816
  for (const warning of layout.contentWarnings ?? [])
2334
2817
  work.report(`core-${warning.code}`, warning.code);
2335
- (0, import_layout5.forEachSemanticSpan)(layout, (visit) => {
2818
+ (0, import_layout6.forEachSemanticSpan)(layout, (visit) => {
2336
2819
  work.tick();
2337
2820
  spans.push(visit);
2338
2821
  });
2339
- (0, import_layout5.forEachSemanticDrawing)(layout, (visit) => {
2822
+ (0, import_layout6.forEachSemanticDrawing)(layout, (visit) => {
2340
2823
  work.tick();
2341
2824
  drawings.push(visit);
2342
2825
  });
2343
- (0, import_layout5.forEachSemanticStory)(layout, (root) => {
2826
+ (0, import_layout6.forEachSemanticStory)(layout, (root) => {
2344
2827
  streams[root.page.index].push(
2345
2828
  ...decorations(
2346
2829
  root.host.fragments,
@@ -2369,7 +2852,7 @@ async function paint(doc, session, layout, work, includeComments) {
2369
2852
  markRotated(inner.blocks, cell ?? (inner.textDirection ? inner : void 0));
2370
2853
  }
2371
2854
  };
2372
- (0, import_layout5.forEachSemanticStory)(layout, (root) => markRotated(root.host.fragments));
2855
+ (0, import_layout6.forEachSemanticStory)(layout, (root) => markRotated(root.host.fragments));
2373
2856
  const rotatedBuffers = /* @__PURE__ */ new Map();
2374
2857
  const outFor = (visit) => {
2375
2858
  const cell = visit.paragraph && rotatedCellOf.get(visit.paragraph);
@@ -2423,19 +2906,29 @@ async function paint(doc, session, layout, work, includeComments) {
2423
2906
  const first = group[0];
2424
2907
  const markerText = first.paragraph.lines[0] === first.line ? first.paragraph.marker?.text : void 0;
2425
2908
  const logical = (markerText ? markerText + " " : "") + group.map((v) => v.span.equation?.fallbackText ?? v.span.text).join("");
2909
+ const rightToLeft = group.some((v) => (v.span.style.shaping?.level ?? 0) % 2 === 1);
2426
2910
  group.sort((a, b) => a.absoluteBox.x - b.absoluteBox.x);
2427
- lineStarts.set(group[0], logical);
2911
+ lineStarts.set(group[0], rightToLeft ? null : logical);
2428
2912
  for (const visit of group) spans.push(visit);
2429
2913
  }
2430
2914
  let activeOut;
2915
+ let lineOut;
2916
+ const placeOutlines = () => {
2917
+ const outlines2 = text.takeOutlines();
2918
+ if (outlines2 && lineOut) lineOut.push(outlines2);
2919
+ };
2431
2920
  const markersByPage = /* @__PURE__ */ new Map();
2432
2921
  for (let i = 0; i < spans.length; i++) {
2433
2922
  if (i % 128 === 0) await work.yield();
2434
2923
  const visit = spans[i], page = pages[visit.page.index], out = outFor(visit);
2435
2924
  if (lineStarts.has(visit)) {
2925
+ placeOutlines();
2926
+ lineOut = out;
2436
2927
  if (activeOut) activeOut.push("EMC");
2437
- activeOut = out;
2438
- out.push(`/Span << /ActualText <FEFF${unicodeHex(lineStarts.get(visit) ?? "")}> >> BDC`);
2928
+ const logical = lineStarts.get(visit);
2929
+ activeOut = logical === null ? void 0 : out;
2930
+ if (logical !== null)
2931
+ out.push(`/Span << /ActualText <FEFF${unicodeHex(logical ?? "")}> >> BDC`);
2439
2932
  }
2440
2933
  let markers = markersByPage.get(visit.page.index);
2441
2934
  if (!markers) {
@@ -2457,27 +2950,28 @@ async function paint(doc, session, layout, work, includeComments) {
2457
2950
  ) : "";
2458
2951
  if (picture) out.push(picture);
2459
2952
  else
2460
- out.push(
2461
- text.paint(
2462
- {
2463
- ...visit,
2464
- span: {
2465
- ...visit.span,
2466
- text: marker.text,
2467
- style: marker.style,
2468
- box: marker.box,
2469
- link: void 0,
2470
- revisions: void 0
2953
+ for (const piece of marker.pieces ?? [marker])
2954
+ out.push(
2955
+ text.paint(
2956
+ {
2957
+ ...visit,
2958
+ span: {
2959
+ ...visit.span,
2960
+ text: piece.text,
2961
+ style: piece.style,
2962
+ box: piece.box,
2963
+ link: void 0,
2964
+ revisions: void 0
2965
+ },
2966
+ absoluteBox: {
2967
+ ...piece.box,
2968
+ x: piece.box.x + visit.storyOrigin.x,
2969
+ y: piece.box.y + visit.storyOrigin.y
2970
+ }
2471
2971
  },
2472
- absoluteBox: {
2473
- ...marker.box,
2474
- x: marker.box.x + visit.storyOrigin.x,
2475
- y: marker.box.y + visit.storyOrigin.y
2476
- }
2477
- },
2478
- page
2479
- )
2480
- );
2972
+ page
2973
+ )
2974
+ );
2481
2975
  }
2482
2976
  const fill = HIGHLIGHTS[visit.span.style.highlight ?? ""] ?? visit.span.style.shading;
2483
2977
  if (fill)
@@ -2495,11 +2989,12 @@ async function paint(doc, session, layout, work, includeComments) {
2495
2989
  if (clipping) out.push("Q");
2496
2990
  linkAnnotation(doc, page, visit, names);
2497
2991
  }
2992
+ placeOutlines();
2498
2993
  if (activeOut) activeOut.push("EMC");
2499
2994
  for (const lines of pageGroups.values()) {
2500
2995
  for (const group of lines.values()) {
2501
2996
  const visit = group[0];
2502
- const strokes = (0, import_layout5.runBorderStrokesForLine)(visit.line);
2997
+ const strokes = (0, import_layout6.runBorderStrokesForLine)(visit.line);
2503
2998
  if (strokes.length === 0) continue;
2504
2999
  work.tick();
2505
3000
  const out = outFor(visit);
@@ -2574,7 +3069,7 @@ async function paint(doc, session, layout, work, includeComments) {
2574
3069
  }
2575
3070
 
2576
3071
  // src/pdf-work.ts
2577
- var import_export3 = require("@docx-editor.dev/core/export");
3072
+ var import_export4 = require("@docx-editor.dev/core/export");
2578
3073
 
2579
3074
  // src/types.ts
2580
3075
  var PdfFidelityError = class extends Error {
@@ -2633,12 +3128,12 @@ async function withPdfWork(options, run) {
2633
3128
  const controller = new AbortController();
2634
3129
  const signal = options.signal;
2635
3130
  const abort = () => controller.abort(
2636
- new import_export3.ExportResourceError("aborted", "PDF conversion aborted", { cause: signal?.reason })
3131
+ new import_export4.ExportResourceError("aborted", "PDF conversion aborted", { cause: signal?.reason })
2637
3132
  );
2638
3133
  if (signal?.aborted) abort();
2639
3134
  signal?.addEventListener("abort", abort, { once: true });
2640
3135
  const timer = setTimeout(
2641
- () => controller.abort(new import_export3.ExportResourceError("timedOut", "PDF conversion timed out")),
3136
+ () => controller.abort(new import_export4.ExportResourceError("timedOut", "PDF conversion timed out")),
2642
3137
  timeoutMs
2643
3138
  );
2644
3139
  const work = new Work(controller.signal, Date.now() + timeoutMs);
@@ -2647,9 +3142,9 @@ async function withPdfWork(options, run) {
2647
3142
  work.check();
2648
3143
  return await run(work, () => Math.round(performance.now() - started));
2649
3144
  } catch (error) {
2650
- if (error instanceof PdfDocumentOpenError && error.reason === "aborted" || error instanceof import_export3.ExportResourceError && error.code === "aborted")
3145
+ if (error instanceof PdfDocumentOpenError && error.reason === "aborted" || error instanceof import_export4.ExportResourceError && error.code === "aborted")
2651
3146
  work.check();
2652
- if (error instanceof PdfFidelityError || error instanceof PdfDocumentOpenError || error instanceof import_export3.ExportResourceError || error instanceof RangeError || error instanceof PdfEncodingError || error instanceof PdfWorkLimitError)
3147
+ if (error instanceof PdfFidelityError || error instanceof PdfDocumentOpenError || error instanceof import_export4.ExportResourceError || error instanceof RangeError || error instanceof PdfEncodingError || error instanceof PdfWorkLimitError)
2653
3148
  throw error;
2654
3149
  work.check();
2655
3150
  throw new PdfEncodingError("PDF encoding failed", { cause: error });
@@ -2722,7 +3217,7 @@ async function exportPdfFrom(session, options = {}) {
2722
3217
  ].includes(key))
2723
3218
  throw new TypeError(`${key} must be configured when opening the PDF session`);
2724
3219
  }
2725
- if (!session || typeof session !== "object" || !(0, import_export4.hasFontBackedExportCapabilities)(session) || typeof session.layout !== "function" || typeof session.layoutFor !== "function" || !session.fontResolution)
3220
+ if (!session || typeof session !== "object" || !(0, import_export5.hasFontBackedExportCapabilities)(session) || typeof session.layout !== "function" || typeof session.layoutFor !== "function" || !session.fontResolution)
2726
3221
  throw new TypeError("exportPdfFrom requires a font-backed export session");
2727
3222
  return withPdfWork(options, (work, phase) => renderSession(session, options, work, phase, 0));
2728
3223
  }
@@ -2786,7 +3281,7 @@ async function openDocumentForExport(source, options = {}) {
2786
3281
 
2787
3282
  // src/index.ts
2788
3283
  var import_editor2 = require("@docx-editor.dev/core/editor");
2789
- var import_export5 = require("@docx-editor.dev/core/export");
3284
+ var import_export6 = require("@docx-editor.dev/core/export");
2790
3285
  // Annotate the CommonJS export names for ESM import in node:
2791
3286
  0 && (module.exports = {
2792
3287
  ExportResourceError,