reamkit 1.22.0 → 1.24.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.
Files changed (101) hide show
  1. package/dist/esm/core/bmp.d.ts +33 -0
  2. package/dist/esm/core/bmp.js +276 -0
  3. package/dist/esm/core/converter/ream.d.ts +16 -0
  4. package/dist/esm/core/converter/ream.js +59 -9
  5. package/dist/esm/core/document-model/types.d.ts +55 -0
  6. package/dist/esm/core/drawingml/chart-geometry.js +14 -7
  7. package/dist/esm/core/drawingml/colors.d.ts +4 -2
  8. package/dist/esm/core/drawingml/colors.js +15 -0
  9. package/dist/esm/core/drawingml/diagram/colors.d.ts +52 -0
  10. package/dist/esm/core/drawingml/diagram/colors.js +125 -0
  11. package/dist/esm/core/drawingml/diagram/data-model.d.ts +47 -0
  12. package/dist/esm/core/drawingml/diagram/data-model.js +119 -0
  13. package/dist/esm/core/drawingml/diagram/layout-engine.d.ts +46 -0
  14. package/dist/esm/core/drawingml/diagram/layout-engine.js +948 -0
  15. package/dist/esm/core/drawingml/diagram/run.d.ts +37 -0
  16. package/dist/esm/core/drawingml/diagram/run.js +50 -0
  17. package/dist/esm/core/drawingml/diagram/to-drawing.d.ts +16 -0
  18. package/dist/esm/core/drawingml/diagram/to-drawing.js +86 -0
  19. package/dist/esm/core/drawingml/preset-geometry.js +116 -6
  20. package/dist/esm/core/drawingml/text-warp.d.ts +58 -0
  21. package/dist/esm/core/drawingml/text-warp.js +355 -0
  22. package/dist/esm/core/drawingml/theme-parser.d.ts +38 -0
  23. package/dist/esm/core/drawingml/theme-parser.js +73 -4
  24. package/dist/esm/core/font/measure.d.ts +12 -0
  25. package/dist/esm/core/font/measure.js +36 -0
  26. package/dist/esm/core/fonts/families.d.ts +10 -0
  27. package/dist/esm/core/fonts/families.js +44 -0
  28. package/dist/esm/core/fonts/index.d.ts +3 -2
  29. package/dist/esm/core/fonts/remote-fonts.d.ts +50 -0
  30. package/dist/esm/core/fonts/remote-fonts.js +114 -8
  31. package/dist/esm/core/fonts/scripts.d.ts +26 -0
  32. package/dist/esm/core/fonts/scripts.js +177 -0
  33. package/dist/esm/core/images.d.ts +30 -2
  34. package/dist/esm/core/images.js +121 -5
  35. package/dist/esm/core/metafile/blit.d.ts +33 -0
  36. package/dist/esm/core/metafile/blit.js +67 -0
  37. package/dist/esm/core/metafile/dib.d.ts +61 -0
  38. package/dist/esm/core/metafile/dib.js +205 -0
  39. package/dist/esm/core/metafile/emf.js +280 -38
  40. package/dist/esm/core/metafile/picture.d.ts +61 -1
  41. package/dist/esm/core/metafile/picture.js +70 -1
  42. package/dist/esm/core/metafile/wmf.js +202 -13
  43. package/dist/esm/core/ole/escher-blip.d.ts +12 -0
  44. package/dist/esm/core/ole/escher-blip.js +98 -0
  45. package/dist/esm/{pdf-reader → core}/png-encode.js +1 -1
  46. package/dist/esm/excel/print-model.js +5 -3
  47. package/dist/esm/excel/sheet-drawing.js +1 -1
  48. package/dist/esm/excel/sheet-to-flow.js +16 -2
  49. package/dist/esm/excel/styles-parser.js +1 -1
  50. package/dist/esm/excel/xls/biff-reader.js +53 -18
  51. package/dist/esm/excel/xls/biff-styles.d.ts +5 -2
  52. package/dist/esm/excel/xls/biff-styles.js +9 -7
  53. package/dist/esm/excel/xls/escher.js +90 -24
  54. package/dist/esm/html/html-writer.js +2 -1
  55. package/dist/esm/layout/page-doc.d.ts +74 -0
  56. package/dist/esm/layout/page-doc.js +14 -1
  57. package/dist/esm/layout/styled-layout.d.ts +2 -2
  58. package/dist/esm/layout/styled-layout.js +475 -48
  59. package/dist/esm/pdf/shading.js +8 -6
  60. package/dist/esm/pdf/styled-page-emitter.js +77 -5
  61. package/dist/esm/pdf/vector-graphics.js +1 -1
  62. package/dist/esm/pdf-reader/image-decode.js +1 -1
  63. package/dist/esm/pptx/embedded-fonts.d.ts +31 -0
  64. package/dist/esm/pptx/embedded-fonts.js +104 -0
  65. package/dist/esm/pptx/placeholder-cascade.d.ts +73 -6
  66. package/dist/esm/pptx/placeholder-cascade.js +114 -18
  67. package/dist/esm/pptx/ppt/ppt-reader.js +191 -13
  68. package/dist/esm/pptx/ppt/ppt-text.d.ts +107 -1
  69. package/dist/esm/pptx/ppt/ppt-text.js +873 -144
  70. package/dist/esm/pptx/pptx-reader.d.ts +8 -0
  71. package/dist/esm/pptx/pptx-reader.js +117 -15
  72. package/dist/esm/pptx/preset-table-styles.d.ts +9 -0
  73. package/dist/esm/pptx/preset-table-styles.js +27 -0
  74. package/dist/esm/pptx/slide-parser.d.ts +11 -2
  75. package/dist/esm/pptx/slide-parser.js +203 -44
  76. package/dist/esm/pptx/sp-helpers.d.ts +9 -4
  77. package/dist/esm/pptx/sp-helpers.js +48 -9
  78. package/dist/esm/pptx/table-style.d.ts +16 -1
  79. package/dist/esm/pptx/table-style.js +104 -10
  80. package/dist/esm/svg/svg-writer.js +1 -0
  81. package/dist/esm/word/document-parser.d.ts +11 -2
  82. package/dist/esm/word/document-parser.js +10 -7
  83. package/dist/esm/word/docx-reader.js +32 -16
  84. package/dist/esm/word/docx-to-pdf.d.ts +8 -7
  85. package/dist/esm/word/docx-to-pdf.js +25 -7
  86. package/dist/esm/word/docx-writer.js +4 -0
  87. package/dist/esm/word/drawing-parser.d.ts +20 -0
  88. package/dist/esm/word/drawing-parser.js +67 -22
  89. package/dist/esm/word/numbering-parser.d.ts +2 -1
  90. package/dist/esm/word/numbering-parser.js +6 -6
  91. package/dist/esm/word/paragraph-properties.d.ts +2 -1
  92. package/dist/esm/word/paragraph-properties.js +2 -2
  93. package/dist/esm/word/run-properties.d.ts +5 -2
  94. package/dist/esm/word/run-properties.js +13 -8
  95. package/dist/esm/word/styles-parser.d.ts +2 -1
  96. package/dist/esm/word/styles-parser.js +12 -12
  97. package/dist/esm/word/table-parser.js +6 -3
  98. package/dist/esm/word/theme-fonts.d.ts +10 -0
  99. package/dist/esm/word/theme-fonts.js +28 -0
  100. package/package.json +1 -1
  101. /package/dist/esm/{pdf-reader → core}/png-encode.d.ts +0 -0
