pdf-codec 3.2.0 → 3.3.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
@@ -87,6 +87,8 @@ An encrypted PDF that opens without a password decrypts transparently — no ext
87
87
 
88
88
  Both accept an optional `signal` (`AbortSignal`); `readPdf` additionally takes a `sink` (`PdfDiagnosticSink`, called once per recoverable parse diagnostic — see the three-tier failure policy under [Conventions](#conventions)), and `writePdf` an `onSubstitution` callback (called once per character not representable in a standard-14 font — see [Fidelity](#fidelity)).
89
89
 
90
+ **Page boundaries: the crop box is the visible region** (ISO 32000-1 14.11.2). A page's reported `widthPt`/`heightPt` and coordinate frame come from its effective `/CropBox` — the rectangle a viewer displays and prints, inherited through the page tree like `/MediaBox` and defaulting to it — not from the media box: content an author placed outside the crop box (printer's marks, bleed, off-page slugs) is not visible and does not extract. Content wholly outside the crop box is dropped; content straddling the boundary keeps its original unclipped geometry (the item layer records what the file states — a viewer's clipping is a rendering fact, not source data); link annotations are anchored constructs rather than painted content and are never filtered, the same line the optional-content filter draws. The declared boundary rectangles a distinct crop box hides — plus `/BleedBox`/`/TrimBox`/`/ArtBox`, print-production facts with no field in the layout model — are quarantined verbatim as the `page-boxes` package-level residue row rather than silently dropped. A degenerate `/CropBox` (zero width or height) falls back to the media box with a `pdf/invalid-crop-box` warning.
91
+
90
92
  **Cancellation granularity and cost, for CPU-metered runtimes.** Both pipelines are synchronous end to end — there is no `await` point for cancellation to hook into implicitly — so the `signal` is checked explicitly, once per page-loop iteration (and once before `readPdf`'s document-open phase begins). A signal aborted mid-parse therefore takes effect at the next page boundary, not instantly: `readPdf`'s document-open phase (cross-reference resolution, object parsing) and a single page's content-stream interpretation are the two spans that cannot be interrupted, and a document consisting of one enormous page is effectively uninterruptible however many pages it claims. Cost is roughly linear in decompressed content length, so budget for the worst single page, not the page count. On Cloudflare Workers this is the honest shape of the trade: the parse holds the isolate for its whole duration with no opportunity to yield or report progress, and an `AbortSignal` shared with whatever can abort concurrently (a binding, another context) makes a deadline enforceable at page granularity — but it cannot convert a synchronous parse into a resumable one. An async page-at-a-time API is a deliberate non-goal of this package's current surface.
91
93
 
92
94
  The same round trip is also available as a schema-validated [`z.codec()`](https://zod.dev) pair:
package/dist/read.cjs CHANGED
@@ -53,10 +53,15 @@ function readPdf(bytes, options) {
53
53
  const optionalContent = require_optional_content.readOptionalContent(doc.catalog, doc, sink);
54
54
  const form = require_form.readAcroForm(doc.catalog, doc, (obj) => doc.pageIndex(obj), sink);
55
55
  const source = readDocumentResidue(doc, sink);
56
- const pages = pageDicts.map((pageDict) => {
56
+ const pageBoxRows = [];
57
+ const pages = pageDicts.map((pageDict, index) => {
57
58
  require_util_abort.throwIfAborted(signal);
58
- return readPage(pageDict, doc, fontResolver, images, imageIdCache, destinationRegistry, optionalContent.layerNameOf, sink);
59
+ return readPage(index, pageDict, doc, fontResolver, images, imageIdCache, destinationRegistry, optionalContent.layerNameOf, pageBoxRows, sink);
59
60
  });
61
+ if (pageBoxRows.length > 0) source["page-boxes"] = {
62
+ format: "pdf",
63
+ xml: require_serialize.serializeObjectToText(require_objects.pdfArray(pageBoxRows))
64
+ };
60
65
  return {
61
66
  formatVersion: 1,
62
67
  metadata: readMetadata(doc.trailer, doc, doc.catalog, sink),
@@ -70,18 +75,13 @@ function readPdf(bytes, options) {
70
75
  ...Object.keys(source).length > 0 ? { source } : {}
71
76
  };
72
77
  }
73
- function readMediaBox(page) {
74
- const arr = require_objects.asArray(require_objects.dictGet(page, "MediaBox"));
75
- if (arr === void 0) return {
76
- llx: 0,
77
- lly: 0,
78
- urx: DEFAULT_PAGE_WIDTH_PT,
79
- ury: DEFAULT_PAGE_HEIGHT_PT
80
- };
78
+ function readDeclaredPageBox(page, key) {
79
+ const arr = require_objects.asArray(require_objects.dictGet(page, key));
80
+ if (arr === void 0) return;
81
81
  const a = require_objects.asNumber(arr[0]) ?? 0;
82
82
  const b = require_objects.asNumber(arr[1]) ?? 0;
83
- const c = require_objects.asNumber(arr[2]) ?? DEFAULT_PAGE_WIDTH_PT;
84
- const d = require_objects.asNumber(arr[3]) ?? DEFAULT_PAGE_HEIGHT_PT;
83
+ const c = require_objects.asNumber(arr[2]) ?? 0;
84
+ const d = require_objects.asNumber(arr[3]) ?? 0;
85
85
  return {
86
86
  llx: Math.min(a, c),
87
87
  lly: Math.min(b, d),
@@ -89,6 +89,33 @@ function readMediaBox(page) {
89
89
  ury: Math.max(b, d)
90
90
  };
91
91
  }
92
+ function readMediaBox(page) {
93
+ return readDeclaredPageBox(page, "MediaBox") ?? {
94
+ llx: 0,
95
+ lly: 0,
96
+ urx: DEFAULT_PAGE_WIDTH_PT,
97
+ ury: DEFAULT_PAGE_HEIGHT_PT
98
+ };
99
+ }
100
+ function rotatedRectBounds(rect, matrix) {
101
+ const transformed = [
102
+ [rect.llx, rect.lly],
103
+ [rect.urx, rect.lly],
104
+ [rect.llx, rect.ury],
105
+ [rect.urx, rect.ury]
106
+ ].map(([x, y]) => require_matrix.applyMatrix(matrix, {
107
+ x,
108
+ y
109
+ }));
110
+ const xs = transformed.map((point) => point.x);
111
+ const ys = transformed.map((point) => point.y);
112
+ return {
113
+ minX: Math.min(...xs),
114
+ minY: Math.min(...ys),
115
+ maxX: Math.max(...xs),
116
+ maxY: Math.max(...ys)
117
+ };
118
+ }
92
119
  function normalizeRotation(rotate) {
93
120
  if (rotate === void 0) return 0;
94
121
  const normalized = (Math.round(rotate / 90) * 90 % 360 + 360) % 360;
@@ -157,11 +184,114 @@ function readPageContentBytes(page, resolver, sink) {
157
184
  }
158
185
  return /* @__PURE__ */ new Uint8Array(0);
159
186
  }
160
- function readPage(page, resolver, fontResolver, images, imageIdCache, destinationRegistry, layerNameOf, sink) {
187
+ function contentItemBounds(item) {
188
+ if (item.kind === "link" || item.kind === "internalLink") return;
189
+ const frameBounds = (xPt, yPt, widthPt, heightPt) => ({
190
+ minX: xPt,
191
+ minY: yPt,
192
+ maxX: xPt + widthPt,
193
+ maxY: yPt + heightPt
194
+ });
195
+ if (item.kind === "text" || item.kind === "image" || item.kind === "rect" || item.kind === "ellipse") {
196
+ if (item.kind !== "text" || item.rotationDeg === void 0 || item.rotationDeg % 180 === 0) return frameBounds(item.xPt, item.yPt, item.widthPt ?? 0, item.kind === "text" ? item.sizePt : item.heightPt);
197
+ const rotationDeg = item.rotationDeg;
198
+ const anchorX = item.xPt;
199
+ const anchorY = item.yPt;
200
+ const rotated = [
201
+ [0, 0],
202
+ [item.widthPt ?? 0, 0],
203
+ [0, item.sizePt],
204
+ [item.widthPt ?? 0, item.sizePt]
205
+ ].map(([dx, dy]) => require_matrix.rotatePointAboutCenter({
206
+ x: anchorX + dx,
207
+ y: anchorY + dy
208
+ }, {
209
+ x: anchorX,
210
+ y: anchorY
211
+ }, rotationDeg));
212
+ const xs = rotated.map((point) => point.x);
213
+ const ys = rotated.map((point) => point.y);
214
+ return {
215
+ minX: Math.min(...xs),
216
+ minY: Math.min(...ys),
217
+ maxX: Math.max(...xs),
218
+ maxY: Math.max(...ys)
219
+ };
220
+ }
221
+ if (item.kind === "line") return {
222
+ minX: Math.min(item.x1Pt, item.x2Pt),
223
+ minY: Math.min(item.y1Pt, item.y2Pt),
224
+ maxX: Math.max(item.x1Pt, item.x2Pt),
225
+ maxY: Math.max(item.y1Pt, item.y2Pt)
226
+ };
227
+ let minX = Number.POSITIVE_INFINITY;
228
+ let minY = Number.POSITIVE_INFINITY;
229
+ let maxX = Number.NEGATIVE_INFINITY;
230
+ let maxY = Number.NEGATIVE_INFINITY;
231
+ const include = (x, y) => {
232
+ minX = Math.min(minX, x);
233
+ minY = Math.min(minY, y);
234
+ maxX = Math.max(maxX, x);
235
+ maxY = Math.max(maxY, y);
236
+ };
237
+ for (const subpath of item.subpaths) {
238
+ include(subpath.startXPt, subpath.startYPt);
239
+ for (const segment of subpath.segments) {
240
+ include(segment.xPt, segment.yPt);
241
+ if (segment.kind === "cubic") {
242
+ include(segment.c1xPt, segment.c1yPt);
243
+ include(segment.c2xPt, segment.c2yPt);
244
+ }
245
+ }
246
+ }
247
+ return {
248
+ minX,
249
+ minY,
250
+ maxX,
251
+ maxY
252
+ };
253
+ }
254
+ function itemIntersectsVisibleRegion(item, widthPt, heightPt) {
255
+ if (item.kind === "link" || item.kind === "internalLink") return true;
256
+ const bounds = contentItemBounds(item);
257
+ if (bounds === void 0) return true;
258
+ return bounds.maxX >= 0 && bounds.minX <= widthPt && bounds.maxY >= 0 && bounds.minY <= heightPt;
259
+ }
260
+ function pageBoxResidueEntry(pageIndex, page, mediaBox, cropBox) {
261
+ const declaredBoxes = [
262
+ ["MediaBox", require_objects.dictGet(page, "MediaBox")],
263
+ ["CropBox", require_objects.dictGet(page, "CropBox")],
264
+ ["BleedBox", require_objects.dictGet(page, "BleedBox")],
265
+ ["TrimBox", require_objects.dictGet(page, "TrimBox")],
266
+ ["ArtBox", require_objects.dictGet(page, "ArtBox")]
267
+ ];
268
+ const cropDeclared = require_objects.dictGet(page, "CropBox") !== void 0;
269
+ const productionBoxDeclared = declaredBoxes.slice(2).some(([, value]) => value !== void 0);
270
+ if (!(cropDeclared && (cropBox.llx !== mediaBox.llx || cropBox.lly !== mediaBox.lly || cropBox.urx !== mediaBox.urx || cropBox.ury !== mediaBox.ury)) && !productionBoxDeclared) return;
271
+ const entries = { Page: require_objects.pdfNum(pageIndex) };
272
+ for (const [key, value] of declaredBoxes) if (value !== void 0) entries[key] = value;
273
+ return require_objects.pdfDict(entries);
274
+ }
275
+ function readPage(pageIndex, page, resolver, fontResolver, images, imageIdCache, destinationRegistry, layerNameOf, pageBoxRows, sink) {
161
276
  const resources = resolver.resolveDict(require_objects.dictGet(page, "Resources"));
162
277
  const mediaBox = readMediaBox(page);
278
+ let cropBox = readDeclaredPageBox(page, "CropBox") ?? mediaBox;
279
+ if (cropBox.urx - cropBox.llx <= 0 || cropBox.ury - cropBox.lly <= 0) {
280
+ sink({
281
+ code: "pdf/invalid-crop-box",
282
+ severity: "warning",
283
+ pageIndex,
284
+ message: "page /CropBox is degenerate (zero width or height); falling back to the /MediaBox as the visible region"
285
+ });
286
+ cropBox = mediaBox;
287
+ }
163
288
  const rotationResult = pageRotationTransform(normalizeRotation(require_objects.asNumber(require_objects.dictGet(page, "Rotate"))), mediaBox.urx - mediaBox.llx, mediaBox.ury - mediaBox.lly);
164
- const pageMatrix = require_matrix.multiplyMatrices(require_matrix.translationMatrix(-mediaBox.llx, -mediaBox.lly), rotationResult.matrix);
289
+ const visibleRect = rotatedRectBounds(cropBox, rotationResult.matrix);
290
+ const widthPt = visibleRect.maxX - visibleRect.minX;
291
+ const heightPt = visibleRect.maxY - visibleRect.minY;
292
+ const pageMatrix = require_matrix.multiplyMatrices(rotationResult.matrix, require_matrix.translationMatrix(-visibleRect.minX, -visibleRect.minY));
293
+ const boxRow = pageBoxResidueEntry(pageIndex, page, mediaBox, cropBox);
294
+ if (boxRow !== void 0) pageBoxRows.push(boxRow);
165
295
  const items = [];
166
296
  if (resources !== void 0) {
167
297
  const contentBytes = readPageContentBytes(page, resolver, sink);
@@ -173,7 +303,7 @@ function readPage(page, resolver, fontResolver, images, imageIdCache, destinatio
173
303
  });
174
304
  for (const item of extracted) {
175
305
  const converted = convertExtractedItem(item, pageMatrix, fontResolver, images, imageIdCache, resolver, sink);
176
- if (converted !== void 0) items.push(converted);
306
+ if (converted !== void 0 && itemIntersectsVisibleRegion(converted, widthPt, heightPt)) items.push(converted);
177
307
  }
178
308
  } else sink({
179
309
  code: "pdf/object-missing-value",
@@ -184,8 +314,8 @@ function readPage(page, resolver, fontResolver, images, imageIdCache, destinatio
184
314
  const notes = readPageNotes(page, resolver);
185
315
  const annotations = require_annotations.readPageAnnotations(page, pageMatrix, resolver, sink);
186
316
  return {
187
- widthPt: rotationResult.widthPt,
188
- heightPt: rotationResult.heightPt,
317
+ widthPt,
318
+ heightPt,
189
319
  items,
190
320
  ...notes !== void 0 ? { notes } : {},
191
321
  ...annotations.length > 0 ? { annotations } : {}
package/dist/read.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import "./notes-annotation-author.js";
2
- import { asArray, asName, asNumber, dictGet, pdfArray, pdfNull } from "./objects.js";
2
+ import { asArray, asName, asNumber, dictGet, pdfArray, pdfDict, pdfNull, pdfNum } from "./objects.js";
3
3
  import { decodePdfString, parsePdfDate } from "./pdf-text.js";
4
4
  import { concatBytes } from "./bytes/writer.js";
5
5
  import { serializeObjectToText } from "./serialize.js";
6
- import { applyMatrix, matrixRotationDegrees, matrixScaleX, matrixScaleY, multiplyMatrices, translationMatrix } from "./matrix.js";
6
+ import { applyMatrix, matrixRotationDegrees, matrixScaleX, matrixScaleY, multiplyMatrices, rotatePointAboutCenter, translationMatrix } from "./matrix.js";
7
7
  import { readPageAnnotations } from "./annotations.js";
8
8
  import { decodeStream } from "./filters.js";
9
9
  import { bytesToBase64 } from "./util/base64.js";
@@ -52,10 +52,15 @@ function readPdf(bytes, options) {
52
52
  const optionalContent = readOptionalContent(doc.catalog, doc, sink);
53
53
  const form = readAcroForm(doc.catalog, doc, (obj) => doc.pageIndex(obj), sink);
54
54
  const source = readDocumentResidue(doc, sink);
55
- const pages = pageDicts.map((pageDict) => {
55
+ const pageBoxRows = [];
56
+ const pages = pageDicts.map((pageDict, index) => {
56
57
  throwIfAborted(signal);
57
- return readPage(pageDict, doc, fontResolver, images, imageIdCache, destinationRegistry, optionalContent.layerNameOf, sink);
58
+ return readPage(index, pageDict, doc, fontResolver, images, imageIdCache, destinationRegistry, optionalContent.layerNameOf, pageBoxRows, sink);
58
59
  });
60
+ if (pageBoxRows.length > 0) source["page-boxes"] = {
61
+ format: "pdf",
62
+ xml: serializeObjectToText(pdfArray(pageBoxRows))
63
+ };
59
64
  return {
60
65
  formatVersion: 1,
61
66
  metadata: readMetadata(doc.trailer, doc, doc.catalog, sink),
@@ -69,18 +74,13 @@ function readPdf(bytes, options) {
69
74
  ...Object.keys(source).length > 0 ? { source } : {}
70
75
  };
71
76
  }
72
- function readMediaBox(page) {
73
- const arr = asArray(dictGet(page, "MediaBox"));
74
- if (arr === void 0) return {
75
- llx: 0,
76
- lly: 0,
77
- urx: DEFAULT_PAGE_WIDTH_PT,
78
- ury: DEFAULT_PAGE_HEIGHT_PT
79
- };
77
+ function readDeclaredPageBox(page, key) {
78
+ const arr = asArray(dictGet(page, key));
79
+ if (arr === void 0) return;
80
80
  const a = asNumber(arr[0]) ?? 0;
81
81
  const b = asNumber(arr[1]) ?? 0;
82
- const c = asNumber(arr[2]) ?? DEFAULT_PAGE_WIDTH_PT;
83
- const d = asNumber(arr[3]) ?? DEFAULT_PAGE_HEIGHT_PT;
82
+ const c = asNumber(arr[2]) ?? 0;
83
+ const d = asNumber(arr[3]) ?? 0;
84
84
  return {
85
85
  llx: Math.min(a, c),
86
86
  lly: Math.min(b, d),
@@ -88,6 +88,33 @@ function readMediaBox(page) {
88
88
  ury: Math.max(b, d)
89
89
  };
90
90
  }
91
+ function readMediaBox(page) {
92
+ return readDeclaredPageBox(page, "MediaBox") ?? {
93
+ llx: 0,
94
+ lly: 0,
95
+ urx: DEFAULT_PAGE_WIDTH_PT,
96
+ ury: DEFAULT_PAGE_HEIGHT_PT
97
+ };
98
+ }
99
+ function rotatedRectBounds(rect, matrix) {
100
+ const transformed = [
101
+ [rect.llx, rect.lly],
102
+ [rect.urx, rect.lly],
103
+ [rect.llx, rect.ury],
104
+ [rect.urx, rect.ury]
105
+ ].map(([x, y]) => applyMatrix(matrix, {
106
+ x,
107
+ y
108
+ }));
109
+ const xs = transformed.map((point) => point.x);
110
+ const ys = transformed.map((point) => point.y);
111
+ return {
112
+ minX: Math.min(...xs),
113
+ minY: Math.min(...ys),
114
+ maxX: Math.max(...xs),
115
+ maxY: Math.max(...ys)
116
+ };
117
+ }
91
118
  function normalizeRotation(rotate) {
92
119
  if (rotate === void 0) return 0;
93
120
  const normalized = (Math.round(rotate / 90) * 90 % 360 + 360) % 360;
@@ -156,11 +183,114 @@ function readPageContentBytes(page, resolver, sink) {
156
183
  }
157
184
  return /* @__PURE__ */ new Uint8Array(0);
158
185
  }
159
- function readPage(page, resolver, fontResolver, images, imageIdCache, destinationRegistry, layerNameOf, sink) {
186
+ function contentItemBounds(item) {
187
+ if (item.kind === "link" || item.kind === "internalLink") return;
188
+ const frameBounds = (xPt, yPt, widthPt, heightPt) => ({
189
+ minX: xPt,
190
+ minY: yPt,
191
+ maxX: xPt + widthPt,
192
+ maxY: yPt + heightPt
193
+ });
194
+ if (item.kind === "text" || item.kind === "image" || item.kind === "rect" || item.kind === "ellipse") {
195
+ if (item.kind !== "text" || item.rotationDeg === void 0 || item.rotationDeg % 180 === 0) return frameBounds(item.xPt, item.yPt, item.widthPt ?? 0, item.kind === "text" ? item.sizePt : item.heightPt);
196
+ const rotationDeg = item.rotationDeg;
197
+ const anchorX = item.xPt;
198
+ const anchorY = item.yPt;
199
+ const rotated = [
200
+ [0, 0],
201
+ [item.widthPt ?? 0, 0],
202
+ [0, item.sizePt],
203
+ [item.widthPt ?? 0, item.sizePt]
204
+ ].map(([dx, dy]) => rotatePointAboutCenter({
205
+ x: anchorX + dx,
206
+ y: anchorY + dy
207
+ }, {
208
+ x: anchorX,
209
+ y: anchorY
210
+ }, rotationDeg));
211
+ const xs = rotated.map((point) => point.x);
212
+ const ys = rotated.map((point) => point.y);
213
+ return {
214
+ minX: Math.min(...xs),
215
+ minY: Math.min(...ys),
216
+ maxX: Math.max(...xs),
217
+ maxY: Math.max(...ys)
218
+ };
219
+ }
220
+ if (item.kind === "line") return {
221
+ minX: Math.min(item.x1Pt, item.x2Pt),
222
+ minY: Math.min(item.y1Pt, item.y2Pt),
223
+ maxX: Math.max(item.x1Pt, item.x2Pt),
224
+ maxY: Math.max(item.y1Pt, item.y2Pt)
225
+ };
226
+ let minX = Number.POSITIVE_INFINITY;
227
+ let minY = Number.POSITIVE_INFINITY;
228
+ let maxX = Number.NEGATIVE_INFINITY;
229
+ let maxY = Number.NEGATIVE_INFINITY;
230
+ const include = (x, y) => {
231
+ minX = Math.min(minX, x);
232
+ minY = Math.min(minY, y);
233
+ maxX = Math.max(maxX, x);
234
+ maxY = Math.max(maxY, y);
235
+ };
236
+ for (const subpath of item.subpaths) {
237
+ include(subpath.startXPt, subpath.startYPt);
238
+ for (const segment of subpath.segments) {
239
+ include(segment.xPt, segment.yPt);
240
+ if (segment.kind === "cubic") {
241
+ include(segment.c1xPt, segment.c1yPt);
242
+ include(segment.c2xPt, segment.c2yPt);
243
+ }
244
+ }
245
+ }
246
+ return {
247
+ minX,
248
+ minY,
249
+ maxX,
250
+ maxY
251
+ };
252
+ }
253
+ function itemIntersectsVisibleRegion(item, widthPt, heightPt) {
254
+ if (item.kind === "link" || item.kind === "internalLink") return true;
255
+ const bounds = contentItemBounds(item);
256
+ if (bounds === void 0) return true;
257
+ return bounds.maxX >= 0 && bounds.minX <= widthPt && bounds.maxY >= 0 && bounds.minY <= heightPt;
258
+ }
259
+ function pageBoxResidueEntry(pageIndex, page, mediaBox, cropBox) {
260
+ const declaredBoxes = [
261
+ ["MediaBox", dictGet(page, "MediaBox")],
262
+ ["CropBox", dictGet(page, "CropBox")],
263
+ ["BleedBox", dictGet(page, "BleedBox")],
264
+ ["TrimBox", dictGet(page, "TrimBox")],
265
+ ["ArtBox", dictGet(page, "ArtBox")]
266
+ ];
267
+ const cropDeclared = dictGet(page, "CropBox") !== void 0;
268
+ const productionBoxDeclared = declaredBoxes.slice(2).some(([, value]) => value !== void 0);
269
+ if (!(cropDeclared && (cropBox.llx !== mediaBox.llx || cropBox.lly !== mediaBox.lly || cropBox.urx !== mediaBox.urx || cropBox.ury !== mediaBox.ury)) && !productionBoxDeclared) return;
270
+ const entries = { Page: pdfNum(pageIndex) };
271
+ for (const [key, value] of declaredBoxes) if (value !== void 0) entries[key] = value;
272
+ return pdfDict(entries);
273
+ }
274
+ function readPage(pageIndex, page, resolver, fontResolver, images, imageIdCache, destinationRegistry, layerNameOf, pageBoxRows, sink) {
160
275
  const resources = resolver.resolveDict(dictGet(page, "Resources"));
161
276
  const mediaBox = readMediaBox(page);
277
+ let cropBox = readDeclaredPageBox(page, "CropBox") ?? mediaBox;
278
+ if (cropBox.urx - cropBox.llx <= 0 || cropBox.ury - cropBox.lly <= 0) {
279
+ sink({
280
+ code: "pdf/invalid-crop-box",
281
+ severity: "warning",
282
+ pageIndex,
283
+ message: "page /CropBox is degenerate (zero width or height); falling back to the /MediaBox as the visible region"
284
+ });
285
+ cropBox = mediaBox;
286
+ }
162
287
  const rotationResult = pageRotationTransform(normalizeRotation(asNumber(dictGet(page, "Rotate"))), mediaBox.urx - mediaBox.llx, mediaBox.ury - mediaBox.lly);
163
- const pageMatrix = multiplyMatrices(translationMatrix(-mediaBox.llx, -mediaBox.lly), rotationResult.matrix);
288
+ const visibleRect = rotatedRectBounds(cropBox, rotationResult.matrix);
289
+ const widthPt = visibleRect.maxX - visibleRect.minX;
290
+ const heightPt = visibleRect.maxY - visibleRect.minY;
291
+ const pageMatrix = multiplyMatrices(rotationResult.matrix, translationMatrix(-visibleRect.minX, -visibleRect.minY));
292
+ const boxRow = pageBoxResidueEntry(pageIndex, page, mediaBox, cropBox);
293
+ if (boxRow !== void 0) pageBoxRows.push(boxRow);
164
294
  const items = [];
165
295
  if (resources !== void 0) {
166
296
  const contentBytes = readPageContentBytes(page, resolver, sink);
@@ -172,7 +302,7 @@ function readPage(page, resolver, fontResolver, images, imageIdCache, destinatio
172
302
  });
173
303
  for (const item of extracted) {
174
304
  const converted = convertExtractedItem(item, pageMatrix, fontResolver, images, imageIdCache, resolver, sink);
175
- if (converted !== void 0) items.push(converted);
305
+ if (converted !== void 0 && itemIntersectsVisibleRegion(converted, widthPt, heightPt)) items.push(converted);
176
306
  }
177
307
  } else sink({
178
308
  code: "pdf/object-missing-value",
@@ -183,8 +313,8 @@ function readPage(page, resolver, fontResolver, images, imageIdCache, destinatio
183
313
  const notes = readPageNotes(page, resolver);
184
314
  const annotations = readPageAnnotations(page, pageMatrix, resolver, sink);
185
315
  return {
186
- widthPt: rotationResult.widthPt,
187
- heightPt: rotationResult.heightPt,
316
+ widthPt,
317
+ heightPt,
188
318
  items,
189
319
  ...notes !== void 0 ? { notes } : {},
190
320
  ...annotations.length > 0 ? { annotations } : {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-codec",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "Hand-written, dependency-minimal PDF codec: parses arbitrary real-world PDFs and generates new ones, built on its own codec-owned LayoutDocument item model and Zod 4 codecs.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -98,7 +98,7 @@
98
98
  "packageManager": "pnpm@11.6.0",
99
99
  "dependencies": {
100
100
  "byte-codec": "^1.1.13",
101
- "document-schema.js": "^4.8.0",
101
+ "document-schema.js": "^4.9.0",
102
102
  "fflate": "^0.8.3",
103
103
  "zod": "^4.4.3"
104
104
  },