@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.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
- // Prefer available Word CJK faces; packaged Noto remains the portable fallback.
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
- "Noto Sans CJK JP",
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, "Noto Sans CJK JP"])
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
- function isGenericSubstitution(family, sourceFamily) {
357
- if (family === sourceFamily) return false;
358
- return !knownFamilies.has(canonicalFamily(family).family);
359
- }
360
- var supplementalFonts = defineFontResolver(async ({ families, signal }) => {
361
- const wanted = /* @__PURE__ */ new Set();
362
- for (const family of families) {
363
- wanted.add(family);
364
- const target = substituteFor(family);
365
- if (target) wanted.add(target);
366
- }
367
- const sources = await Promise.all(
368
- supplemental.filter(([family]) => wanted.has(family)).map(async ([family, file, weight, style]) => {
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.pathname}`);
426
+ if ("failure" in result) throw new Error(`Invalid packaged PDF font: ${String(file)}`);
372
427
  return result.source;
373
428
  })
374
429
  );
375
- return {
376
- sources,
377
- substitutions: families.flatMap((family) => {
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 (!target) return [];
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
- return [400, 700].flatMap(
382
- (weight) => ["normal", "italic"].map((style) => ({
494
+ for (const [weight, style] of faces)
495
+ substitutions.push({
383
496
  from: face(family, weight, style),
384
- // The packaged CJK, symbol and mathematics faces come in one weight and one style.
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
- if (knownFamilies.has(canonicalFamily(family).family)) continue;
416
- const target = genericSubstituteFor(family);
417
- wanted.add(target);
418
- const { bold, italic } = canonicalFamily(family);
419
- for (const [weight, style] of faces)
420
- substitutions.push({
421
- from: face(family, weight, style),
422
- to: face(
423
- target,
424
- bold || weight === 700 ? 700 : 400,
425
- italic || style === "italic" ? "italic" : "normal"
426
- )
427
- });
428
- }
429
- const sources = await Promise.all(
430
- supplemental.filter(([family]) => wanted.has(family)).map(async ([family, file, weight, style]) => {
431
- const bytes = new Uint8Array(await readFontFile(file, signal));
432
- const result = createFontSource(bytes, face(family, weight, style));
433
- if ("failure" in result) throw new Error(`Invalid packaged PDF font: ${file.pathname}`);
434
- return result.source;
435
- })
436
- );
437
- return { sources, substitutions };
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.subset.includeGlyph(glyph);
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.subset.encode();
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
- const result = readColorGlyph(font, glyphId, work);
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
- return { kind: "none", outlined: glyph.path.commands.length > 0 };
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 path = pathOperators(layer.glyph.path.commands, work);
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(`${number(horizontal)} 0 0 1 ${number(startX)} ${number(baselineY)} Tm ${show}`);
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("missing-glyph", `A visible span in ${family} contains missing glyphs`);
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 code = activeFace.encode(glyph.id, i === cluster.glyphStart ? text : "");
1389
- batch.add(activeFace, activeSize, gx, gy, code);
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 bounds = visit.absolutePaintBounds;
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 sx = content.width / (shape.extentEmu.cx / unit);
1803
- const sy = content.height / (shape.extentEmu.cy / unit);
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
- `${color(component.strokeHex)} RG ${number(Math.max(1, component.strokeWidthEmu) / unit)} w`
2267
+ component.fillHex !== null ? component.strokeHex !== null ? "B*" : "f*" : component.strokeHex !== null ? "S" : "n"
1835
2268
  );
1836
- for (const [index, points] of component.subpathsEmu.entries())
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 left = bounds.x - visit.page.box.x;
1859
- const bottom = pageHeight(page) - bounds.y + visit.page.box.y - bounds.height;
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 + bounds.width, bottom + bounds.height],
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
- activeOut = out;
2420
- out.push(`/Span << /ActualText <FEFF${unicodeHex(lineStarts.get(visit) ?? "")}> >> BDC`);
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
- out.push(
2443
- text.paint(
2444
- {
2445
- ...visit,
2446
- span: {
2447
- ...visit.span,
2448
- text: marker.text,
2449
- style: marker.style,
2450
- box: marker.box,
2451
- link: void 0,
2452
- revisions: void 0
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
- absoluteBox: {
2455
- ...marker.box,
2456
- x: marker.box.x + visit.storyOrigin.x,
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 ExportResourceError2 } from "@docx-editor.dev/core/export";
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 ExportResourceError2("aborted", "PDF conversion aborted", { cause: signal?.reason })
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 ExportResourceError2("timedOut", "PDF conversion timed out")),
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 ExportResourceError2 && error.code === "aborted")
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 ExportResourceError2 || error instanceof RangeError || error instanceof PdfEncodingError || error instanceof PdfWorkLimitError)
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 ExportResourceError3 } from "@docx-editor.dev/core/export";
3256
+ import { ExportResourceError as ExportResourceError4 } from "@docx-editor.dev/core/export";
2772
3257
  export {
2773
- ExportResourceError3 as ExportResourceError,
3258
+ ExportResourceError4 as ExportResourceError,
2774
3259
  PdfDocumentOpenError,
2775
3260
  PdfEncodingError,
2776
3261
  PdfFidelityError,