@@ -1,4 +1,5 @@
1
- import { resolveFamilyKey } from "../core/fonts/remote-fonts.js";
1
+ import { resolveFamilyStyle } from "../core/fonts/remote-fonts.js";
2
+ import { scriptForCodepoint } from "../core/fonts/scripts.js";
2
3
  import { shapeText } from "../core/font/opentype-layout.js";
3
4
  import { createFontMeasure } from "../core/font/measure.js";
4
5
  import { halfPtToPt, pt } from "../core/ir/units.js";
@@ -948,8 +949,9 @@ function layoutBodyElement(el, options, fontResources, imageResources, contentWi
948
949
  if (el.kind === "paragraph") return layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, contentWidth);
949
950
  if (el.kind === "table") return layoutTableBlock(el.table, options, fontResources, imageResources, contentWidth);
950
951
  if (el.kind === "image") {
951
- const meta = el.image.resource ? imageResources?.get(el.image.resource)?.metafile : void 0;
952
- return meta ? layoutMetafileBlock(el.image, meta, options, fontResources, contentWidth, maxHeight, box) : layoutImageBlock(el.image, imageResources, contentWidth, maxHeight, box);
952
+ const res = el.image.resource ? imageResources?.get(el.image.resource) : void 0;
953
+ const meta = res?.metafile;
954
+ return meta ? layoutMetafileBlock(el.image, meta, res.metafileImages, options, fontResources, contentWidth, maxHeight, box) : layoutImageBlock(el.image, imageResources, contentWidth, maxHeight, box);
953
955
  }
954
956
  if (el.kind === "chart") return layoutChartBlock(el.chart, options, fontResources, contentWidth, maxHeight, imageResources);
955
957
  return layoutShapeBlock(el.shape, options, fontResources, imageResources, contentWidth, maxHeight, box);
@@ -1027,9 +1029,9 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1027
1029
  const fillGradient = shape.fill.kind === "gradient" ? shape.fill.gradient : void 0;
1028
1030
  const fillImage = shape.fill.kind === "picture" && shape.fill.imageResource !== void 0 ? imageResources?.get(shape.fill.imageResource) : void 0;
1029
1031
  const fillImageResourceName = fillImage?.resourceName;
1030
- const fillImageTile = shape.fill.kind === "picture" && shape.fill.tiled === true && fillImage?.prepared ? {
1031
- widthPt: fillImage.prepared.widthPx * 72 / 96,
1032
- heightPt: fillImage.prepared.heightPx * 72 / 96
1032
+ const fillImageTile = shape.fill.kind === "picture" && shape.fill.tiled === true && fillImage?.prepared ? shape.fill.tileSizePt ?? {
1033
+ widthPt: fillImage.prepared.widthPx * 72 / (fillImage.prepared.dpiX ?? 96) * (shape.fill.tileScale?.sx ?? 1),
1034
+ heightPt: fillImage.prepared.heightPx * 72 / (fillImage.prepared.dpiY ?? 96) * (shape.fill.tileScale?.sy ?? 1)
1033
1035
  } : void 0;
1034
1036
  const fillColorHex = shape.fill.kind === "solid" ? shape.fill.colorHex : fillGradient ? gradientToSolid(fillGradient) : void 0;
1035
1037
  const stroke = buildStroke(shape.line);
@@ -1047,8 +1049,9 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1047
1049
  const textLineGaps = /* @__PURE__ */ new Map();
1048
1050
  let textHeightPt = 0;
1049
1051
  const vertical = text?.vertical;
1052
+ const warp = text?.warp;
1050
1053
  if (text && text.content.length > 0) {
1051
- const innerWidth = vertical ? Math.max(1, heightPt - insetTopPt - insetBottomPt) : Math.max(1, widthPt - insetLeftPt - insetRightPt);
1054
+ const innerWidth = warp ? Number.MAX_SAFE_INTEGER : vertical ? Math.max(1, heightPt - insetTopPt - insetBottomPt) : Math.max(1, widthPt - insetLeftPt - insetRightPt);
1052
1055
  for (const el of text.content) {
1053
1056
  if (el.kind === "image") {
1054
1057
  const res = el.image.resource ? imageResources?.get(el.image.resource) : void 0;
@@ -1141,6 +1144,16 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1141
1144
  }
1142
1145
  }
1143
1146
  if (text?.autoFit && !vertical && textLines.length > 0) heightPt = textHeightPt + insetTopPt + insetBottomPt;
1147
+ if (text?.shrinkToFit === true && text.fitToBox !== true && warp === void 0 && textLines.length > 0 && fitPass < MAX_FIT_PASSES) {
1148
+ const innerW = Math.max(1, widthPt - insetLeftPt - insetRightPt);
1149
+ const innerH = Math.max(1, heightPt - insetTopPt - insetBottomPt);
1150
+ const widest = textLines.reduce((a, l) => Math.max(a, l.contentWidthPt), 0);
1151
+ const k = Math.min(1, innerW / Math.max(1, widest), innerH / Math.max(1, textHeightPt));
1152
+ if (Number.isFinite(k) && k > .05 && k < .98) return layoutShapeBlock({
1153
+ ...shape,
1154
+ text: scaleTextBody(text, k)
1155
+ }, options, fontResources, imageResources, contentWidth, maxHeight, box, fitPass + 1);
1156
+ }
1144
1157
  if (text?.fitToBox === true && textLines.length > 0 && fitPass < MAX_FIT_PASSES) {
1145
1158
  const innerW = Math.max(1, widthPt - insetLeftPt - insetRightPt);
1146
1159
  const innerH = Math.max(1, heightPt - insetTopPt - insetBottomPt);
@@ -1164,6 +1177,10 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1164
1177
  text: scaleTextBody(text, k)
1165
1178
  }, options, fontResources, imageResources, contentWidth, maxHeight, box, fitPass + 1);
1166
1179
  }
1180
+ const warpInk = warp && textLines.length > 0 ? {
1181
+ above: lineInk(textLines[0]).above,
1182
+ below: lineInk(textLines[textLines.length - 1]).below
1183
+ } : void 0;
1167
1184
  const paths = buildShapePaths(shape.geometry, widthPt, heightPt);
1168
1185
  const markerPaths = lineEndPaths(paths, shape.line, stroke?.widthPt ?? 0);
1169
1186
  const scale = shape.width > 0 ? widthPt / shape.width : 1;
@@ -1185,6 +1202,7 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1185
1202
  ...markerPaths.length > 0 ? { markerPaths } : {},
1186
1203
  ...fillImageResourceName ? { fillImageResourceName } : {},
1187
1204
  ...fillImageTile ? { fillImageTile } : {},
1205
+ ...fillImageTile && shape.fill.kind === "picture" && shape.fill.tileFromCentre === true ? { fillImageTileFromCentre: true } : {},
1188
1206
  ...fillImageResourceName && shape.fill.imageCrop ? { fillImageCrop: shape.fill.imageCrop } : {},
1189
1207
  ...fillImageResourceName && shape.fill.imageFillRect ? { fillImageRect: shape.fill.imageFillRect } : {},
1190
1208
  ...fillImageResourceName && shape.fill.duotone ? { fillImageDuotone: shape.fill.duotone } : {},
