reamkit 1.23.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 (57) 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/document-model/types.d.ts +37 -0
  4. package/dist/esm/core/drawingml/chart-geometry.js +3 -2
  5. package/dist/esm/core/drawingml/diagram/colors.d.ts +52 -0
  6. package/dist/esm/core/drawingml/diagram/colors.js +125 -0
  7. package/dist/esm/core/drawingml/diagram/data-model.d.ts +47 -0
  8. package/dist/esm/core/drawingml/diagram/data-model.js +119 -0
  9. package/dist/esm/core/drawingml/diagram/layout-engine.d.ts +46 -0
  10. package/dist/esm/core/drawingml/diagram/layout-engine.js +948 -0
  11. package/dist/esm/core/drawingml/diagram/run.d.ts +37 -0
  12. package/dist/esm/core/drawingml/diagram/run.js +50 -0
  13. package/dist/esm/core/drawingml/diagram/to-drawing.d.ts +16 -0
  14. package/dist/esm/core/drawingml/diagram/to-drawing.js +86 -0
  15. package/dist/esm/core/drawingml/preset-geometry.js +116 -6
  16. package/dist/esm/core/drawingml/text-warp.d.ts +58 -0
  17. package/dist/esm/core/drawingml/text-warp.js +355 -0
  18. package/dist/esm/core/drawingml/theme-parser.js +9 -3
  19. package/dist/esm/core/font/measure.d.ts +12 -0
  20. package/dist/esm/core/font/measure.js +36 -0
  21. package/dist/esm/core/images.d.ts +30 -2
  22. package/dist/esm/core/images.js +121 -5
  23. package/dist/esm/core/metafile/emf.js +144 -16
  24. package/dist/esm/core/metafile/picture.d.ts +49 -0
  25. package/dist/esm/core/metafile/picture.js +70 -1
  26. package/dist/esm/core/metafile/wmf.js +53 -4
  27. package/dist/esm/core/ole/escher-blip.js +11 -1
  28. package/dist/esm/excel/print-model.js +2 -2
  29. package/dist/esm/excel/sheet-drawing.js +1 -1
  30. package/dist/esm/excel/sheet-to-flow.js +16 -2
  31. package/dist/esm/html/html-writer.js +2 -1
  32. package/dist/esm/layout/page-doc.d.ts +35 -0
  33. package/dist/esm/layout/page-doc.js +14 -1
  34. package/dist/esm/layout/styled-layout.js +176 -15
  35. package/dist/esm/pdf/shading.js +8 -6
  36. package/dist/esm/pdf/styled-page-emitter.js +42 -3
  37. package/dist/esm/pdf/vector-graphics.js +1 -1
  38. package/dist/esm/pdf-reader/image-decode.js +1 -1
  39. package/dist/esm/pptx/ppt/ppt-reader.js +95 -27
  40. package/dist/esm/pptx/ppt/ppt-text.d.ts +41 -0
  41. package/dist/esm/pptx/ppt/ppt-text.js +327 -37
  42. package/dist/esm/pptx/pptx-reader.js +90 -10
  43. package/dist/esm/pptx/slide-parser.d.ts +4 -1
  44. package/dist/esm/pptx/slide-parser.js +99 -13
  45. package/dist/esm/pptx/sp-helpers.d.ts +7 -3
  46. package/dist/esm/pptx/sp-helpers.js +45 -7
  47. package/dist/esm/pptx/table-style.d.ts +7 -0
  48. package/dist/esm/pptx/table-style.js +59 -7
  49. package/dist/esm/svg/svg-writer.js +1 -0
  50. package/dist/esm/word/document-parser.d.ts +4 -1
  51. package/dist/esm/word/document-parser.js +4 -1
  52. package/dist/esm/word/docx-reader.js +26 -13
  53. package/dist/esm/word/docx-writer.js +4 -0
  54. package/dist/esm/word/drawing-parser.d.ts +20 -0
  55. package/dist/esm/word/drawing-parser.js +37 -4
  56. package/dist/esm/word/table-parser.js +3 -0
  57. package/package.json +1 -1
