documents.js 1.45.0 → 1.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,7 @@ npm install documents.js
30
30
 
31
31
  ## Usage
32
32
 
33
- The eight round-trip ergonomic conversions (docx/pptx/odt/odp ⇄ PDF), plus `odsToPdf`/`odgToPdf`'s one-directional additions (there is no `pdfToOds` yet — PDF spreadsheet reconstruction needs general vector-path tracking in the PDF reader that doesn't exist yet; there is no `pdfToOdg` either, for the equivalent reason on the drawing side — see [Gotchas](#gotchas-and-quirks)):
33
+ The eight round-trip ergonomic conversions (docx/pptx/odt/odp ⇄ PDF), plus `odsToPdf`/`odgToPdf`'s one-directional additions (there is no `pdfToOds`/`pdfToOdg` yet — the PDF reader's general vector-path tracking both directions need now exists (`src/pdf/interpret.ts`), but the reconstruction algorithms themselves, `reconstructSpreadsheet`/`reconstructDrawing`, are not yet built — see [Gotchas](#gotchas-and-quirks)):
34
34
 
35
35
  ```ts
36
36
  import { docxToPdf, odgToPdf, odpToPdf, odsToPdf, odtToPdf, pdfToDocx, pdfToOdp, pdfToOdt, pptxToPdf, pdfToPptx } from 'documents.js';
@@ -206,8 +206,9 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
206
206
  - **The docx⇄PDF and pptx⇄PDF conversions are explicitly not round-trip-lossless** — in deliberate contrast to `ooxml.js`'s own `packageCodec`, which is byte/part-faithful by design. See [Fidelity](#fidelity).
207
207
  - **`odpToPdf`/`pdfToOdp` needed zero new layout code.** `readOdpContent` (`src/odf/odp/read.ts`) produces the identical `presentation` `ContentDocument` shape `readPptxContent` does, so it feeds `convertPresentationToLayout` unmodified — including the existing hidden-annotation speaker-notes mechanism below, which carries odp's `presentation:notes` through to the PDF with no new notes-handling code at all; `pdfToOdp` reuses `reconstructPresentation` unmodified too, the same architectural bet `pdfToOdt` already proved for `reconstructWordprocessing`. The genuinely new work for the reverse direction was the live-view editor itself (`src/edit/odp/*`) — see Architecture above.
208
208
  - **`OdpShape.rotationDeg` writes a real `draw:transform`, built on `odf.js`'s own transform machinery.** It is the write-side inverse of `odf.js`'s `resolveOdfShapeGeometry` (`typed/shared/transform.ts`), built on that module's own exported `applyOdfTransform` rather than a hand-rolled rotation matrix, so it inherits that module's own empirically-verified rotate/translate composition order and sign convention by construction. Unlike `PptxShape` (see the `colSpan`/`rowSpan` gotcha below, which pptx still has and odp does not), `buildOdpPackage` writes a rotated shape's rotation back correctly — verified both by this package's own tests and by opening a fresh, editor-built `.odp` in actual LibreOffice.
209
- - **`odsToPdf` is one-directional there is no `pdfToOds` yet, and no `reconstructSpreadsheet`.** Unlike odt/odp, going from PDF back to a spreadsheet needs general vector-path tracking in the PDF reader (`src/pdf/interpret.ts` currently only tracks the specific `re` rectangle operator, discarding general `m`/`l`/`c` path construction) so a reconstructed sheet's gridlines can be detected from the recovered geometry that infrastructure doesn't exist yet. `buildOdsPackage` (`src/edit/ods/content.ts`) is built and exported, ready for `pdfToOds` to call the moment path tracking lands; nothing currently calls it.
210
- - **`odgToPdf` is one-directional — there is no `pdfToOdg`, and no `reconstructDrawing`, even though the odg live-view editor and `buildOdgPackage` both now exist.** Reconstructing a `ContentDrawPage`'s own vector-primitive geometry (which recovered path is a rect vs. a genuine curve, where one shape ends and another begins) from PDF geometry alone is a fundamentally different, unstarted problem from `reconstructWordprocessing`/`reconstructPresentation`'s own paragraph/shape geometry clustering — the same `src/pdf/interpret.ts` gap `odsToPdf`'s own gotcha above describes (no general `m`/`l`/`c` path tracking) blocks this direction too, on top of needing its own reconstruction algorithm even once that infrastructure exists. `buildOdgPackage` (`src/edit/odg/content.ts`) is built and exported, ready for `pdfToOdg` to call the moment both land; nothing currently calls it, mirroring `buildOdsPackage`'s own identical situation.
209
+ - **`src/pdf/interpret.ts` now tracks general vector paths, not just axis-aligned `re` rectangles.** `m`/`l`/`c`/`v`/`y`/`h` (and `re` itself, per its own ISO 32000-1 definition as a 4-point rectangle subpath) accumulate real subpaths — CTM-transformed line/cubic segments, open or closed — and any paint operator (`f`/`F`/`f*`/`S`/`s`/`B`/`B*`/`b`/`b*`) emits a `LayoutPath` item when the path isn't reducible to the simple single-`re`-on-an-axis-aligned-CTM case (which still takes the original, unchanged `LayoutRect` fast path). Verified both by dedicated tests and by a genuine `writePath` `writePdf` `readPdf` round trip recovering the original `LayoutPath` value exactly. This is the shared infrastructure both `pdfToOds` and `pdfToOdg` below need it exists now, but neither conversion function itself does yet.
210
+ - **`odsToPdf` is one-directional — there is no `pdfToOds` yet, and no `reconstructSpreadsheet`.** The PDF-side blocker (general vector-path tracking, needed to detect a reconstructed sheet's gridlines from recovered geometry) is resolved see the `interpret.ts` gotcha above. What's still missing is `reconstructSpreadsheet` itself: the actual gridline-detection/text-clustering-into-a-grid algorithm. `buildOdsPackage` (`src/edit/ods/content.ts`) is built and exported, ready for `pdfToOds` to call the moment that algorithm lands; nothing currently calls it.
211
+ - **`odgToPdf` is one-directional — there is no `pdfToOdg`, and no `reconstructDrawing`, even though the odg live-view editor and `buildOdgPackage` both now exist.** The PDF-side blocker (general vector-path tracking) is resolved — see the `interpret.ts` gotcha above. What's still missing is `reconstructDrawing` itself: mapping recovered `LayoutRect`/`LayoutPath`/`LayoutText`/`LayoutImage` geometry back onto ODF shapes (which recovered path is a rect vs. a genuine curve, where one shape ends and another begins) — a fundamentally different, unstarted problem from `reconstructWordprocessing`/`reconstructPresentation`'s own paragraph/shape geometry clustering, though genuinely more tractable since a drawing has no semantic structure to infer, only shapes to map near-1:1. `buildOdgPackage` (`src/edit/odg/content.ts`) is built and exported, ready for `pdfToOdg` to call the moment that algorithm lands; nothing currently calls it, mirroring `buildOdsPackage`'s own identical situation.
211
212
  - **A vector primitive's own fill/stroke needed a self-contained graphic-family style writer, not `odf.js`'s own `StyleRegistry`.** `'graphic'` is a recognised `StyleFamily` member (`odf.js`'s `src/styles/registry.ts`), but `StylePropertiesSchema`/`buildStylePropertyElements` (`properties.ts`/`serialize.ts`) only ever model text/paragraph formatting and never emit a `style:graphic-properties` element for any family — extending that shared package for one narrow, documents.js-local need (`draw:fill(-color)`/`draw:stroke` + `svg:stroke-color`/`svg:stroke-width`) would be scope creep into a foreign package for a two-attribute-group writer this package can express directly. `src/edit/odg/style.ts` is that writer: it still reuses `odf.js`'s general append-only style-editing invariant (a setter always mints a fresh `style:style` and repoints `draw:style-name`, never mutates an existing entry — verified by the same `assertAutomaticStylesOnlyAppended` helper `OdpEditor`'s own live-view fidelity test uses) and `src/edit/odt/automatic-styles.ts`'s `ensureAutomaticStyles`/`nextStyleName` (the "find-or-create `office:automatic-styles`, mint the next unused name" logic every other hand-rolled style helper in this package already shares), rather than a third reimplementation of either.
212
213
  - **A path vector's own `svg:d` is cross-checked against `odf.js`'s real parser, not merely asserted to "look plausible".** `src/edit/odg/svg-path.ts`'s `buildSvgPathData` is the write-side inverse of `odf.js`'s `parseOdfPathData`; `OdgPathVector.subpaths` re-derives its value by reparsing the actual written `svg:viewBox`/`svg:d` through that exact function (plus `parseOdfViewBox`/`buildOdfSubpaths`) on every read, rather than echoing back whatever `ContentSubpath[]` the caller originally passed to `addPath` — so every read is itself a live round-trip proof, and this module's own test suite additionally feeds `buildSvgPathData`'s output straight into `parseOdfPathData` to confirm point-for-point recovery.
213
214
  - **A newly added vector/shape's paint order is expressed purely as document order, with no `draw:z-index` ever written.** This matches `odf.js`'s own reader-side convention exactly (`typed/draw/shapes.ts`'s `paintOrderKey`: honour an explicit `draw:z-index` when present, otherwise fall back to document order — and real LibreOffice output never emits one, it reorders elements instead), so `OdgPage.addRect`/`addEllipse`/`addLine`/`addPath`/`addTextBox`/`addImage` simply append to `draw:page`'s own children in call order and nothing more is needed for a later `add*` call to paint in front of an earlier one.
package/dist/index.cjs CHANGED
@@ -7544,6 +7544,7 @@ function readInlineImage(reader, sink) {
7544
7544
  //#region src/pdf/interpret.ts
7545
7545
  const MAX_FORM_XOBJECT_DEPTH = 12;
7546
7546
  const FALLBACK_GLYPH_WIDTH_PER_1000 = 500;
7547
+ const DEFAULT_LINE_WIDTH_PT = 1;
7547
7548
  function defaultTextState() {
7548
7549
  return {
7549
7550
  tm: IDENTITY_MATRIX,
@@ -7636,16 +7637,118 @@ function interpretContentStream(bytes, resources, context) {
7636
7637
  runContentStream(bytes, resources, {
7637
7638
  ctm: IDENTITY_MATRIX,
7638
7639
  fillColor: document_content_model.COLOR_BLACK,
7639
- strokeColor: document_content_model.COLOR_BLACK
7640
+ strokeColor: document_content_model.COLOR_BLACK,
7641
+ lineWidth: DEFAULT_LINE_WIDTH_PT
7640
7642
  }, context, items, 0);
7641
7643
  return items;
7642
7644
  }
7645
+ function lastPointOf(subpath) {
7646
+ const last = subpath.segments[subpath.segments.length - 1];
7647
+ return last === void 0 ? {
7648
+ x: subpath.startXPt,
7649
+ y: subpath.startYPt
7650
+ } : {
7651
+ x: last.xPt,
7652
+ y: last.yPt
7653
+ };
7654
+ }
7643
7655
  function runContentStream(bytes, resources, initialState, context, items, depth) {
7644
7656
  const operations = readContentStream(bytes, context.sink);
7645
7657
  const gsStack = [];
7646
7658
  let gs = initialState;
7647
7659
  let ts = defaultTextState();
7648
7660
  let pendingRect;
7661
+ let pathSubpaths = [];
7662
+ let currentSubpath;
7663
+ const finalizeCurrentSubpath = () => {
7664
+ if (currentSubpath !== void 0) {
7665
+ pathSubpaths.push(currentSubpath);
7666
+ currentSubpath = void 0;
7667
+ }
7668
+ };
7669
+ const resetPath = () => {
7670
+ pendingRect = void 0;
7671
+ pathSubpaths = [];
7672
+ currentSubpath = void 0;
7673
+ };
7674
+ const appendRectSubpath = (operands, ctm) => {
7675
+ finalizeCurrentSubpath();
7676
+ const x = numAt(operands, 0);
7677
+ const y = numAt(operands, 1);
7678
+ const w = numAt(operands, 2);
7679
+ const h = numAt(operands, 3);
7680
+ const p1 = applyMatrix(ctm, {
7681
+ x,
7682
+ y
7683
+ });
7684
+ const p2 = applyMatrix(ctm, {
7685
+ x: x + w,
7686
+ y
7687
+ });
7688
+ const p3 = applyMatrix(ctm, {
7689
+ x: x + w,
7690
+ y: y + h
7691
+ });
7692
+ const p4 = applyMatrix(ctm, {
7693
+ x,
7694
+ y: y + h
7695
+ });
7696
+ pathSubpaths.push({
7697
+ startXPt: p1.x,
7698
+ startYPt: p1.y,
7699
+ segments: [
7700
+ {
7701
+ kind: "line",
7702
+ xPt: p2.x,
7703
+ yPt: p2.y
7704
+ },
7705
+ {
7706
+ kind: "line",
7707
+ xPt: p3.x,
7708
+ yPt: p3.y
7709
+ },
7710
+ {
7711
+ kind: "line",
7712
+ xPt: p4.x,
7713
+ yPt: p4.y
7714
+ }
7715
+ ],
7716
+ closed: true
7717
+ });
7718
+ };
7719
+ const paintFillRuleFor = (operator) => operator.endsWith("*") ? "evenodd" : "nonzero";
7720
+ const emitPaint = (operator) => {
7721
+ if (operator === "n") {
7722
+ resetPath();
7723
+ return;
7724
+ }
7725
+ if ((operator === "s" || operator === "b" || operator === "b*") && currentSubpath !== void 0) currentSubpath.closed = true;
7726
+ if ((operator === "f" || operator === "F" || operator === "f*") && currentSubpath === void 0 && pathSubpaths.length === 1 && pendingRect !== void 0) {
7727
+ items.push({
7728
+ kind: "rect",
7729
+ ...pendingRect,
7730
+ color: gs.fillColor
7731
+ });
7732
+ resetPath();
7733
+ return;
7734
+ }
7735
+ finalizeCurrentSubpath();
7736
+ if (pathSubpaths.length > 0) {
7737
+ const isFillOp = operator === "f" || operator === "F" || operator === "f*" || operator === "B" || operator === "B*" || operator === "b" || operator === "b*";
7738
+ const isStrokeOp = operator === "S" || operator === "s" || operator === "B" || operator === "B*" || operator === "b" || operator === "b*";
7739
+ items.push({
7740
+ kind: "path",
7741
+ subpaths: pathSubpaths,
7742
+ fillRule: paintFillRuleFor(operator),
7743
+ fill: isFillOp ? gs.fillColor : void 0,
7744
+ stroke: isStrokeOp ? {
7745
+ color: gs.strokeColor,
7746
+ widthPt: gs.lineWidth
7747
+ } : void 0
7748
+ });
7749
+ }
7750
+ resetPath();
7751
+ };
7649
7752
  const advanceThroughString = (codes) => {
7650
7753
  if (ts.fontResourceName === void 0) return;
7651
7754
  let offset = 0;
@@ -7829,25 +7932,118 @@ function runContentStream(bytes, resources, initialState, context, items, depth)
7829
7932
  }
7830
7933
  case "re":
7831
7934
  pendingRect = rectFromOperands(operands, gs.ctm);
7935
+ appendRectSubpath(operands, gs.ctm);
7936
+ break;
7937
+ case "m": {
7938
+ finalizeCurrentSubpath();
7939
+ const p = applyMatrix(gs.ctm, {
7940
+ x: numAt(operands, 0),
7941
+ y: numAt(operands, 1)
7942
+ });
7943
+ currentSubpath = {
7944
+ startXPt: p.x,
7945
+ startYPt: p.y,
7946
+ segments: [],
7947
+ closed: false
7948
+ };
7949
+ pendingRect = void 0;
7950
+ break;
7951
+ }
7952
+ case "l": {
7953
+ const p = applyMatrix(gs.ctm, {
7954
+ x: numAt(operands, 0),
7955
+ y: numAt(operands, 1)
7956
+ });
7957
+ currentSubpath?.segments.push({
7958
+ kind: "line",
7959
+ xPt: p.x,
7960
+ yPt: p.y
7961
+ });
7962
+ pendingRect = void 0;
7832
7963
  break;
7833
- case "m":
7834
- case "l":
7835
- case "c":
7964
+ }
7965
+ case "c": {
7966
+ const c1 = applyMatrix(gs.ctm, {
7967
+ x: numAt(operands, 0),
7968
+ y: numAt(operands, 1)
7969
+ });
7970
+ const c2 = applyMatrix(gs.ctm, {
7971
+ x: numAt(operands, 2),
7972
+ y: numAt(operands, 3)
7973
+ });
7974
+ const p = applyMatrix(gs.ctm, {
7975
+ x: numAt(operands, 4),
7976
+ y: numAt(operands, 5)
7977
+ });
7978
+ currentSubpath?.segments.push({
7979
+ kind: "cubic",
7980
+ c1xPt: c1.x,
7981
+ c1yPt: c1.y,
7982
+ c2xPt: c2.x,
7983
+ c2yPt: c2.y,
7984
+ xPt: p.x,
7985
+ yPt: p.y
7986
+ });
7987
+ pendingRect = void 0;
7988
+ break;
7989
+ }
7836
7990
  case "v":
7837
- case "y":
7991
+ if (currentSubpath !== void 0) {
7992
+ const cur = lastPointOf(currentSubpath);
7993
+ const c2 = applyMatrix(gs.ctm, {
7994
+ x: numAt(operands, 0),
7995
+ y: numAt(operands, 1)
7996
+ });
7997
+ const p = applyMatrix(gs.ctm, {
7998
+ x: numAt(operands, 2),
7999
+ y: numAt(operands, 3)
8000
+ });
8001
+ currentSubpath.segments.push({
8002
+ kind: "cubic",
8003
+ c1xPt: cur.x,
8004
+ c1yPt: cur.y,
8005
+ c2xPt: c2.x,
8006
+ c2yPt: c2.y,
8007
+ xPt: p.x,
8008
+ yPt: p.y
8009
+ });
8010
+ }
8011
+ pendingRect = void 0;
8012
+ break;
8013
+ case "y": {
8014
+ const c1 = applyMatrix(gs.ctm, {
8015
+ x: numAt(operands, 0),
8016
+ y: numAt(operands, 1)
8017
+ });
8018
+ const p = applyMatrix(gs.ctm, {
8019
+ x: numAt(operands, 2),
8020
+ y: numAt(operands, 3)
8021
+ });
8022
+ currentSubpath?.segments.push({
8023
+ kind: "cubic",
8024
+ c1xPt: c1.x,
8025
+ c1yPt: c1.y,
8026
+ c2xPt: p.x,
8027
+ c2yPt: p.y,
8028
+ xPt: p.x,
8029
+ yPt: p.y
8030
+ });
8031
+ pendingRect = void 0;
8032
+ break;
8033
+ }
7838
8034
  case "h":
8035
+ if (currentSubpath !== void 0) currentSubpath.closed = true;
7839
8036
  pendingRect = void 0;
7840
8037
  break;
8038
+ case "w":
8039
+ gs = {
8040
+ ...gs,
8041
+ lineWidth: numAt(operands, 0)
8042
+ };
8043
+ break;
7841
8044
  case "f":
7842
8045
  case "F":
7843
8046
  case "f*":
7844
- if (pendingRect !== void 0) items.push({
7845
- kind: "rect",
7846
- ...pendingRect,
7847
- color: gs.fillColor
7848
- });
7849
- pendingRect = void 0;
7850
- break;
7851
8047
  case "S":
7852
8048
  case "s":
7853
8049
  case "B":
@@ -7855,7 +8051,7 @@ function runContentStream(bytes, resources, initialState, context, items, depth)
7855
8051
  case "b":
7856
8052
  case "b*":
7857
8053
  case "n":
7858
- pendingRect = void 0;
8054
+ emitPaint(operator);
7859
8055
  break;
7860
8056
  case "BT":
7861
8057
  ts = defaultTextState();
@@ -8837,6 +9033,7 @@ function readPage(page, resolver, fontResolver, images, imageIdCache, sink) {
8837
9033
  function convertExtractedItem(item, pageMatrix, fontResolver, images, imageIdCache, resolver, sink) {
8838
9034
  if (item.kind === "text") return convertText(item, pageMatrix, fontResolver);
8839
9035
  if (item.kind === "rect") return convertRect(item, pageMatrix);
9036
+ if (item.kind === "path") return convertPath(item, pageMatrix);
8840
9037
  if (item.kind === "image") return convertImage(item, pageMatrix, images, imageIdCache, resolver, sink);
8841
9038
  return convertInlineImage(item, pageMatrix, images, resolver, sink);
8842
9039
  }
@@ -8884,6 +9081,61 @@ function convertRect(item, pageMatrix) {
8884
9081
  fill: item.color
8885
9082
  };
8886
9083
  }
9084
+ function transformSubpath(subpath, pageMatrix) {
9085
+ const start = applyMatrix(pageMatrix, {
9086
+ x: subpath.startXPt,
9087
+ y: subpath.startYPt
9088
+ });
9089
+ const segments = subpath.segments.map((segment) => {
9090
+ if (segment.kind === "line") {
9091
+ const p = applyMatrix(pageMatrix, {
9092
+ x: segment.xPt,
9093
+ y: segment.yPt
9094
+ });
9095
+ return {
9096
+ kind: "line",
9097
+ xPt: p.x,
9098
+ yPt: p.y
9099
+ };
9100
+ }
9101
+ const c1 = applyMatrix(pageMatrix, {
9102
+ x: segment.c1xPt,
9103
+ y: segment.c1yPt
9104
+ });
9105
+ const c2 = applyMatrix(pageMatrix, {
9106
+ x: segment.c2xPt,
9107
+ y: segment.c2yPt
9108
+ });
9109
+ const p = applyMatrix(pageMatrix, {
9110
+ x: segment.xPt,
9111
+ y: segment.yPt
9112
+ });
9113
+ return {
9114
+ kind: "cubic",
9115
+ c1xPt: c1.x,
9116
+ c1yPt: c1.y,
9117
+ c2xPt: c2.x,
9118
+ c2yPt: c2.y,
9119
+ xPt: p.x,
9120
+ yPt: p.y
9121
+ };
9122
+ });
9123
+ return {
9124
+ startXPt: start.x,
9125
+ startYPt: start.y,
9126
+ segments,
9127
+ closed: subpath.closed
9128
+ };
9129
+ }
9130
+ function convertPath(item, pageMatrix) {
9131
+ return {
9132
+ kind: "path",
9133
+ subpaths: item.subpaths.map((subpath) => transformSubpath(subpath, pageMatrix)),
9134
+ ...item.fill !== void 0 ? { fill: item.fill } : {},
9135
+ ...item.fill !== void 0 && item.fillRule === "evenodd" ? { fillRule: "evenodd" } : {},
9136
+ ...item.stroke !== void 0 ? { stroke: item.stroke } : {}
9137
+ };
9138
+ }
8887
9139
  function imagePlacementFrom(matrix) {
8888
9140
  const rotationDeg = matrixRotationDegrees(matrix);
8889
9141
  return {
package/dist/index.js CHANGED
@@ -7543,6 +7543,7 @@ function readInlineImage(reader, sink) {
7543
7543
  //#region src/pdf/interpret.ts
7544
7544
  const MAX_FORM_XOBJECT_DEPTH = 12;
7545
7545
  const FALLBACK_GLYPH_WIDTH_PER_1000 = 500;
7546
+ const DEFAULT_LINE_WIDTH_PT = 1;
7546
7547
  function defaultTextState() {
7547
7548
  return {
7548
7549
  tm: IDENTITY_MATRIX,
@@ -7635,16 +7636,118 @@ function interpretContentStream(bytes, resources, context) {
7635
7636
  runContentStream(bytes, resources, {
7636
7637
  ctm: IDENTITY_MATRIX,
7637
7638
  fillColor: COLOR_BLACK,
7638
- strokeColor: COLOR_BLACK
7639
+ strokeColor: COLOR_BLACK,
7640
+ lineWidth: DEFAULT_LINE_WIDTH_PT
7639
7641
  }, context, items, 0);
7640
7642
  return items;
7641
7643
  }
7644
+ function lastPointOf(subpath) {
7645
+ const last = subpath.segments[subpath.segments.length - 1];
7646
+ return last === void 0 ? {
7647
+ x: subpath.startXPt,
7648
+ y: subpath.startYPt
7649
+ } : {
7650
+ x: last.xPt,
7651
+ y: last.yPt
7652
+ };
7653
+ }
7642
7654
  function runContentStream(bytes, resources, initialState, context, items, depth) {
7643
7655
  const operations = readContentStream(bytes, context.sink);
7644
7656
  const gsStack = [];
7645
7657
  let gs = initialState;
7646
7658
  let ts = defaultTextState();
7647
7659
  let pendingRect;
7660
+ let pathSubpaths = [];
7661
+ let currentSubpath;
7662
+ const finalizeCurrentSubpath = () => {
7663
+ if (currentSubpath !== void 0) {
7664
+ pathSubpaths.push(currentSubpath);
7665
+ currentSubpath = void 0;
7666
+ }
7667
+ };
7668
+ const resetPath = () => {
7669
+ pendingRect = void 0;
7670
+ pathSubpaths = [];
7671
+ currentSubpath = void 0;
7672
+ };
7673
+ const appendRectSubpath = (operands, ctm) => {
7674
+ finalizeCurrentSubpath();
7675
+ const x = numAt(operands, 0);
7676
+ const y = numAt(operands, 1);
7677
+ const w = numAt(operands, 2);
7678
+ const h = numAt(operands, 3);
7679
+ const p1 = applyMatrix(ctm, {
7680
+ x,
7681
+ y
7682
+ });
7683
+ const p2 = applyMatrix(ctm, {
7684
+ x: x + w,
7685
+ y
7686
+ });
7687
+ const p3 = applyMatrix(ctm, {
7688
+ x: x + w,
7689
+ y: y + h
7690
+ });
7691
+ const p4 = applyMatrix(ctm, {
7692
+ x,
7693
+ y: y + h
7694
+ });
7695
+ pathSubpaths.push({
7696
+ startXPt: p1.x,
7697
+ startYPt: p1.y,
7698
+ segments: [
7699
+ {
7700
+ kind: "line",
7701
+ xPt: p2.x,
7702
+ yPt: p2.y
7703
+ },
7704
+ {
7705
+ kind: "line",
7706
+ xPt: p3.x,
7707
+ yPt: p3.y
7708
+ },
7709
+ {
7710
+ kind: "line",
7711
+ xPt: p4.x,
7712
+ yPt: p4.y
7713
+ }
7714
+ ],
7715
+ closed: true
7716
+ });
7717
+ };
7718
+ const paintFillRuleFor = (operator) => operator.endsWith("*") ? "evenodd" : "nonzero";
7719
+ const emitPaint = (operator) => {
7720
+ if (operator === "n") {
7721
+ resetPath();
7722
+ return;
7723
+ }
7724
+ if ((operator === "s" || operator === "b" || operator === "b*") && currentSubpath !== void 0) currentSubpath.closed = true;
7725
+ if ((operator === "f" || operator === "F" || operator === "f*") && currentSubpath === void 0 && pathSubpaths.length === 1 && pendingRect !== void 0) {
7726
+ items.push({
7727
+ kind: "rect",
7728
+ ...pendingRect,
7729
+ color: gs.fillColor
7730
+ });
7731
+ resetPath();
7732
+ return;
7733
+ }
7734
+ finalizeCurrentSubpath();
7735
+ if (pathSubpaths.length > 0) {
7736
+ const isFillOp = operator === "f" || operator === "F" || operator === "f*" || operator === "B" || operator === "B*" || operator === "b" || operator === "b*";
7737
+ const isStrokeOp = operator === "S" || operator === "s" || operator === "B" || operator === "B*" || operator === "b" || operator === "b*";
7738
+ items.push({
7739
+ kind: "path",
7740
+ subpaths: pathSubpaths,
7741
+ fillRule: paintFillRuleFor(operator),
7742
+ fill: isFillOp ? gs.fillColor : void 0,
7743
+ stroke: isStrokeOp ? {
7744
+ color: gs.strokeColor,
7745
+ widthPt: gs.lineWidth
7746
+ } : void 0
7747
+ });
7748
+ }
7749
+ resetPath();
7750
+ };
7648
7751
  const advanceThroughString = (codes) => {
7649
7752
  if (ts.fontResourceName === void 0) return;
7650
7753
  let offset = 0;
@@ -7828,25 +7931,118 @@ function runContentStream(bytes, resources, initialState, context, items, depth)
7828
7931
  }
7829
7932
  case "re":
7830
7933
  pendingRect = rectFromOperands(operands, gs.ctm);
7934
+ appendRectSubpath(operands, gs.ctm);
7935
+ break;
7936
+ case "m": {
7937
+ finalizeCurrentSubpath();
7938
+ const p = applyMatrix(gs.ctm, {
7939
+ x: numAt(operands, 0),
7940
+ y: numAt(operands, 1)
7941
+ });
7942
+ currentSubpath = {
7943
+ startXPt: p.x,
7944
+ startYPt: p.y,
7945
+ segments: [],
7946
+ closed: false
7947
+ };
7948
+ pendingRect = void 0;
7949
+ break;
7950
+ }
7951
+ case "l": {
7952
+ const p = applyMatrix(gs.ctm, {
7953
+ x: numAt(operands, 0),
7954
+ y: numAt(operands, 1)
7955
+ });
7956
+ currentSubpath?.segments.push({
7957
+ kind: "line",
7958
+ xPt: p.x,
7959
+ yPt: p.y
7960
+ });
7961
+ pendingRect = void 0;
7831
7962
  break;
7832
- case "m":
7833
- case "l":
7834
- case "c":
7963
+ }
7964
+ case "c": {
7965
+ const c1 = applyMatrix(gs.ctm, {
7966
+ x: numAt(operands, 0),
7967
+ y: numAt(operands, 1)
7968
+ });
7969
+ const c2 = applyMatrix(gs.ctm, {
7970
+ x: numAt(operands, 2),
7971
+ y: numAt(operands, 3)
7972
+ });
7973
+ const p = applyMatrix(gs.ctm, {
7974
+ x: numAt(operands, 4),
7975
+ y: numAt(operands, 5)
7976
+ });
7977
+ currentSubpath?.segments.push({
7978
+ kind: "cubic",
7979
+ c1xPt: c1.x,
7980
+ c1yPt: c1.y,
7981
+ c2xPt: c2.x,
7982
+ c2yPt: c2.y,
7983
+ xPt: p.x,
7984
+ yPt: p.y
7985
+ });
7986
+ pendingRect = void 0;
7987
+ break;
7988
+ }
7835
7989
  case "v":
7836
- case "y":
7990
+ if (currentSubpath !== void 0) {
7991
+ const cur = lastPointOf(currentSubpath);
7992
+ const c2 = applyMatrix(gs.ctm, {
7993
+ x: numAt(operands, 0),
7994
+ y: numAt(operands, 1)
7995
+ });
7996
+ const p = applyMatrix(gs.ctm, {
7997
+ x: numAt(operands, 2),
7998
+ y: numAt(operands, 3)
7999
+ });
8000
+ currentSubpath.segments.push({
8001
+ kind: "cubic",
8002
+ c1xPt: cur.x,
8003
+ c1yPt: cur.y,
8004
+ c2xPt: c2.x,
8005
+ c2yPt: c2.y,
8006
+ xPt: p.x,
8007
+ yPt: p.y
8008
+ });
8009
+ }
8010
+ pendingRect = void 0;
8011
+ break;
8012
+ case "y": {
8013
+ const c1 = applyMatrix(gs.ctm, {
8014
+ x: numAt(operands, 0),
8015
+ y: numAt(operands, 1)
8016
+ });
8017
+ const p = applyMatrix(gs.ctm, {
8018
+ x: numAt(operands, 2),
8019
+ y: numAt(operands, 3)
8020
+ });
8021
+ currentSubpath?.segments.push({
8022
+ kind: "cubic",
8023
+ c1xPt: c1.x,
8024
+ c1yPt: c1.y,
8025
+ c2xPt: p.x,
8026
+ c2yPt: p.y,
8027
+ xPt: p.x,
8028
+ yPt: p.y
8029
+ });
8030
+ pendingRect = void 0;
8031
+ break;
8032
+ }
7837
8033
  case "h":
8034
+ if (currentSubpath !== void 0) currentSubpath.closed = true;
7838
8035
  pendingRect = void 0;
7839
8036
  break;
8037
+ case "w":
8038
+ gs = {
8039
+ ...gs,
8040
+ lineWidth: numAt(operands, 0)
8041
+ };
8042
+ break;
7840
8043
  case "f":
7841
8044
  case "F":
7842
8045
  case "f*":
7843
- if (pendingRect !== void 0) items.push({
7844
- kind: "rect",
7845
- ...pendingRect,
7846
- color: gs.fillColor
7847
- });
7848
- pendingRect = void 0;
7849
- break;
7850
8046
  case "S":
7851
8047
  case "s":
7852
8048
  case "B":
@@ -7854,7 +8050,7 @@ function runContentStream(bytes, resources, initialState, context, items, depth)
7854
8050
  case "b":
7855
8051
  case "b*":
7856
8052
  case "n":
7857
- pendingRect = void 0;
8053
+ emitPaint(operator);
7858
8054
  break;
7859
8055
  case "BT":
7860
8056
  ts = defaultTextState();
@@ -8836,6 +9032,7 @@ function readPage(page, resolver, fontResolver, images, imageIdCache, sink) {
8836
9032
  function convertExtractedItem(item, pageMatrix, fontResolver, images, imageIdCache, resolver, sink) {
8837
9033
  if (item.kind === "text") return convertText(item, pageMatrix, fontResolver);
8838
9034
  if (item.kind === "rect") return convertRect(item, pageMatrix);
9035
+ if (item.kind === "path") return convertPath(item, pageMatrix);
8839
9036
  if (item.kind === "image") return convertImage(item, pageMatrix, images, imageIdCache, resolver, sink);
8840
9037
  return convertInlineImage(item, pageMatrix, images, resolver, sink);
8841
9038
  }
@@ -8883,6 +9080,61 @@ function convertRect(item, pageMatrix) {
8883
9080
  fill: item.color
8884
9081
  };
8885
9082
  }
9083
+ function transformSubpath(subpath, pageMatrix) {
9084
+ const start = applyMatrix(pageMatrix, {
9085
+ x: subpath.startXPt,
9086
+ y: subpath.startYPt
9087
+ });
9088
+ const segments = subpath.segments.map((segment) => {
9089
+ if (segment.kind === "line") {
9090
+ const p = applyMatrix(pageMatrix, {
9091
+ x: segment.xPt,
9092
+ y: segment.yPt
9093
+ });
9094
+ return {
9095
+ kind: "line",
9096
+ xPt: p.x,
9097
+ yPt: p.y
9098
+ };
9099
+ }
9100
+ const c1 = applyMatrix(pageMatrix, {
9101
+ x: segment.c1xPt,
9102
+ y: segment.c1yPt
9103
+ });
9104
+ const c2 = applyMatrix(pageMatrix, {
9105
+ x: segment.c2xPt,
9106
+ y: segment.c2yPt
9107
+ });
9108
+ const p = applyMatrix(pageMatrix, {
9109
+ x: segment.xPt,
9110
+ y: segment.yPt
9111
+ });
9112
+ return {
9113
+ kind: "cubic",
9114
+ c1xPt: c1.x,
9115
+ c1yPt: c1.y,
9116
+ c2xPt: c2.x,
9117
+ c2yPt: c2.y,
9118
+ xPt: p.x,
9119
+ yPt: p.y
9120
+ };
9121
+ });
9122
+ return {
9123
+ startXPt: start.x,
9124
+ startYPt: start.y,
9125
+ segments,
9126
+ closed: subpath.closed
9127
+ };
9128
+ }
9129
+ function convertPath(item, pageMatrix) {
9130
+ return {
9131
+ kind: "path",
9132
+ subpaths: item.subpaths.map((subpath) => transformSubpath(subpath, pageMatrix)),
9133
+ ...item.fill !== void 0 ? { fill: item.fill } : {},
9134
+ ...item.fill !== void 0 && item.fillRule === "evenodd" ? { fillRule: "evenodd" } : {},
9135
+ ...item.stroke !== void 0 ? { stroke: item.stroke } : {}
9136
+ };
9137
+ }
8886
9138
  function imagePlacementFrom(matrix) {
8887
9139
  const rotationDeg = matrixRotationDegrees(matrix);
8888
9140
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "documents.js",
3
- "version": "1.45.0",
3
+ "version": "1.46.1",
4
4
  "description": "Bidirectional docx/pptx <-> PDF conversion and a read+write editable OOXML document model, built on ooxml.js and Zod 4 codecs.",
5
5
  "type": "module",
6
6
  "repository": {