@@ -1200,6 +1218,12 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1200
1218
  spacingBeforePt: pp.spacingBefore ?? 0,
1201
1219
  spacingAfterPt: pp.spacingAfter ?? 0,
1202
1220
  textLines,
1221
+ ...warp && warpInk ? { textWarp: {
1222
+ preset: warp.preset,
1223
+ ...warp.adjust !== void 0 ? { adjust: warp.adjust } : {},
1224
+ inkAbovePt: warpInk.above,
1225
+ inkBelowPt: warpInk.below
1226
+ } } : {},
1203
1227
  ...textCharts.size > 0 ? { textCharts } : {},
1204
1228
  ...textShapes.size > 0 ? { textShapes } : {},
1205
1229
  ...textTables.size > 0 ? { textTables } : {},
@@ -1225,11 +1249,11 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1225
1249
  * frame. So one is turned into the other rather than growing a block kind, a
1226
1250
  * pagination arm and an emitter of its own.
1227
1251
  */
1228
- function layoutMetafileBlock(image, pic, options, fontResources, contentWidth, maxHeight, box) {
1252
+ function layoutMetafileBlock(image, pic, blitted, options, fontResources, contentWidth, maxHeight, box) {
1229
1253
  const placed = layoutImageBlock(image, void 0, contentWidth, maxHeight, box);
1230
1254
  const widthPt = placed.widthPt;
1231
1255
  const heightPt = placed.heightPt;
1232
- const layout = rotateDrawing(metafileDrawing(pic, widthPt, heightPt, options, fontResources, image.colorChange), (image.rotation60k ?? 0) / 6e4, widthPt / 2, heightPt / 2);
1256
+ const layout = rotateDrawing(metafileDrawing(pic, widthPt, heightPt, options, fontResources, image.colorChange, blitted), (image.rotation60k ?? 0) / 6e4, widthPt / 2, heightPt / 2);
1233
1257
  const pp = image.paragraphProperties;
1234
1258
  return {
1235
1259
  kind: "chart",
@@ -1293,7 +1317,16 @@ function rotateDrawing(layout, deg, cx, cy) {
1293
1317
  y: p.y,
1294
1318
  rotationDeg: (t.rotationDeg ?? 0) - deg
1295
1319
  };
1296
- })
1320
+ }),
1321
+ ...layout.images ? { images: layout.images.map((img) => {
1322
+ const c = at(img.x + img.width / 2, img.y + img.height / 2);
1323
+ return {
1324
+ ...img,
1325
+ x: c.x - img.width / 2,
1326
+ y: c.y - img.height / 2,
1327
+ rotationDeg: (img.rotationDeg ?? 0) + deg
1328
+ };
1329
+ }) } : {}
1297
1330
  };
1298
1331
  }
1299
1332
  /**
@@ -1301,7 +1334,7 @@ function rotateDrawing(layout, deg, cx, cy) {
1301
1334
  * `heightPt` one and turned y-up about its bottom-left corner — the frame both
1302
1335
  * a chart block and an inline picture token are drawn in.
1303
1336
  */
1304
- function metafileDrawing(pic, widthPt, heightPt, options, fontResources, colorChange) {
1337
+ function metafileDrawing(pic, widthPt, heightPt, options, fontResources, colorChange, blitted) {
1305
1338
  const recolour = (hex) => {
1306
1339
  if (hex === void 0 || colorChange === void 0) return hex;
1307
1340
  if (hex.toUpperCase() !== colorChange.fromHex.toUpperCase()) return hex;
@@ -1313,8 +1346,26 @@ function metafileDrawing(pic, widthPt, heightPt, options, fontResources, colorCh
1313
1346
  const mapY = (y) => heightPt - (y - pic.top) * sy;
1314
1347
  const shapes = [];
1315
1348
  const texts = [];
1349
+ const images = [];
1316
1350
  let seq = 0;
1351
+ let blit = 0;
1317
1352
  for (const prim of pic.prims) {
1353
+ if (prim.kind === "image") {
1354
+ const resourceName = blitted?.[blit++] ?? "";
1355
+ if (resourceName === "") {
1356
+ seq++;
1357
+ continue;
1358
+ }
1359
+ images.push({
1360
+ seq: seq++,
1361
+ resourceName,
1362
+ x: mapX(prim.x),
1363
+ y: mapY(prim.y + prim.height),
1364
+ width: prim.width * sx,
1365
+ height: prim.height * sy
1366
+ });
1367
+ continue;
1368
+ }
1318
1369
  if (prim.kind === "path") {
1319
1370
  const fill = recolour(prim.fillColorHex);
1320
1371
  const strokeHex = recolour(prim.stroke?.colorHex);
@@ -1349,7 +1400,6 @@ function metafileDrawing(pic, widthPt, heightPt, options, fontResources, colorCh
1349
1400
  });
1350
1401
  continue;
1351
1402
  }
1352
- if (prim.kind !== "text") continue;
1353
1403
  const { variant } = options.registry.resolveByStyle(prim.bold === true, prim.italic === true);
1354
1404
  const font = fontResources.get(variant);
1355
1405
  if (!font) continue;
@@ -1372,7 +1422,8 @@ function metafileDrawing(pic, widthPt, heightPt, options, fontResources, colorCh
1372
1422
  }
1373
1423
  return {
1374
1424
  shapes,
1375
- texts
1425
+ texts,
1426
+ ...images.length > 0 ? { images } : {}
1376
1427
  };
1377
1428
  }
1378
1429
  function layoutChartBlock(block, options, fontResources, contentWidth, maxHeight, imageResources) {
@@ -1699,9 +1750,29 @@ function collectImageResources(body, headersFooters, options) {
1699
1750
  if (!bytes) continue;
1700
1751
  if (isEmf(bytes) || isWmf(bytes)) {
1701
1752
  try {
1753
+ const metafile = isEmf(bytes) ? readEmf(bytes) : readWmf(bytes);
1754
+ const metafileImages = [];
1755
+ for (const prim of metafile.prims) {
1756
+ if (prim.kind !== "image") continue;
1757
+ let blitted;
1758
+ try {
1759
+ blitted = prepareImage(prim.png, { flattenAlpha });
1760
+ } catch {
1761
+ metafileImages.push("");
1762
+ continue;
1763
+ }
1764
+ counter++;
1765
+ const name = `Im${counter}`;
1766
+ metafileImages.push(name);
1767
+ out.set(`${resourceId}#${String(metafileImages.length)}`, {
1768
+ resourceName: name,
1769
+ prepared: blitted
1770
+ });
1771
+ }
1702
1772
  out.set(resourceId, {
1703
1773
  resourceName: "",
1704
- metafile: isEmf(bytes) ? readEmf(bytes) : readWmf(bytes)
1774
+ metafile,
1775
+ ...metafileImages.length > 0 ? { metafileImages } : {}
1705
1776
  });
1706
1777
  } catch {}
1707
1778
  continue;
@@ -1722,7 +1793,7 @@ function collectImageResources(body, headersFooters, options) {
1722
1793
  }
1723
1794
  function chartPageItems(laid, x, bottomYUp, pageHeight, structId) {
1724
1795
  const fig = structId !== void 0 ? { structId } : {};
1725
- const ordered = laid.layout.shapes.some((sh) => sh.seq !== void 0);
1796
+ const ordered = laid.layout.shapes.some((sh) => sh.seq !== void 0) || (laid.layout.images?.length ?? 0) > 0;
1726
1797
  const picture = ordered ? { pictureId: nextPictureId++ } : {};
1727
1798
  const backdrop = [];
1728
1799
  if (laid.background) {
@@ -1791,7 +1862,23 @@ function chartPageItems(laid, x, bottomYUp, pageHeight, structId) {
1791
1862
  ...picture,
1792
1863
  seq: t.seq ?? Number.MAX_SAFE_INTEGER
1793
1864
  }));
1794
- const all = [...shapes, ...texts];
1865
+ const blits = (laid.layout.images ?? []).map((img) => ({
1866
+ type: "image",
1867
+ x: pt(x + img.x),
1868
+ y: pt(pageHeight - (bottomYUp + img.y + img.height)),
1869
+ width: pt(img.width),
1870
+ height: pt(img.height),
1871
+ imageResourceName: img.resourceName,
1872
+ ...img.rotationDeg ? { rotationDeg: img.rotationDeg } : {},
1873
+ ...fig,
1874
+ ...picture,
1875
+ seq: img.seq ?? 0
1876
+ }));
1877
+ const all = [
1878
+ ...shapes,
1879
+ ...texts,
1880
+ ...blits
1881
+ ];
1795
1882
  if (ordered) all.sort((a, b) => a.seq - b.seq);
1796
1883
  return [...backdrop, ...all.map(({ seq: _seq, ...item }) => item)];
1797
1884
  }
@@ -1819,6 +1906,10 @@ function emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId) {
1819
1906
  }
1820
1907
  return;
1821
1908
  }
1909
+ if (sh.textWarp) {
1910
+ emitWarpedText(sh, sh.textWarp, x, bottomYUp, sink, pageHeight, figId);
1911
+ return;
1912
+ }
1822
1913
  const shapeTop = bottomYUp + sh.heightPt;
1823
1914
  const innerWidth = Math.max(1, sh.widthPt - sh.insetLeftPt - sh.insetRightPt);
1824
1915
  let textY;
@@ -1858,6 +1949,65 @@ function emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId) {
1858
1949
  textY -= sh.textLineGaps?.get(i) ?? 0;
1859
1950
  });
1860
1951
  }