@@ -1030,8 +1030,8 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1030
1030
  const fillImage = shape.fill.kind === "picture" && shape.fill.imageResource !== void 0 ? imageResources?.get(shape.fill.imageResource) : void 0;
1031
1031
  const fillImageResourceName = fillImage?.resourceName;
1032
1032
  const fillImageTile = shape.fill.kind === "picture" && shape.fill.tiled === true && fillImage?.prepared ? shape.fill.tileSizePt ?? {
1033
- widthPt: fillImage.prepared.widthPx * 72 / 96 * (shape.fill.tileScale?.sx ?? 1),
1034
- heightPt: fillImage.prepared.heightPx * 72 / 96 * (shape.fill.tileScale?.sy ?? 1)
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)
1035
1035
  } : void 0;
1036
1036
  const fillColorHex = shape.fill.kind === "solid" ? shape.fill.colorHex : fillGradient ? gradientToSolid(fillGradient) : void 0;
1037
1037
  const stroke = buildStroke(shape.line);
@@ -1049,8 +1049,9 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1049
1049
  const textLineGaps = /* @__PURE__ */ new Map();
1050
1050
  let textHeightPt = 0;
1051
1051
  const vertical = text?.vertical;
1052
+ const warp = text?.warp;
1052
1053
  if (text && text.content.length > 0) {
1053
- 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);
1054
1055
  for (const el of text.content) {
1055
1056
  if (el.kind === "image") {
1056
1057
  const res = el.image.resource ? imageResources?.get(el.image.resource) : void 0;
@@ -1143,6 +1144,16 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1143
1144
  }
1144
1145
  }
1145
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
+ }
1146
1157
  if (text?.fitToBox === true && textLines.length > 0 && fitPass < MAX_FIT_PASSES) {
1147
1158
  const innerW = Math.max(1, widthPt - insetLeftPt - insetRightPt);
1148
1159
  const innerH = Math.max(1, heightPt - insetTopPt - insetBottomPt);
@@ -1166,6 +1177,10 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1166
1177
  text: scaleTextBody(text, k)
1167
1178
  }, options, fontResources, imageResources, contentWidth, maxHeight, box, fitPass + 1);
1168
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;
1169
1184
  const paths = buildShapePaths(shape.geometry, widthPt, heightPt);
1170
1185
  const markerPaths = lineEndPaths(paths, shape.line, stroke?.widthPt ?? 0);
1171
1186
  const scale = shape.width > 0 ? widthPt / shape.width : 1;
@@ -1187,6 +1202,7 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1187
1202
  ...markerPaths.length > 0 ? { markerPaths } : {},
1188
1203
  ...fillImageResourceName ? { fillImageResourceName } : {},
1189
1204
  ...fillImageTile ? { fillImageTile } : {},
1205
+ ...fillImageTile && shape.fill.kind === "picture" && shape.fill.tileFromCentre === true ? { fillImageTileFromCentre: true } : {},
1190
1206
  ...fillImageResourceName && shape.fill.imageCrop ? { fillImageCrop: shape.fill.imageCrop } : {},
1191
1207
  ...fillImageResourceName && shape.fill.imageFillRect ? { fillImageRect: shape.fill.imageFillRect } : {},
1192
1208
  ...fillImageResourceName && shape.fill.duotone ? { fillImageDuotone: shape.fill.duotone } : {},
@@ -1202,6 +1218,12 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1202
1218
  spacingBeforePt: pp.spacingBefore ?? 0,
1203
1219
  spacingAfterPt: pp.spacingAfter ?? 0,
1204
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
+ } } : {},
1205
1227
  ...textCharts.size > 0 ? { textCharts } : {},
1206
1228
  ...textShapes.size > 0 ? { textShapes } : {},
1207
1229
  ...textTables.size > 0 ? { textTables } : {},
@@ -1884,6 +1906,10 @@ function emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId) {
1884
1906
  }
1885
1907
  return;
1886
1908
  }
1909
+ if (sh.textWarp) {
1910
+ emitWarpedText(sh, sh.textWarp, x, bottomYUp, sink, pageHeight, figId);
1911
+ return;
1912
+ }
1887
1913
  const shapeTop = bottomYUp + sh.heightPt;
