pdf-codec 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/read.cjs +147 -17
- package/dist/read.js +149 -19
- package/package.json +1 -1
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
|
|
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
|
|
74
|
-
const arr = require_objects.asArray(require_objects.dictGet(page,
|
|
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]) ??
|
|
84
|
-
const d = require_objects.asNumber(arr[3]) ??
|
|
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
|
|
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
|
|
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
|
|
188
|
-
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
|
|
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
|
|
73
|
-
const arr = asArray(dictGet(page,
|
|
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]) ??
|
|
83
|
-
const d = asNumber(arr[3]) ??
|
|
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
|
|
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
|
|
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
|
|
187
|
-
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.
|
|
3
|
+
"version": "3.3.0",
|
|
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": {
|