1952
+ /**
1953
+ * §20.1.9.10 — place a WordArt body's lines in the block's own frame and hand
1954
+ * each one the warp that maps that frame onto the shape's box.
1955
+ *
1956
+ * The block is laid out flat: its lines stack by their own heights, each
1957
+ * aligned within the widest of them, and its ink starts at the box's top-left
1958
+ * corner. Nothing here bends anything — the emitter does that, glyph by glyph,
1959
+ * from the two frames this records.
1960
+ *
1961
+ * @param sh The laid-out shape.
1962
+ * @param warp The shape's warp and the block's ink reach.
1963
+ * @param x The shape's left edge on the page.
1964
+ * @param bottomYUp The shape's bottom edge, y-up from the page's bottom.
1965
+ * @param sink Where the line items are pushed.
1966
+ * @param pageHeight The page's height, for the y-up ⇄ top-left conversion.
1967
+ * @param figId The tagged-PDF Figure this text belongs to, when it has one.
1968
+ */
1969
+ function emitWarpedText(sh, warp, x, bottomYUp, sink, pageHeight, figId) {
1970
+ const boxX = x + sh.insetLeftPt;
1971
+ const boxY = pageHeight - (bottomYUp + sh.heightPt) + sh.insetTopPt;
1972
+ const boxWidth = Math.max(1, sh.widthPt - sh.insetLeftPt - sh.insetRightPt);
1973
+ const boxHeight = Math.max(1, sh.heightPt - sh.insetTopPt - sh.insetBottomPt);
1974
+ const srcWidth = Math.max(1, ...sh.textLines.map((l) => l.contentWidthPt));
1975
+ const baselines = [];
1976
+ let top = 0;
1977
+ sh.textLines.forEach((line, i) => {
1978
+ const h = computeLineHeight(line, line.resolved);
1979
+ baselines.push(top + h - lineBaselineOffset(line, line.resolved));
1980
+ top += h + (sh.textLineGaps?.get(i) ?? 0);
1981
+ });
1982
+ const first = baselines[0] ?? 0;
1983
+ const last = baselines[baselines.length - 1] ?? 0;
1984
+ const srcTop = boxY;
1985
+ const srcHeight = Math.max(1, last - first + warp.inkAbovePt + warp.inkBelowPt);
1986
+ const shift = srcTop + warp.inkAbovePt - first;
1987
+ sh.textLines.forEach((line, i) => {
1988
+ if (sh.textCharts?.has(i) === true || sh.textShapes?.has(i) === true) return;
1989
+ if (sh.textTables?.has(i) === true) return;
1990
+ sink.push({
1991
+ type: "line",
1992
+ line,
1993
+ originX: pt(boxX + alignmentOffset(line.resolved.alignment, line.contentWidthPt, srcWidth)),
1994
+ baselineY: pt(baselines[i] + shift),
1995
+ warp: {
1996
+ preset: warp.preset,
1997
+ ...warp.adjust !== void 0 ? { adjust: warp.adjust } : {},
1998
+ boxX: pt(boxX),
1999
+ boxY: pt(boxY),
2000
+ boxWidth: pt(boxWidth),
2001
+ boxHeight: pt(boxHeight),
2002
+ srcX: pt(boxX),
2003
+ srcWidth: pt(srcWidth),
2004
+ srcTop: pt(srcTop),
2005
+ srcHeight: pt(srcHeight)
2006
+ },
2007
+ ...figId !== void 0 ? { structId: figId } : {}
2008
+ });
2009
+ });
2010
+ }
1861
2011
  /** The page a table is being drawn onto, as a cell's floats need to see it. */
1862
2012
  function cellAnchorCtx(asm) {
1863
2013
  return {
@@ -1897,6 +2047,19 @@ function emitCellFloat(cf, ctx, frame) {
1897
2047
  if (float.wrap !== "none") ctx.exclude(float, x, topYUp, cf.block.widthPt, cf.block.heightPt);
1898
2048
  }
1899
2049
  function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
2050
+ const castsUnderPicture = sh.fillImageResourceName !== void 0 ? sh.shadow : void 0;
2051
+ const shadowedFill = castsUnderPicture !== void 0;
2052
+ const group = shadowedFill ? { pictureId: nextPictureId++ } : {};
2053
+ if (castsUnderPicture) sink.push({
2054
+ type: "shape",
2055
+ shape: {
2056
+ paths: sh.paths,
2057
+ shadow: castsUnderPicture,
2058
+ transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
2059
+ },
2060
+ ...group,
2061
+ ...figId !== void 0 ? { structId: figId } : {}
2062
+ });
1900
2063
  if (sh.fillImageResourceName) {
1901
2064
  const clip = {
1902
2065
  paths: sh.paths,
@@ -1909,12 +2072,13 @@ function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
1909
2072
  y: pageHeight - (bottomYUp + sh.heightPt),
1910
2073
  width: sh.widthPt,
1911
2074
  height: sh.heightPt
1912
- })) {
2075
+ }, sh.fillImageTileFromCentre === true)) {
1913
2076
  if (item.type !== "image") continue;
1914
2077
  sink.push({
1915
2078
  ...item,
1916
2079
  clip,
1917
2080
  ...fig,
2081
+ ...group,
1918
2082
  ...sh.fillAlpha !== void 0 ? { alpha: sh.fillAlpha } : {}
1919
2083
  });
1920
2084
  }