1888
1914
  const innerWidth = Math.max(1, sh.widthPt - sh.insetLeftPt - sh.insetRightPt);
1889
1915
  let textY;
@@ -1923,6 +1949,65 @@ function emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId) {
1923
1949
  textY -= sh.textLineGaps?.get(i) ?? 0;
1924
1950
  });
1925
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
+ }
1926
2011
  /** The page a table is being drawn onto, as a cell's floats need to see it. */
1927
2012
  function cellAnchorCtx(asm) {
1928
2013
  return {
@@ -1962,6 +2047,19 @@ function emitCellFloat(cf, ctx, frame) {
1962
2047
  if (float.wrap !== "none") ctx.exclude(float, x, topYUp, cf.block.widthPt, cf.block.heightPt);
1963
2048
  }
1964
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
+ });
1965
2063
  if (sh.fillImageResourceName) {
1966
2064
  const clip = {
1967
2065
  paths: sh.paths,
@@ -1974,12 +2072,13 @@ function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
1974
2072
  y: pageHeight - (bottomYUp + sh.heightPt),
1975
2073
  width: sh.widthPt,
1976
2074
  height: sh.heightPt
1977
- })) {
2075
+ }, sh.fillImageTileFromCentre === true)) {
1978
2076
  if (item.type !== "image") continue;
1979
2077
  sink.push({
1980
2078
  ...item,
1981
2079
  clip,
1982
2080
  ...fig,
2081
+ ...group,
1983
2082
  ...sh.fillAlpha !== void 0 ? { alpha: sh.fillAlpha } : {}
1984
2083
  });
1985
2084
  }
@@ -2000,7 +2099,8 @@ function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
2000
2099
  ...sh.fillAlpha !== void 0 ? { alpha: sh.fillAlpha } : {},
2001
2100
  ...sh.fillImageDuotone ? { duotone: sh.fillImageDuotone } : {},
2002
2101
  clip,
2003
- ...fig
2102
+ ...fig,
2103
+ ...group
2004
2104
  });
2005
2105
  }
2006
2106
  }
@@ -2021,9 +2121,10 @@ function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
2021
2121
  ...sh.fillGradient ? { fillGradient: sh.fillGradient } : {},
2022
2122
  ...sh.fillAlpha !== void 0 ? { fillAlpha: sh.fillAlpha } : {},
2023
2123
  ...sh.stroke ? { stroke: sh.stroke } : {},
2024
- ...sh.shadow ? { shadow: sh.shadow } : {},
2124
+ ...sh.shadow && !shadowedFill ? { shadow: sh.shadow } : {},
2025
2125
  transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
2026
2126
  },
2127
+ ...group,
2027
2128
  ...figId !== void 0 ? { structId: figId } : {}
2028
2129
  });
2029
2130
  for (const child of sh.children ?? []) emitShapeItems(child.laid, x + child.xPt, bottomYUp + sh.heightPt - child.yPt - child.laid.heightPt, sink, pageHeight, figId);
@@ -3647,6 +3748,30 @@ function alignmentOffset(alignment, lineWidth, available) {
3647
3748
  return 0;
3648
3749
  }
3649
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
+ /**
3650
3775
  * §17.4.85 `w:vMerge` — a vertical merge is ONE cell, so its continuation rows
3651
3776
  * are painted with the START cell's shading. Word writes the fill on the cell
3652
3777
  * that opens the merge and nothing on the rest; painting only what is written,
@@ -4066,7 +4191,9 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
4066
4191
  const padRightBase = cellMar?.right ?? tableMar?.right ?? DEFAULT_CELL_PADDING_TWIPS * TWIP_TO_PT;
4067
4192
  const padLeftPt = padLeftBase + (cell.properties.icon ? CF_ICON_GUTTER_PT : 0);
4068
4193
  const padRightPt = padRightBase;
4194
+ const turned = mergeRole === "middle" || mergeRole === "end" ? void 0 : cell.properties.textDirection;
4069
4195
  const innerWidth = Math.max(1, widthPt - padLeftPt - padRightPt);
4196
+ const measureWidth = turned ? NO_WRAP_MEASURE_WIDTH : innerWidth;
4070
4197
  const lines = [];
4071
4198
  const nestedTables = [];
4072
4199
  const cellShapes = [];
@@ -4085,7 +4212,7 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
4085
4212
  if (mergeRole !== "middle" && mergeRole !== "end") {
4086
4213
  for (const el of cell.content) if (el.kind === "paragraph") {
4087
4214
  const noWrap = cell.properties.noWrap === true;
4088
- 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);
4089
4216
  const cutAt = widthPt - padLeftPt;
4090
4217
  if (noWrap && (block.lines[0]?.contentWidthPt ?? 0) > cutAt) clipped = true;
4091
4218
  const keep = noWrap ? clipLineToWidth(block.lines, cutAt, cell.properties.hashOnOverflow === true) : block.lines;
@@ -4136,7 +4263,9 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
4136
4263
  }
4137
4264
  const colEnd = colStart + colSpan - 1;
4138
4265
  const borders = resolveCellBorders(cell.properties.borders, tableProps.borders, rowIdx, colStart, colEnd, rowCount, colCount, mergeRole, leftNeighborBorders, aboveNeighborBorders);
4139
- 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;
4140
4269
  return {
4141
4270
  widthPt,
4142
4271
  padTopPt,
@@ -4153,8 +4282,9 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
4153
4282
  ...nestedTables.length > 0 ? { nestedTables } : {},
4154
4283
  ...cellShapes.length > 0 ? { shapes: cellShapes } : {},
4155
4284
  ...cellFloats.length > 0 ? { floats: cellFloats } : {},
4156
- contentHeightPt,
4285
+ contentHeightPt: heightPt,
4157
4286
  totalHeightPt,
4287
+ ...turned ? { textDirection: turned } : {},
4158
4288
  ...cell.properties.verticalAlign ? { verticalAlign: cell.properties.verticalAlign } : {},
4159
4289
  colStart,
4160
4290
  colSpan,
@@ -4807,16 +4937,24 @@ var PageAssembler = class PageAssembler {
4807
4937
  * one: stretched over the page it is a blur, and its 128 pixels are 96 points
4808
4938
  * at the resolution Office measures a picture in.
4809
4939
  */
4810
- static tileItems = (resourceName, natural, box) => {
4940
+ static tileItems = (resourceName, natural, box, fromCentre = false) => {
4811
4941
  const tw = Math.max(1, natural.widthPt);
4812
4942
  const th = Math.max(1, natural.heightPt);
4813
- const cols = Math.min(200, Math.ceil(box.width / tw));
4814
- 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)));
4815
4953
  const out = [];
4816
4954
  for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) out.push({
4817
4955
  type: "image",
4818
- x: pt(box.x + c * tw),
4819
- y: pt(box.y + r * th),
4956
+ x: pt(x0 + c * tw),
4957
+ y: pt(y0 + r * th),
4820
4958
  width: pt(tw),
4821
4959
  height: pt(th),
4822
4960
  imageResourceName: resourceName
@@ -5191,7 +5329,7 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
5191
5329
  const chunks = !row.cantSplit && row.heightPt > room && asm.colHasContent() && room > 0 ? splitRowIntoChunks(row, room, page) : row.heightPt > page ? splitRowIntoChunks(row, page) : [row];
5192
5330
  for (let ci = 0; ci < chunks.length; ci++) {
5193
5331
  const chunk = chunks[ci];
5194
- const forcedBreak = ci === 0 && row.breakBefore && !isLeadingHeader && asm.current.length > 0;
5332
+ const forcedBreak = ci === 0 && row.breakBefore && !isLeadingHeader && asm.pageHasContent();
5195
5333
  const overflow = asm.cursorY - chunk.heightPt < asm.bottomLimit() && asm.colHasContent();
5196
5334
  if (forcedBreak || overflow) {
5197
5335
  if (forcedBreak) asm.flushPage();
@@ -5484,6 +5622,29 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
5484
5622
  });
5485
5623
  }
5486
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
+ }
5487
5648
  for (const [lineIdx, line] of cell.lines.entries()) {
5488
5649
  drawNestedBefore(lineIdx);
5489
5650
  textY -= cell.lineGaps?.get(lineIdx) ?? 0;
@@ -284,14 +284,16 @@ function normalizeStops(stops) {
284
284
  colorHex: "000000",
285
285
  alpha: 1
286
286
  }];