@@ -1935,7 +2099,8 @@ function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
1935
2099
  ...sh.fillAlpha !== void 0 ? { alpha: sh.fillAlpha } : {},
1936
2100
  ...sh.fillImageDuotone ? { duotone: sh.fillImageDuotone } : {},
1937
2101
  clip,
1938
- ...fig
2102
+ ...fig,
2103
+ ...group
1939
2104
  });
1940
2105
  }
1941
2106
  }
@@ -1956,9 +2121,10 @@ function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
1956
2121
  ...sh.fillGradient ? { fillGradient: sh.fillGradient } : {},
1957
2122
  ...sh.fillAlpha !== void 0 ? { fillAlpha: sh.fillAlpha } : {},
1958
2123
  ...sh.stroke ? { stroke: sh.stroke } : {},
1959
- ...sh.shadow ? { shadow: sh.shadow } : {},
2124
+ ...sh.shadow && !shadowedFill ? { shadow: sh.shadow } : {},
1960
2125
  transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
1961
2126
  },
2127
+ ...group,
1962
2128
  ...figId !== void 0 ? { structId: figId } : {}
1963
2129
  });
1964
2130
  for (const child of sh.children ?? []) emitShapeItems(child.laid, x + child.xPt, bottomYUp + sh.heightPt - child.yPt - child.laid.heightPt, sink, pageHeight, figId);
@@ -2173,24 +2339,197 @@ function runFontKeyAndParsed(options, ascii, bold, italic) {
2173
2339
  }
2174
2340
  const byFamily = options.registriesByFamily;
2175
2341
  if (byFamily && byFamily.size > 0) {
2176
- let key = resolveFamilyKey(ascii);
2342
+ const named = resolveFamilyStyle(ascii);
2343
+ let key = named.key;
2177
2344
  let reg = byFamily.get(key);
2178
2345
  if (!reg) {
2179
2346
  key = byFamily.keys().next().value;
2180
2347
  reg = byFamily.get(key);
2181
2348
  }
2182
- const { variant, parsed } = reg.resolveByStyle(bold, italic);
2349
+ const wantBold = bold || named.bold === true;
2350
+ const wantItalic = italic || named.italic === true;
2351
+ const { variant, parsed } = reg.resolveByStyle(wantBold, wantItalic);
2183
2352
  return {
2184
2353
  fontKey: `${key}:${variant}`,
2185
- parsed
2354
+ parsed,
2355
+ ...syntheticFace(variant, wantBold, wantItalic, named.widthScale)
2186
2356
  };
2187
2357
  }
2188
2358
  const { variant, parsed } = options.registry.resolveByStyle(bold, italic);
2189
2359
  return {
2190
2360
  fontKey: variant,
2191
- parsed
2361
+ parsed,
2362
+ ...syntheticFace(variant, bold, italic, resolveFamilyStyle(ascii).widthScale)
2363
+ };
2364
+ }
2365
+ /**
2366
+ * The face a CHARACTER falls back to when the run's own has no glyph for it.
2367
+ *
2368
+ * The curated substitutes are Latin families: Han, Kana, Hangul, Arabic, Thai
2369
+ * and the geometric symbols are a notdef box in every one of them, and 2145
2370
+ * characters of the corpus are exactly those. A face for the character's
2371
+ * writing system is fetched when the document holds one (see `addScriptFonts`),
2372
+ * and this is where a run reaches it — followed by every other family already
2373
+ * loaded, because a glyph one of them happens to carry is still better than a
2374
+ * box.
2375
+ *
2376
+ * Called from BOTH walks that need it — the glyph-subset collection and the
2377
+ * tokenizer — so the face a character is measured, subset and drawn with is one
2378
+ * decision made once.
2379
+ *
2380
+ * @param options The render options (the loaded registries).
2381
+ * @param cp The code point with no glyph in the run's own face.
2382
+ * @param bold Whether the run is bold…
2383
+ * @param italic …and whether it is italic.
2384
+ * @returns The face to draw it with, or `undefined` when nobody has it.
2385
+ */
2386
+ function fallbackFaceKey(options, cp, bold, italic) {
2387
+ const byFamily = options.registriesByFamily;
2388
+ if (!byFamily || byFamily.size === 0) return void 0;
2389
+ const script = scriptForCodepoint(cp, byFamily.has("jp") ? "jp" : byFamily.has("kr") ? "kr" : "sc");
2390
+ const order = script ? [script] : [];
2391
+ for (const key of byFamily.keys()) if (key !== script) order.push(key);
2392
+ for (const key of order) {
2393
+ const reg = byFamily.get(key);
2394
+ if (!reg) continue;
2395
+ const { variant, parsed } = reg.resolveByStyle(bold, italic);
2396
+ if (parsed.glyphForCodepoint(cp) !== 0) return {
2397
+ fontKey: `${key}:${variant}`,
2398
+ parsed
2399
+ };
2400
+ }
2401
+ }
2402
+ /**
2403
+ * §9.2.2 — the face a request had to settle for, and what is left to draw.
2404
+ *
2405
+ * A registry may hold one face and be asked for four: `FontBytesByVariant`
2406
+ * requires only `regular`, so a caller who supplies a single file (or a script
2407
+ * fallback with no bold cut) had every heading drawn plain. What the file does
2408
+ * not carry, the page draws: a stroke around the glyphs for weight, a shear for
2409
+ * slant — the same two tricks a word processor calls faux bold and faux italic.
2410
+ *
2411
+ * @param variant The face the registry actually gave.
2412
+ * @param bold Whether bold was asked for.
2413
+ * @param italic Whether italic was.
2414
+ * @param widthScale The squeeze a condensed NAME asks for, when it does.
2415
+ * @returns `{ synthetic }` when something is missing, `{}` when nothing is.
2416
+ */
2417
+ function syntheticFace(variant, bold, italic, widthScale) {
2418
+ const missingBold = bold && variant !== "bold" && variant !== "boldItalic";
2419
+ const missingItalic = italic && variant !== "italic" && variant !== "boldItalic";
2420
+ if (!missingBold && !missingItalic && widthScale === void 0) return {};
2421
+ return { synthetic: {
2422
+ ...missingBold ? { bold: true } : {},
2423
+ ...missingItalic ? { italic: true } : {},
2424
+ ...widthScale !== void 0 ? { widthScale } : {}
2425
+ } };
2426
+ }
2427
+ /**
2428
+ * `text` cut into the stretches ONE face draws — the whole string as a single
2429
+ * piece when the run's own face covers it, which is nearly always.
2430
+ *
2431
+ * @param plan The run being tokenized.
2432
+ * @param text The stretch of it to cut.
2433
+ */
2434
+ function splitByFace(plan, text) {
2435
+ const first = faceOf(plan, text.codePointAt(0) ?? 0);
2436
+ if (!plan.faceFor || plan.faceFor.size === 0) return [{
2437
+ text,
2438
+ face: first
2439
+ }];
2440
+ const out = [];
2441
+ let buf = "";
2442
+ let face = first;
2443
+ for (const ch of text) {
2444
+ const next = faceOf(plan, ch.codePointAt(0));
2445
+ if (buf !== "" && next.font !== face.font) {
2446
+ out.push({
2447
+ text: buf,
2448
+ face
2449
+ });
2450
+ buf = "";
2451
+ }
2452
+ face = next;
2453
+ buf += ch;
2454
+ }
2455
+ if (buf !== "") out.push({
2456
+ text: buf,
2457
+ face
2458
+ });
2459
+ return out;
2460
+ }
2461
+ /**
2462
+ * The face a character of a run is drawn with — its own, unless the run carries
2463
+ * a fallback for that code point (see {@link facesForRun}).
2464
+ */
2465
+ function faceOf(plan, cp) {
2466
+ return plan.faceFor?.get(cp) ?? {
2467
+ font: plan.font,
2468
+ ...plan.synthetic ? { synthetic: plan.synthetic } : {}
2192
2469
  };
2193
2470
  }
2471
+ /**
2472
+ * `text` cut into the stretches ONE face draws, by the same decision the
2473
+ * tokenizer makes — the whole string as a single span when the run's own face
2474
+ * covers it, which is nearly always.
2475
+ *
2476
+ * @param options The render options (the loaded registries).
2477
+ * @param primary The run's own face.
2478
+ * @param resolved Its resolved properties (weight and slant).
2479
+ * @param text The run's text.
2480
+ */
2481
+ function faceSpans(options, primary, resolved, text) {
2482
+ const out = [];
2483
+ let cur = primary;
2484
+ let buf = "";
2485
+ for (const ch of text) {
2486
+ const cp = ch.codePointAt(0);
2487
+ const face = primary.parsed.glyphForCodepoint(cp) !== 0 ? primary : fallbackFaceKey(options, cp, resolved.bold, resolved.italic) ?? primary;
2488
+ if (buf !== "" && face.fontKey !== cur.fontKey) {
2489
+ out.push({
2490
+ ...cur,
2491
+ text: buf
2492
+ });
2493
+ buf = "";
2494
+ }
2495
+ cur = face;
2496
+ buf += ch;
2497
+ }
2498
+ if (buf !== "") out.push({
2499
+ ...cur,
2500
+ text: buf
2501
+ });
2502
+ return out;
2503
+ }
2504
+ /**
2505
+ * The face for every character of `text` the run's own cannot draw.
2506
+ *
2507
+ * @param options The render options (the loaded registries).
2508
+ * @param fontResources The faces the document embeds, by key.
2509
+ * @param primary The run's own face.
2510
+ * @param resolved Its resolved properties (weight and slant).
2511
+ * @param text The run's text.
2512
+ * @returns A map from code point to face, or `undefined` when the run's own
2513
+ * face draws all of it.
2514
+ */
2515
+ function facesForRun(options, fontResources, primary, resolved, text) {
2516
+ let out;
2517
+ for (const ch of text) {
2518
+ const cp = ch.codePointAt(0);
2519
+ if (out?.has(cp) === true || primary.glyphForCodepoint(cp) !== 0) continue;
2520
+ const fallback = fallbackFaceKey(options, cp, resolved.bold, resolved.italic);
2521
+ if (!fallback) continue;
2522
+ const font = fontResources.get(fallback.fontKey);
2523
+ if (!font) continue;
2524
+ out ??= /* @__PURE__ */ new Map();
2525
+ const { variant } = { variant: fallback.fontKey.split(":")[1] ?? "" };
2526
+ out.set(cp, {
2527
+ font,
2528
+ ...syntheticFace(variant === "bold" || variant === "boldItalic" || variant === "italic" ? variant : "regular", resolved.bold, resolved.italic)
2529
+ });
2530
+ }
2531
+ return out;
2532
+ }
2194
2533
  function lookupFont(resources, fontKey) {
2195
2534
  return resources.get(fontKey) ?? resources.values().next().value;
2196
2535
  }
@@ -2207,8 +2546,22 @@ function collectFontResources(body, headersFooters, options, imageResources) {
2207
2546
  };
2208
2547
  used.set(fontKey, bucket);
2209
2548
  }
2210
- const shaped = shapeText(run.listMarker === true ? markerText(run.text, parsed) : run.text, parsed.glyphForCodepoint, parsed.advanceWidths, parsed.ligatures, parsed.kerning, parsed.joiningForms);
2211
- for (const g of shaped.gids) bucket.gids.add(g);
2549
+ const text = run.listMarker === true ? markerText(run.text, parsed) : run.text;
2550
+ for (const span of faceSpans(options, {
2551
+ fontKey,
2552
+ parsed
2553
+ }, resolved, text)) {
2554
+ let sink = used.get(span.fontKey);
2555
+ if (!sink) {
2556
+ sink = {
2557
+ parsed: span.parsed,
2558
+ gids: /* @__PURE__ */ new Set()
2559
+ };
2560
+ used.set(span.fontKey, sink);
2561
+ }
2562
+ const shaped = shapeText(span.text, span.parsed.glyphForCodepoint, span.parsed.advanceWidths, span.parsed.ligatures, span.parsed.kerning, span.parsed.joiningForms);
2563
+ for (const g of shaped.gids) sink.gids.add(g);
2564
+ }
2212
2565
  if (run.text.includes(" ")) for (const stop of resolveParagraphProperties(para.properties, options.styles).tabs) {
2213
2566
  const ch = stop.leader === void 0 ? void 0 : TAB_LEADER_CHARS.get(stop.leader);
2214
2567
  if (ch !== void 0) bucket.gids.add(parsed.glyphForCodepoint(ch.codePointAt(0)));
@@ -2578,7 +2931,7 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
2578
2931
  imageWidthPt: widthPt,
2579
2932
  imageHeightPt: heightPt,
2580
2933
  imageResourceName: res?.resourceName ?? "",
2581
- ...res?.metafile ? { imageMetafile: metafileDrawing(res.metafile, drawBox?.widthPt ?? widthPt, drawBox?.heightPt ?? heightPt, options, fontResources) } : {},
2934
+ ...res?.metafile ? { imageMetafile: metafileDrawing(res.metafile, drawBox?.widthPt ?? widthPt, drawBox?.heightPt ?? heightPt, options, fontResources, void 0, res.metafileImages) } : {},
2582
2935
  ...run.inlineImage.outline ? { imageOutline: run.inlineImage.outline } : {},
2583
2936
  ...run.inlineImage.shadow ? { imageShadow: run.inlineImage.shadow } : {},
2584
2937
  ...run.inlineImage.wash ? { imageWash: run.inlineImage.wash } : {},
@@ -2618,12 +2971,16 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
2618
2971
  };
2619
2972
  }
2620
2973
  const resolvedRun = resolveRunProperties(run.properties, paragraph.properties, options.styles);
2621
- const { fontKey } = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
2974
+ const { fontKey, synthetic } = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
2622
2975
  const script = SCRIPT_OFFSET[resolvedRun.verticalAlign] ?? 0;