287
- out[0] = {
288
- ...out[0],
287
+ const first = out[0];
288
+ if (first.offset > 0) out.unshift({
289
+ ...first,
289
290
  offset: 0
290
- };
291
- out[out.length - 1] = {
292
- ...out[out.length - 1],
291
+ });
292
+ const last = out[out.length - 1];
293
+ if (last.offset < 1) out.push({
294
+ ...last,
293
295
  offset: 1
294
- };
296
+ });
295
297
  return out;
296
298
  }
297
299
  function rgb01(hex) {
@@ -7,6 +7,7 @@ import { GLUE_SHRINK_RATIO } from "../layout/styled-layout.js";
7
7
  import { paintPlan } from "../layout/page-doc.js";
8
8
  import { emitClipPath, emitVectorShape, shadowBlurLayers } from "./vector-graphics.js";
9
9
  import { buildGradientAlphaMask, buildGradientPattern, shapeBbox } from "./shading.js";
10
+ import { warpGlyphMatrix } from "../core/drawingml/text-warp.js";
10
11
  import { sanitizeHref } from "../core/links.js";
11
12
  import { embedAssociatedFile } from "./embedded-file.js";
12
13
  import { buildSrgbIccProfile } from "./icc-profile.js";
@@ -111,7 +112,11 @@ function assembleStyledPdf(laid, options, doc) {
111
112
  if (!pdfaProfile) for (const page of renderedPages) {
112
113
  const ph = page.height;
113
114
  const plan = paintPlan(page.commands);
114
- const shapesNeedingPatterns = [...plan.shapes, ...[...plan.behind, ...plan.pictures.flat()].filter((i) => i.type === "shape")];
115
+ const shapesNeedingPatterns = [...plan.shapes, ...[
116
+ ...plan.behind,
117
+ ...plan.pictures.flat(),
118
+ ...plan.ordered.flat()
119
+ ].filter((i) => i.type === "shape")];
115
120
  for (const item of shapesNeedingPatterns) {
116
121
  const gradient = item.shape.fillGradient;
117
122
  if (!gradient) continue;
@@ -211,7 +216,11 @@ function assembleStyledPdf(laid, options, doc) {
211
216
  wantAlpha(img.alpha);
212
217
  wantDuotone(img, page.height);
213
218
  }
214
- for (const item of [...plan.behind, ...plan.pictures.flat()]) {
219
+ for (const item of [
220
+ ...plan.behind,
221
+ ...plan.pictures.flat(),
222
+ ...plan.ordered.flat()
223
+ ]) {
215
224
  if (item.type === "image") {
216
225
  wantAlpha(washVeil(item.wash)?.alpha);
217
226
  wantAlpha(item.alpha);
@@ -1009,6 +1018,34 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
1009
1018
  }
1010
1019
  const hasRise = line.tokens.some((t) => t.kind === "text" && t.risePt !== void 0);
1011
1020
  const hasFauxItalic = line.tokens.some((t) => t.kind === "text" && t.synthetic?.italic === true);
1021
+ const warp = cmd.warp;
1022
+ if (warp) {
1023
+ if (!inBT) {
1024
+ out.push("BT");
1025
+ inBT = true;
1026
+ }
1027
+ let cursor = originX;
1028
+ for (const tok of line.tokens) {
1029
+ if (tok.kind !== "text" || tok.isSpace) {
1030
+ cursor += tok.widthPt;
1031
+ continue;
1032
+ }
1033
+ switchFontIfNeeded(tok);
1034
+ const chars = [...tok.text];
1035
+ const raw = chars.map((c) => tok.font.measure.textWidthPt(c, tok.fontSizePt));
1036
+ const total = raw.reduce((a, b) => a + b, 0);
1037
+ const k = total > 0 ? tok.widthPt / total : 1;
1038
+ for (let i = 0; i < chars.length; i++) {
1039
+ const adv = raw[i] * k;
1040
+ const m = warpGlyphMatrix(warp, cursor, adv, cmd.baselineY);
1041
+ cursor += adv;
1042
+ if (!m) continue;
1043
+ out.push(`${formatNumber(m[0])} ${formatNumber(-m[1])} ${formatNumber(m[2])} ${formatNumber(-m[3])} ${formatNumber(m[4])} ${formatNumber(H - m[5])} Tm`);
1044
+ out.push(tok.font.measure.showText(chars[i]));
1045
+ }
1046
+ }
1047
+ return;
1048
+ }
1012
1049
  if (rotationDeg !== 0 && !hasImageToken && !hasMathToken) {
1013
1050
  if (!inBT) {
1014
1051
  out.push("BT");
@@ -1104,6 +1141,7 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
1104
1141
  if (item.type === "shape") {
1105
1142
  const t = item.shape.transform;
1106
1143
  const fillAlpha = item.shape.fillAlpha;
1144
+ const layer = item.shape.shadow ? shadowBlurLayers(item.shape.shadow) : void 0;
1107
1145
  for (const op of emitVectorShape({
1108
1146
  ...item.shape,
1109
1147
  transform: [
@@ -1114,7 +1152,7 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
1114
1152
  t[4],
1115
1153
  H - t[5]
1116
1154
  ]
1117
- }, gradientNames?.get(item.shape), void 0, gradientMaskNames?.get(item.shape) ?? (fillAlpha !== void 0 && fillAlpha < 1 ? alphaStateNames?.get(Math.round(fillAlpha * 1e3) / 1e3) : void 0))) out.push(op);
1155
+ }, gradientNames?.get(item.shape), layer && layer.alpha < 1 ? alphaStateNames?.get(Math.round(layer.alpha * 1e3) / 1e3) : void 0, gradientMaskNames?.get(item.shape) ?? (fillAlpha !== void 0 && fillAlpha < 1 ? alphaStateNames?.get(Math.round(fillAlpha * 1e3) / 1e3) : void 0))) out.push(op);
1118
1156
  return;
1119
1157
  }
1120
1158
  if (item.type === "image") {
@@ -1287,6 +1325,7 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
1287
1325
  for (const item of run) emitInOrder(item);
1288
1326
  if (tagging) out.push("EMC");
1289
1327
  }
1328
+ for (const run of plan.ordered) for (const item of run) emitInOrder(item);
1290
1329
  emitLinesPass();
1291
1330
  return {
1292
1331
  content: encoder.encode(out.join("\n")),
@@ -31,7 +31,7 @@ function shadowBlurLayers(shadow) {
31
31
  const count = Math.min(8, Math.max(2, Math.round(shadow.blurPt / 2)));
32
32
  return {
33
33
  count,
34
- alpha: shadow.alpha / count
34
+ alpha: 1 - Math.pow(1 - shadow.alpha, 1 / count)
35
35
  };
36
36
  }
37
37
  function shadowBbox(shape) {
@@ -1,6 +1,6 @@
1
1
  import { PDF_NULL, PdfHexString, PdfName, PdfStream } from "../pdf/objects.js";
2
- import { lzwDecodeMsb } from "../core/lzw.js";
3
2
  import { encodePng } from "../core/png-encode.js";
3
+ import { lzwDecodeMsb } from "../core/lzw.js";
4
4
  import { reversePredictor } from "./predictor.js";
5
5
  import { decodeCcitt } from "./ccitt.js";
6
6
  import { unzlibSync } from "fflate";
@@ -1,6 +1,7 @@
1
1
  import { pt } from "../../core/ir/units.js";
2
2
  import { ResourceStore } from "../../core/ir/resources.js";
3
3
  import { FEATURES } from "../../core/ir/features.js";
4
+ import { knockOutColor } from "../../core/images.js";
4
5
  import { isEmf } from "../../core/metafile/emf.js";
5
6
  import { isWmf } from "../../core/metafile/wmf.js";
6
7
  import { EMPTY_STYLE_SHEET, resolveBodyStyles } from "../../core/style-cascade/resolver.js";
@@ -136,8 +137,9 @@ function bulletedRuns(para) {
136
137
  const runs = para.runs.map(toRun);
137
138
  if (para.bullet === void 0) return runs;
138
139
  const { bold: _b, italic: _i, ...rest } = runs[0]?.properties ?? {};
140
+ const gap = hangs(para) ? " " : " ";
139
141
  return [{
140
- text: `${para.bullet} `,
142
+ text: `${para.bullet}${gap}`,
141
143
  properties: rest
142
144
  }, ...runs];
143
145
  }
@@ -175,16 +177,13 @@ function backdropFill(bg, resources) {
175
177
  if (bg.image) return {
176
178
  kind: "picture",
177
179
  imageResource: resources.put(bg.image.bytes),
178
- ...bg.imageTiled ? { tiled: true } : {}
180
+ ...bg.imageTiled ? {
181
+ tiled: true,
182
+ tileFromCentre: true
183
+ } : {}
179
184
  };
180
185
  if (bg.gradient) {
181
- const stops = [{
182
- offset: 0,
183
- colorHex: bg.gradient.fromHex
184
- }, {
185
- offset: 1,
186
- colorHex: bg.gradient.toHex
187
- }];
186
+ const stops = gradientStops(bg.gradient);
188
187
  return {
189
188
  kind: "gradient",
190
189
  gradient: bg.gradient.radial ? {
@@ -242,9 +241,10 @@ function positionedImage(rect, image, resources) {
242
241
  kind: "image",
243
242
  image: {
244
243
  float: floatAt(rect),
245
- resource: resources.put(image.bytes),
244
+ resource: resources.put(drawnBytes(image)),
246
245
  width: pt(rect.w),
247
246
  height: pt(rect.h),
247
+ ...image.crop ? { crop: image.crop } : {},
248
248
  paragraphProperties: {}
249
249
  }
250
250
  };
@@ -290,34 +290,72 @@ function toCustomGeometry(g) {
290
290
  commands
291
291
  };
292
292
  }
293
+ /**
294
+ * §2.3.7.6 `fillFocus` — the stops a shaded fill's two colours make.
295
+ *
296
+ * A focus of zero is the plain ramp everyone expects: the first colour at one
297
+ * end, the second at the other. Any other value means the sweep runs OUT from
298
+ * one of them — a focus of 50 puts the first colour halfway along with the
299
+ * second at BOTH ends, which is what 23884's every slide asks for and why its
300
+ * background read as a flat wash into violet where the reference is blue
301
+ * across the middle and dark only at the corners.
302
+ *
303
+ * @param g The gradient as the `.ppt` states it.
304
+ * @returns The stops, in order.
305
+ */
306
+ function gradientStops(g) {
307
+ const focus = g.focusPct ?? 0;
308
+ if (focus === 0) return [{
309
+ offset: 0,
310
+ colorHex: g.fromHex
311
+ }, {
312
+ offset: 1,
313
+ colorHex: g.toHex
314
+ }];
315
+ if (Math.abs(focus) >= 100) return [{
316
+ offset: 0,
317
+ colorHex: g.toHex
318
+ }, {
319
+ offset: 1,
320
+ colorHex: g.fromHex
321
+ }];
322
+ const peak = focus > 0 ? g.fromHex : g.toHex;
323
+ const ends = focus > 0 ? g.toHex : g.fromHex;
324
+ const at = focus > 0 ? focus / 100 : 1 + focus / 100;
325
+ return [
326
+ {
327
+ offset: 0,
328
+ colorHex: ends
329
+ },
330
+ {
331
+ offset: at,
332
+ colorHex: peak
333
+ },
334
+ {
335
+ offset: 1,
336
+ colorHex: ends
337
+ }
338
+ ];
339
+ }
293
340
  function positionedAutoShape(rect, auto, resources) {
294
341
  const line = isLineShape(auto.shapeType);
295
342
  const fill = line ? { kind: "none" } : auto.image ? {
296
343
  kind: "picture",
297
344
  imageResource: resources.put(auto.image.bytes),
298
- ...auto.imageTiled ? { tiled: true } : {},
345
+ ...auto.imageTiled ? {
346
+ tiled: true,
347
+ tileFromCentre: true
348
+ } : {},
299
349
  ...auto.tileSizePt ? { tileSizePt: auto.tileSizePt } : {}
300
350
  } : auto.gradient ? {
301
351
  kind: "gradient",
302
352
  gradient: auto.gradient.radial ? {
303
353
  kind: "radial",
304
- stops: [{
305
- offset: 0,
306
- colorHex: auto.gradient.fromHex
307
- }, {
308
- offset: 1,
309
- colorHex: auto.gradient.toHex
310
- }]
354
+ stops: gradientStops(auto.gradient)
311
355
  } : {
312
356
  kind: "linear",
313
357
  angle: auto.gradient.angleDeg,
314
- stops: [{
315
- offset: 0,
316
- colorHex: auto.gradient.fromHex
317
- }, {
318
- offset: 1,
319
- colorHex: auto.gradient.toHex
320
- }]
358
+ stops: gradientStops(auto.gradient)
321
359
  }
322
360
  } : auto.fillColorHex ? {
323
361
  kind: "solid",
@@ -353,13 +391,30 @@ function inFlowImage(image, resources) {
353
391
  return {
354
392
  kind: "image",
355
393
  image: {
356
- resource: resources.put(image.bytes),
394
+ resource: resources.put(drawnBytes(image)),
357
395
  width: pt(size.w * .75),
358
396
  height: pt(size.h * .75),
397
+ ...image.crop ? { crop: image.crop } : {},
359
398
  paragraphProperties: {}
360
399
  }
361
400
  };
362
401
  }
402
+ /**
403
+ * The bytes a picture is actually DRAWN from.
404
+ *
405
+ * §2.3.23 `pictureTransparent` names a colour the picture is drawn without, and
406
+ * a raster carries no alpha channel to say so — so the knock-out is baked into
407
+ * bytes of its own. The resource store hashes content, so two shapes knocking
408
+ * different colours out of the same blip stay two resources by themselves, and
409
+ * one that knocks out nothing keeps the bytes it arrived with.
410
+ *
411
+ * @param image The picture as the shape states it.
412
+ * @returns The bytes to store, knocked out where the shape asks for it.
413
+ */
414
+ function drawnBytes(image) {
415
+ if (image.transparentHex === void 0 || isMetafile(image.bytes)) return image.bytes;
416
+ return knockOutColor(image.bytes, image.transparentHex) ?? image.bytes;
417
+ }
363
418
  function isMetafile(d) {
364
419
  return isEmf(d) || isWmf(d);
365
420
  }
@@ -418,12 +473,25 @@ function toRun(r) {
418
473
  function toParaProperties(p, breakBefore) {
419
474
  const alignment = p.align !== void 0 ? alignmentFrom(p.align) : void 0;
420
475
  const level = p.level ?? 0;
476
+ const leftPt = p.leftMarginPt ?? (level > 0 ? level * LEVEL_INDENT_PT : void 0);
477
+ const firstPt = p.indentPt !== void 0 ? p.indentPt - (leftPt ?? 0) : void 0;
421
478
  return {
422
479
  ...breakBefore ? { pageBreakBefore: true } : {},
423
480
  ...alignment ? { alignment } : {},
424
- ...level > 0 ? { indentLeft: pt(level * LEVEL_INDENT_PT) } : {}
481
+ ...leftPt !== void 0 ? { indentLeft: pt(leftPt) } : {},
482
+ ...firstPt !== void 0 && firstPt !== 0 ? { indentFirstLine: pt(firstPt) } : {},
483
+ ...p.spaceBeforePt !== void 0 ? { spacingBefore: pt(p.spaceBeforePt) } : {},
484
+ ...p.spaceAfterPt !== void 0 ? { spacingAfter: pt(p.spaceAfterPt) } : {},
485
+ ...p.lineSpacing !== void 0 && p.lineSpacing > 0 ? {
486
+ spacingLineRule: "auto",
487
+ spacingLine: pt(p.lineSpacing / 100 * 12)
488
+ } : {}
425
489
  };
426
490
  }
491
+ /** Whether the paragraph's first line starts LEFT of its body — a hanging bullet. */
492
+ function hangs(p) {
493
+ return p.indentPt !== void 0 && p.leftMarginPt !== void 0 && p.indentPt < p.leftMarginPt;
494
+ }
427
495
  function alignmentFrom(a) {
428
496
  switch (a) {
429
497
  case 1: return "center";