2976
+ const own = lookupFont(fontResources, fontKey);
2977
+ const faceFor = facesForRun(options, fontResources, own.parsed, resolvedRun, run.text);
2623
2978
  return {
2624
2979
  run,
2625
2980
  resolvedRun,
2626
- font: lookupFont(fontResources, fontKey),
2981
+ font: own,
2982
+ ...faceFor ? { faceFor } : {},
2983
+ ...synthetic ? { synthetic } : {},
2627
2984
  fontSizePt: script === 0 ? resolvedRun.fontSizePt : resolvedRun.fontSizePt * SCRIPT_SCALE,
2628
2985
  ...script === 0 ? {} : { risePt: resolvedRun.fontSizePt * script },
2629
2986
  isImage: false,
@@ -2690,8 +3047,8 @@ function tokenizePlansLtr(plans) {
2690
3047
  continue;
2691
3048
  }
2692
3049
  const highlight = (plan.run.commentRangeRefs?.length ?? 0) > 0;
2693
- for (const t of tokenizeText(plan.run.text)) {
2694
- const text = plan.run.listMarker === true ? markerText(t.text, plan.font.parsed) : t.text;
3050
+ for (const t of tokenizeText(plan.run.text)) for (const piece of splitByFace(plan, t.text)) {
3051
+ const text = plan.run.listMarker === true ? markerText(piece.text, plan.font.parsed) : piece.text;
2695
3052
  tokens.push({
2696
3053
  kind: "text",
2697
3054
  text,
@@ -2704,10 +3061,11 @@ function tokenizePlansLtr(plans) {
2704
3061
  ...plan.run.columnBreak ? { columnBreak: true } : {},
2705
3062
  ...highlight ? { highlight: true } : {},
2706
3063
  resolvedRun: plan.resolvedRun,
2707
- font: plan.font,
3064
+ font: piece.face.font,
3065
+ ...piece.face.synthetic ? { synthetic: piece.face.synthetic } : {},
2708
3066
  fontSizePt: plan.fontSizePt,
2709
3067
  ...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
2710
- widthPt: measureRunText(plan, text),
3068
+ widthPt: measureRunText(plan, text, piece.face),
2711
3069
  bidiLevel: 0
2712
3070
  });
2713
3071
  }
@@ -2747,8 +3105,12 @@ function markerText(text, parsed) {
2747
3105
  * @param text The token's text.
2748
3106
  * @returns The advance width in points.
2749
3107
  */
2750
- function measureRunText(plan, text) {
2751
- const base = plan.font.measure.textWidthPt(text, plan.fontSizePt);
3108
+ function measureRunText(plan, text, face) {
3109
+ const drawn = face ?? {
3110
+ font: plan.font,
3111
+ ...plan.synthetic ? { synthetic: plan.synthetic } : {}
3112
+ };
3113
+ const base = drawn.font.measure.textWidthPt(text, plan.fontSizePt) * (drawn.synthetic?.widthScale ?? 1);
2752
3114
  const extra = plan.resolvedRun.letterSpacingPt;
2753
3115
  return extra === void 0 || extra === 0 ? base : base + extra * [...text].length;
2754
3116
  }
@@ -2795,6 +3157,7 @@ function tokenizePlansBidi(plans, realLevels) {
2795
3157
  let bufStart = 0;
2796
3158
  let curSpace = false;
2797
3159
  let curLevel = -1;
3160
+ let curFace = { font: plan.font };
2798
3161
  const flush = (endExclusive) => {
2799
3162
  if (endExclusive <= bufStart) return;
2800
3163
  const raw = chars.slice(bufStart, endExclusive).join("");
@@ -2810,10 +3173,11 @@ function tokenizePlansBidi(plans, realLevels) {
2810
3173
  ...plan.run.columnBreak ? { columnBreak: true } : {},
2811
3174
  ...(plan.run.commentRangeRefs?.length ?? 0) > 0 ? { highlight: true } : {},
2812
3175
  resolvedRun: plan.resolvedRun,
2813
- font: plan.font,
3176
+ font: curFace.font,
3177
+ ...curFace.synthetic ? { synthetic: curFace.synthetic } : {},
2814
3178
  fontSizePt: plan.fontSizePt,
2815
3179
  ...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
2816
- widthPt: measureRunText(plan, text),
3180
+ widthPt: measureRunText(plan, text, curFace),
2817
3181
  bidiLevel: curLevel
2818
3182
  });
2819
3183
  };
@@ -2821,14 +3185,17 @@ function tokenizePlansBidi(plans, realLevels) {
2821
3185
  const ch = chars[c];
2822
3186
  const isSpace = /\s/.test(ch);
2823
3187
  const level = realLevels[realIdx] ?? 0;
3188
+ const face = faceOf(plan, ch.codePointAt(0));
2824
3189
  if (c === 0) {
2825
3190
  curSpace = isSpace;
2826
3191
  curLevel = level;
2827
- } else if (isSpace !== curSpace || level !== curLevel) {
3192
+ curFace = face;
3193
+ } else if (isSpace !== curSpace || level !== curLevel || face.font !== curFace.font) {
2828
3194
  flush(c);
2829
3195
  bufStart = c;
2830
3196
  curSpace = isSpace;
2831
3197
  curLevel = level;
3198
+ curFace = face;
2832
3199
  }
2833
3200
  realIdx++;
2834
3201
  }
@@ -3381,6 +3748,30 @@ function alignmentOffset(alignment, lineWidth, available) {
3381
3748
  return 0;
3382
3749
  }
3383
3750
  /**
3751
+ * How far a line's INK reaches either side of its baseline — the union of its
3752
+ * glyphs' own bounding boxes, not the line box its font metrics describe. Only
3753
+ * WordArt needs this: it stretches the marks it draws onto its shape, and the
3754
+ * empty band an ascender or descender reserves is not one of them.
3755
+ *
3756
+ * @param line The laid-out line.
3757
+ * @returns Points above the baseline the ink rises, and below it that it drops.
3758
+ */
3759
+ function lineInk(line) {
3760
+ let above = 0;
3761
+ let below = 0;
3762
+ for (const tok of line.tokens) {
3763
+ if (tok.kind !== "text" || tok.isSpace) continue;
3764
+ const ink = tok.font.measure.textInkPt(tok.text, tok.fontSizePt);
3765
+ const rise = tok.risePt ?? 0;
3766
+ above = Math.max(above, ink.above + rise);
3767
+ below = Math.max(below, ink.below - rise);
3768
+ }
3769
+ return {
3770
+ above,
3771
+ below
3772
+ };
3773
+ }
3774
+ /**
3384
3775
  * §17.4.85 `w:vMerge` — a vertical merge is ONE cell, so its continuation rows
3385
3776
  * are painted with the START cell's shading. Word writes the fill on the cell
3386
3777
  * that opens the merge and nothing on the rest; painting only what is written,
@@ -3656,10 +4047,10 @@ function measureSingleLine(paragraph, options, fontResources) {
3656
4047
  let total = 0;
3657
4048
  for (const run of paragraph.runs) {
3658
4049
  const resolved = resolveRunProperties(run.properties, paragraph.properties, options.styles);
3659
- const { fontKey } = runFontKeyAndParsed(options, resolved.fontFamily.ascii, resolved.bold, resolved.italic);
4050
+ const { fontKey, synthetic } = runFontKeyAndParsed(options, resolved.fontFamily.ascii, resolved.bold, resolved.italic);
3660
4051
  const font = lookupFont(fontResources, fontKey);
3661
4052
  const fontSizePt = resolved.fontSizePt;
3662
- total += font.measure.textWidthPt(run.text, fontSizePt);
4053
+ total += font.measure.textWidthPt(run.text, fontSizePt) * (synthetic?.widthScale ?? 1);
3663
4054
  }
3664
4055
  return total;
3665
4056
  }
@@ -3800,7 +4191,9 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
3800
4191
  const padRightBase = cellMar?.right ?? tableMar?.right ?? DEFAULT_CELL_PADDING_TWIPS * TWIP_TO_PT;
3801
4192
  const padLeftPt = padLeftBase + (cell.properties.icon ? CF_ICON_GUTTER_PT : 0);
3802
4193
  const padRightPt = padRightBase;
4194
+ const turned = mergeRole === "middle" || mergeRole === "end" ? void 0 : cell.properties.textDirection;
3803
4195
  const innerWidth = Math.max(1, widthPt - padLeftPt - padRightPt);
4196
+ const measureWidth = turned ? NO_WRAP_MEASURE_WIDTH : innerWidth;
3804
4197
  const lines = [];
3805
4198
  const nestedTables = [];
3806
4199
  const cellShapes = [];
@@ -3819,7 +4212,7 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
3819
4212
  if (mergeRole !== "middle" && mergeRole !== "end") {
3820
4213
  for (const el of cell.content) if (el.kind === "paragraph") {
3821
4214
  const noWrap = cell.properties.noWrap === true;
3822
- const block = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, noWrap ? NO_WRAP_MEASURE_WIDTH : innerWidth);
4215
+ const block = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, noWrap ? NO_WRAP_MEASURE_WIDTH : measureWidth);
3823
4216
  const cutAt = widthPt - padLeftPt;
3824
4217
  if (noWrap && (block.lines[0]?.contentWidthPt ?? 0) > cutAt) clipped = true;
3825
4218
  const keep = noWrap ? clipLineToWidth(block.lines, cutAt, cell.properties.hashOnOverflow === true) : block.lines;
@@ -3870,7 +4263,9 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
3870
4263
  }
3871
4264
  const colEnd = colStart + colSpan - 1;
3872
4265
  const borders = resolveCellBorders(cell.properties.borders, tableProps.borders, rowIdx, colStart, colEnd, rowCount, colCount, mergeRole, leftNeighborBorders, aboveNeighborBorders);
3873
- const totalHeightPt = mergeRole === "middle" || mergeRole === "end" ? 0 : padTopPt + contentHeightPt + padBottomPt;
4266
+ const alongPt = turned ? lines.reduce((a, l) => Math.max(a, l.contentWidthPt), 0) : 0;
4267
+ const heightPt = turned ? alongPt : contentHeightPt;
4268
+ const totalHeightPt = mergeRole === "middle" || mergeRole === "end" ? 0 : padTopPt + heightPt + padBottomPt;
3874
4269
  return {
3875
4270
  widthPt,
3876
4271
  padTopPt,
@@ -3887,8 +4282,9 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
3887
4282
  ...nestedTables.length > 0 ? { nestedTables } : {},
3888
4283
  ...cellShapes.length > 0 ? { shapes: cellShapes } : {},
3889
4284
  ...cellFloats.length > 0 ? { floats: cellFloats } : {},
3890
- contentHeightPt,
4285
+ contentHeightPt: heightPt,
3891
4286
  totalHeightPt,
4287
+ ...turned ? { textDirection: turned } : {},
3892
4288
  ...cell.properties.verticalAlign ? { verticalAlign: cell.properties.verticalAlign } : {},
3893
4289
  colStart,
3894
4290
  colSpan,
@@ -4541,16 +4937,24 @@ var PageAssembler = class PageAssembler {
4541
4937
  * one: stretched over the page it is a blur, and its 128 pixels are 96 points
4542
4938
  * at the resolution Office measures a picture in.
4543
4939
  */
4544
- static tileItems = (resourceName, natural, box) => {
4940
+ static tileItems = (resourceName, natural, box, fromCentre = false) => {
4545
4941
  const tw = Math.max(1, natural.widthPt);
4546
4942
  const th = Math.max(1, natural.heightPt);
4547
- const cols = Math.min(200, Math.ceil(box.width / tw));
4548
- const rows = Math.min(200, Math.ceil(box.height / th));
4943
+ const first = (start, span, step) => {
4944
+ if (!fromCentre) return start;
4945
+ let at = start + span / 2 - step / 2;
4946
+ while (at > start) at -= step;
4947
+ return at;
4948
+ };
4949
+ const x0 = first(box.x, box.width, tw);
4950
+ const y0 = first(box.y, box.height, th);
4951
+ const cols = Math.min(200, Math.max(1, Math.ceil((box.x + box.width - x0) / tw)));
4952
+ const rows = Math.min(200, Math.max(1, Math.ceil((box.y + box.height - y0) / th)));
4549
4953
  const out = [];
4550
4954
  for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) out.push({
4551
4955
  type: "image",
4552
- x: pt(box.x + c * tw),
4553
- y: pt(box.y + r * th),
4956
+ x: pt(x0 + c * tw),
4957
+ y: pt(y0 + r * th),
4554
4958
  width: pt(tw),
4555
4959
  height: pt(th),
4556
4960
  imageResourceName: resourceName
@@ -4925,7 +5329,7 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
4925
5329
  const chunks = !row.cantSplit && row.heightPt > room && asm.colHasContent() && room > 0 ? splitRowIntoChunks(row, room, page) : row.heightPt > page ? splitRowIntoChunks(row, page) : [row];
4926
5330
  for (let ci = 0; ci < chunks.length; ci++) {
4927
5331
  const chunk = chunks[ci];
4928
- const forcedBreak = ci === 0 && row.breakBefore && !isLeadingHeader && asm.current.length > 0;
5332
+ const forcedBreak = ci === 0 && row.breakBefore && !isLeadingHeader && asm.pageHasContent();
4929
5333
  const overflow = asm.cursorY - chunk.heightPt < asm.bottomLimit() && asm.colHasContent();
4930
5334
  if (forcedBreak || overflow) {
4931
5335
  if (forcedBreak) asm.flushPage();
@@ -5218,6 +5622,29 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
5218
5622
  });
5219
5623
  }
5220
5624
  };
5625
+ if (cell.textDirection !== void 0) {
5626
+ const up = cell.textDirection === "vert270";
5627
+ const along = Math.max(1, boxHeightPt - cell.padTopPt - cell.padBottomPt);
5628
+ const cellBottomYUp = rowTop - boxHeightPt;
5629
+ let across = cell.padLeftPt;
5630
+ for (const line of cell.lines) {
5631
+ const h = computeLineHeight(line, line.resolved);
5632
+ const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, along);
5633
+ const off = lineBaselineOffset(line, line.resolved);
5634
+ const baselineAcross = up ? across + h - off : across + off;
5635
+ out.push({
5636
+ type: "line",
5637
+ line,
5638
+ originX: pt(up ? cellX + baselineAcross : cellX + cell.widthPt - baselineAcross),
5639
+ baselineY: pt(pageHeight - (up ? cellBottomYUp + cell.padBottomPt + lineOffset : rowTop - cell.padTopPt - lineOffset)),
5640
+ rotationDeg: up ? 90 : -90,
5641
+ ...structId !== void 0 ? { structId } : {}
5642
+ });
5643
+ across += h;
5644
+ }
5645
+ drawNestedBefore(cell.lines.length);
5646
+ continue;
5647
+ }
5221
5648
  for (const [lineIdx, line] of cell.lines.entries()) {
5222
5649
  drawNestedBefore(lineIdx);
5223
5650
  textY -= cell.lineGaps?.get(lineIdx) ?? 0;