pdf-codec 5.5.0 → 5.6.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 +3 -1
- package/dist/codec.d.cts +1 -0
- package/dist/codec.d.ts +1 -0
- package/dist/filters.cjs +39 -14
- package/dist/filters.js +39 -14
- package/dist/images-read.cjs +107 -48
- package/dist/images-read.d.cts +30 -1
- package/dist/images-read.d.ts +30 -1
- package/dist/images-read.js +106 -49
- package/dist/layout.cjs +3 -2
- package/dist/layout.d.cts +3 -1
- package/dist/layout.d.ts +3 -1
- package/dist/layout.js +3 -2
- package/dist/read-convert.cjs +14 -3
- package/dist/read-convert.d.cts +19 -2
- package/dist/read-convert.d.ts +19 -2
- package/dist/read-convert.js +14 -3
- package/dist/read.cjs +7 -4
- package/dist/read.d.cts +2 -1
- package/dist/read.d.ts +2 -1
- package/dist/read.js +7 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ Both accept an optional `signal` (`AbortSignal`); `readPdf` additionally takes a
|
|
|
95
95
|
|
|
96
96
|
**Reading a subset of pages.** `readPdf(bytes, { pages: [0, 1, 2, -1] })` reads only the listed pages: zero-based indices into the page tree, a negative index counted from the end (`-1` is the last page). What a selected read skips: an unselected page's content stream is never fetched, decoded or interpreted, so the fonts and images only unselected pages use are never resolved, and no embedded file is decoded, so `attachments` is absent from a page-selected result (an attachment is document-level payload whose size is independent of the pages asked for); an absent `attachments` therefore means "not read" whenever `pageCount` is set, which only a page-selected read does, so check `pageCount` before concluding a document has no attachments. The case this exists for is identifying a document from its first pages, or reading a code from its last page, inside a memory-capped runtime such as a Cloudflare Worker isolate, without paying for every page's content. What it still reads whole, because page references resolve against it or because it is document-level and holds no page content stream: the page tree, the outline, named destinations, optional-content layers, the AcroForm, the structure element tree and the `/ParentTree` number tree (both of which scale with the document), metadata and package residue; every one of those but the page tree and metadata can be left out too, with `surfaces` (below). Two surfaces are built from page content and so cover selected pages only: the structure-ownership association an item's `structure` field names (with its parent-tree diagnostics) and the `page-boxes` residue row. A destination a link states as a direct array is minted into `destinations` (as `dest1`, `dest2`, ...) only when its page is interpreted, so an unselected page's direct link destinations are absent and a minted name depends on which pages were read (and on whether the outline was read, below); named destinations are unaffected. Duplicates collapse and the result's `pages` holds the selected pages in ascending document order, each carrying its source position as `index` (read it through `sourcePageIndex(page, position)`, which falls back to the position on a full read), with the page tree's length as the document's `pageCount`; every document-level page reference keeps naming source indices, so a reference to a selected page resolves through `index`, and one to an unselected page names a page the result does not hold. A full read sets neither field, because there `pages.length` and each page's position already state them. An empty selection is valid and reads no page, which makes `{ pages: [] }` the cheapest way to learn a file's page count and metadata. An entry naming no page (outside `-pageCount` to `pageCount - 1`, or not an integer, `NaN` and the infinities included; `-0` is page 0) throws `PdfPageSelectionError` (a `PdfParseError` with code `pdf/page-out-of-range` or `pdf/page-index-not-integer`, carrying `pageIndex` and `pageCount`) before any page is read. A page-selected `LayoutDocument` is a reading of part of a file, not a document to re-emit: `writePdf` refuses one (any document carrying `pageCount`) with `PdfPartialDocumentError`, since it places pages by position and the references would point at the wrong pages.
|
|
97
97
|
|
|
98
|
-
**Leaving document-level surfaces out.** `readPdf(bytes, { surfaces: { structure: false, outline: false } })` reads the document without the surfaces turned off. `ReadPdfOptions.surfaces` is one object of independent booleans (`ReadPdfSurfaces`), one per surface, each read unless set to `false`, so omitting the setting reads everything exactly as before. It is independent of `pages`, applying to a full read and a page-selected read alike, and combined with a small selection it is what keeps identifying a document cheap: on a long tagged PDF the structure element tree and its `/ParentTree` can be most of what a page-selected read still parses, and `{ pages: [0, 1, 2, -1], surfaces: { outline: false, destinations: false, form: false, layers: false, structure: false, residue: false } }` reads nothing beyond opening the document (cross-reference table, trailer, catalog and any encryption dictionary), the page tree, metadata and those four pages. A surface turned off is never parsed (its part of the catalog is not walked, so a malformed outline, form, layer list or structure tree reports no diagnostic), its `LayoutDocument` field is absent whatever the file holds, and the result names it in `skippedSurfaces` (in the order `LayoutDocumentSurfaceSchema` lists them, absent when every surface was read). An absent `outline`, `destinations`, `form`, `layers`, `structure` or `source` therefore means "not read" whenever `skippedSurfaces` names it, so check `skippedSurfaces` before concluding a document has none, as with `pageCount` and attachments.
|
|
98
|
+
**Leaving document-level surfaces out.** `readPdf(bytes, { surfaces: { structure: false, outline: false } })` reads the document without the surfaces turned off. `ReadPdfOptions.surfaces` is one object of independent booleans (`ReadPdfSurfaces`), one per surface, each read unless set to `false`, so omitting the setting reads everything exactly as before. It is independent of `pages`, applying to a full read and a page-selected read alike, and combined with a small selection it is what keeps identifying a document cheap: on a long tagged PDF the structure element tree and its `/ParentTree` can be most of what a page-selected read still parses, and `{ pages: [0, 1, 2, -1], surfaces: { outline: false, destinations: false, form: false, layers: false, structure: false, residue: false, images: false } }` reads nothing beyond opening the document (cross-reference table, trailer, catalog and any encryption dictionary), the page tree, metadata and those four pages' text and vector content. A surface turned off is never parsed (its part of the catalog is not walked, so a malformed outline, form, layer list or structure tree reports no diagnostic), its `LayoutDocument` field is absent whatever the file holds (`images`, which every `LayoutDocument` carries, is left empty instead), and the result names it in `skippedSurfaces` (in the order `LayoutDocumentSurfaceSchema` lists them, absent when every surface was read). An absent `outline`, `destinations`, `form`, `layers`, `structure` or `source`, or an empty `images`, therefore means "not read" whenever `skippedSurfaces` names it, so check `skippedSurfaces` before concluding a document has none, as with `pageCount` and attachments.
|
|
99
99
|
|
|
100
100
|
Each surface, and what page items lose without it (everything else about every page item is exactly what a read with the surface on produces, apart from the renumbering a skipped outline causes):
|
|
101
101
|
|
|
@@ -111,6 +111,8 @@ Each surface, and what page items lose without it (everything else about every p
|
|
|
111
111
|
|
|
112
112
|
- `residue`: the package-level residue rows (`source`), the per-page `page-boxes` row included. Page items do not depend on it, and `metadata` still reads the XMP packet it falls back to.
|
|
113
113
|
|
|
114
|
+
- `images`: the image registry (`images`), every image XObject and inline image a read page draws, decoded to PNG or passed through as JPEG. Without it no image stream is decoded or diagnosed, `images` is an empty record, and each page's `image` items are left out, since they would name assets that were never read. Decoding an image holds its decoded stream, its 8-bit raster (unless the stream already is one) and the PNG encoder's buffers at once, in proportion to its pixels, so a read that wants only text, such as text extraction in a memory-capped Worker isolate, leaves this out.
|
|
115
|
+
|
|
114
116
|
A `LayoutDocument` read with any surface skipped is a reading with parts of the file deliberately unread, so `writePdf` refuses one (any document carrying `skippedSurfaces`) with `PdfSkippedSurfacesError`, which names the skipped surfaces; a document that is also page-selected reports `PdfPartialDocumentError` first.
|
|
115
117
|
|
|
116
118
|
**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.
|
package/dist/codec.d.cts
CHANGED
|
@@ -321,6 +321,7 @@ declare const pdfCodec: z.ZodCodec<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Arr
|
|
|
321
321
|
}, z.core.$strict>>>;
|
|
322
322
|
skippedSurfaces: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
323
323
|
structure: "structure";
|
|
324
|
+
images: "images";
|
|
324
325
|
destinations: "destinations";
|
|
325
326
|
outline: "outline";
|
|
326
327
|
layers: "layers";
|
package/dist/codec.d.ts
CHANGED
|
@@ -321,6 +321,7 @@ declare const pdfCodec: z.ZodCodec<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Arr
|
|
|
321
321
|
}, z.core.$strict>>>;
|
|
322
322
|
skippedSurfaces: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
323
323
|
structure: "structure";
|
|
324
|
+
images: "images";
|
|
324
325
|
destinations: "destinations";
|
|
325
326
|
outline: "outline";
|
|
326
327
|
layers: "layers";
|
package/dist/filters.cjs
CHANGED
|
@@ -8,6 +8,24 @@ const require_predictors = require("./predictors.cjs");
|
|
|
8
8
|
//#region src/filters.ts
|
|
9
9
|
const BITS_PER_BYTE = 8;
|
|
10
10
|
const BYTE_MASK = 255;
|
|
11
|
+
var GrowableBytes = class {
|
|
12
|
+
#buffer;
|
|
13
|
+
#length = 0;
|
|
14
|
+
constructor(capacity) {
|
|
15
|
+
this.#buffer = new Uint8Array(Math.max(1, capacity));
|
|
16
|
+
}
|
|
17
|
+
push(byte) {
|
|
18
|
+
if (this.#length === this.#buffer.length) {
|
|
19
|
+
const grown = new Uint8Array(this.#buffer.length * 2);
|
|
20
|
+
grown.set(this.#buffer);
|
|
21
|
+
this.#buffer = grown;
|
|
22
|
+
}
|
|
23
|
+
this.#buffer[this.#length++] = byte;
|
|
24
|
+
}
|
|
25
|
+
toBytes() {
|
|
26
|
+
return this.#buffer.slice(0, this.#length);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
11
29
|
function decodeStream(raw, dict, sink, resolve) {
|
|
12
30
|
const filters = filterNames(dict);
|
|
13
31
|
const parms = decodeParmsList(dict, filters.length);
|
|
@@ -162,7 +180,7 @@ function concatTwo(a, b) {
|
|
|
162
180
|
return out;
|
|
163
181
|
}
|
|
164
182
|
function lzwDecode(data, earlyChange, sink) {
|
|
165
|
-
const out =
|
|
183
|
+
const out = new GrowableBytes(data.length);
|
|
166
184
|
let dict = initialLzwDictionary();
|
|
167
185
|
let nextCode = LZW_FIRST_NEW_CODE;
|
|
168
186
|
let codeWidth = LZW_INITIAL_CODE_WIDTH;
|
|
@@ -214,7 +232,7 @@ function lzwDecode(data, earlyChange, sink) {
|
|
|
214
232
|
}
|
|
215
233
|
prevEntry = entry;
|
|
216
234
|
}
|
|
217
|
-
return
|
|
235
|
+
return out.toBytes();
|
|
218
236
|
}
|
|
219
237
|
const ASCII85_GROUP_BYTES = 4;
|
|
220
238
|
const ASCII85_ZERO_GROUP_MARKER = 122;
|
|
@@ -238,7 +256,7 @@ function ascii85GroupBytes(digits, byteCount) {
|
|
|
238
256
|
].slice(0, byteCount);
|
|
239
257
|
}
|
|
240
258
|
function ascii85Decode(data) {
|
|
241
|
-
const out =
|
|
259
|
+
const out = new GrowableBytes(data.length);
|
|
242
260
|
let tuple = [];
|
|
243
261
|
let i = 0;
|
|
244
262
|
if (data.length >= 2 && data[0] === ASCII85_OPTIONAL_PREFIX_LT && data[1] === ASCII85_END_MARKER) i = 2;
|
|
@@ -247,22 +265,22 @@ function ascii85Decode(data) {
|
|
|
247
265
|
if (byte === ASCII85_END_MARKER) break;
|
|
248
266
|
if (require_bytes_reader.isAsciiWhitespace(byte)) continue;
|
|
249
267
|
if (byte === ASCII85_ZERO_GROUP_MARKER && tuple.length === 0) {
|
|
250
|
-
|
|
268
|
+
for (let zero = 0; zero < ASCII85_GROUP_BYTES; zero++) out.push(0);
|
|
251
269
|
continue;
|
|
252
270
|
}
|
|
253
271
|
if (byte < ASCII85_MIN_DIGIT || byte > ASCII85_MAX_DIGIT) continue;
|
|
254
272
|
tuple.push(byte - ASCII85_MIN_DIGIT);
|
|
255
273
|
if (tuple.length === ASCII85_GROUP_DIGITS) {
|
|
256
|
-
|
|
274
|
+
for (const decoded of ascii85GroupBytes(tuple, ASCII85_GROUP_BYTES)) out.push(decoded);
|
|
257
275
|
tuple = [];
|
|
258
276
|
}
|
|
259
277
|
}
|
|
260
278
|
if (tuple.length > 1) {
|
|
261
279
|
const padded = tuple.slice();
|
|
262
280
|
while (padded.length < ASCII85_GROUP_DIGITS) padded.push(ASCII85_MAX_DIGIT_VALUE);
|
|
263
|
-
|
|
281
|
+
for (const decoded of ascii85GroupBytes(padded, tuple.length - 1)) out.push(decoded);
|
|
264
282
|
}
|
|
265
|
-
return
|
|
283
|
+
return out.toBytes();
|
|
266
284
|
}
|
|
267
285
|
const ASCII_DIGIT_ZERO = 48;
|
|
268
286
|
const ASCII_DIGIT_NINE = 57;
|
|
@@ -279,21 +297,28 @@ function hexDigitValue(byte) {
|
|
|
279
297
|
if (byte >= ASCII_LOWER_A && byte <= ASCII_LOWER_F) return byte - ASCII_LOWER_A + HEX_LETTER_DIGIT_OFFSET;
|
|
280
298
|
}
|
|
281
299
|
function asciiHexDecode(data) {
|
|
282
|
-
|
|
300
|
+
let digitCount = 0;
|
|
301
|
+
for (const byte of data) {
|
|
302
|
+
if (byte === ASCII_HEX_TERMINATOR) break;
|
|
303
|
+
if (hexDigitValue(byte) !== void 0) digitCount++;
|
|
304
|
+
}
|
|
305
|
+
const out = new Uint8Array(Math.ceil(digitCount / 2));
|
|
306
|
+
let digit = 0;
|
|
283
307
|
for (const byte of data) {
|
|
284
308
|
if (byte === ASCII_HEX_TERMINATOR) break;
|
|
285
309
|
const value = hexDigitValue(byte);
|
|
286
|
-
if (value !== void 0)
|
|
310
|
+
if (value !== void 0) {
|
|
311
|
+
const index = Math.floor(digit / 2);
|
|
312
|
+
out[index] = digit % 2 === 0 ? value << HEX_NIBBLE_BITS : (out[index] ?? 0) | value;
|
|
313
|
+
digit++;
|
|
314
|
+
}
|
|
287
315
|
}
|
|
288
|
-
if (digits.length % 2 === 1) digits.push(0);
|
|
289
|
-
const out = new Uint8Array(digits.length / 2);
|
|
290
|
-
for (let i = 0; i < out.length; i++) out[i] = digits[i * 2] << HEX_NIBBLE_BITS | digits[i * 2 + 1];
|
|
291
316
|
return out;
|
|
292
317
|
}
|
|
293
318
|
const RUN_LENGTH_EOD = 128;
|
|
294
319
|
const RUN_LENGTH_REPEAT_COUNT_BASE = 257;
|
|
295
320
|
function runLengthDecode(data) {
|
|
296
|
-
const out =
|
|
321
|
+
const out = new GrowableBytes(data.length);
|
|
297
322
|
let i = 0;
|
|
298
323
|
while (i < data.length) {
|
|
299
324
|
const length = data[i];
|
|
@@ -309,7 +334,7 @@ function runLengthDecode(data) {
|
|
|
309
334
|
for (let j = 0; j < count; j++) out.push(byte);
|
|
310
335
|
}
|
|
311
336
|
}
|
|
312
|
-
return
|
|
337
|
+
return out.toBytes();
|
|
313
338
|
}
|
|
314
339
|
//#endregion
|
|
315
340
|
exports.ascii85Decode = ascii85Decode;
|
package/dist/filters.js
CHANGED
|
@@ -7,6 +7,24 @@ import { applyPredictor, readPredictorParams } from "./predictors.js";
|
|
|
7
7
|
//#region src/filters.ts
|
|
8
8
|
const BITS_PER_BYTE = 8;
|
|
9
9
|
const BYTE_MASK = 255;
|
|
10
|
+
var GrowableBytes = class {
|
|
11
|
+
#buffer;
|
|
12
|
+
#length = 0;
|
|
13
|
+
constructor(capacity) {
|
|
14
|
+
this.#buffer = new Uint8Array(Math.max(1, capacity));
|
|
15
|
+
}
|
|
16
|
+
push(byte) {
|
|
17
|
+
if (this.#length === this.#buffer.length) {
|
|
18
|
+
const grown = new Uint8Array(this.#buffer.length * 2);
|
|
19
|
+
grown.set(this.#buffer);
|
|
20
|
+
this.#buffer = grown;
|
|
21
|
+
}
|
|
22
|
+
this.#buffer[this.#length++] = byte;
|
|
23
|
+
}
|
|
24
|
+
toBytes() {
|
|
25
|
+
return this.#buffer.slice(0, this.#length);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
10
28
|
function decodeStream(raw, dict, sink, resolve) {
|
|
11
29
|
const filters = filterNames(dict);
|
|
12
30
|
const parms = decodeParmsList(dict, filters.length);
|
|
@@ -161,7 +179,7 @@ function concatTwo(a, b) {
|
|
|
161
179
|
return out;
|
|
162
180
|
}
|
|
163
181
|
function lzwDecode(data, earlyChange, sink) {
|
|
164
|
-
const out =
|
|
182
|
+
const out = new GrowableBytes(data.length);
|
|
165
183
|
let dict = initialLzwDictionary();
|
|
166
184
|
let nextCode = LZW_FIRST_NEW_CODE;
|
|
167
185
|
let codeWidth = LZW_INITIAL_CODE_WIDTH;
|
|
@@ -213,7 +231,7 @@ function lzwDecode(data, earlyChange, sink) {
|
|
|
213
231
|
}
|
|
214
232
|
prevEntry = entry;
|
|
215
233
|
}
|
|
216
|
-
return
|
|
234
|
+
return out.toBytes();
|
|
217
235
|
}
|
|
218
236
|
const ASCII85_GROUP_BYTES = 4;
|
|
219
237
|
const ASCII85_ZERO_GROUP_MARKER = 122;
|
|
@@ -237,7 +255,7 @@ function ascii85GroupBytes(digits, byteCount) {
|
|
|
237
255
|
].slice(0, byteCount);
|
|
238
256
|
}
|
|
239
257
|
function ascii85Decode(data) {
|
|
240
|
-
const out =
|
|
258
|
+
const out = new GrowableBytes(data.length);
|
|
241
259
|
let tuple = [];
|
|
242
260
|
let i = 0;
|
|
243
261
|
if (data.length >= 2 && data[0] === ASCII85_OPTIONAL_PREFIX_LT && data[1] === ASCII85_END_MARKER) i = 2;
|
|
@@ -246,22 +264,22 @@ function ascii85Decode(data) {
|
|
|
246
264
|
if (byte === ASCII85_END_MARKER) break;
|
|
247
265
|
if (isAsciiWhitespace(byte)) continue;
|
|
248
266
|
if (byte === ASCII85_ZERO_GROUP_MARKER && tuple.length === 0) {
|
|
249
|
-
|
|
267
|
+
for (let zero = 0; zero < ASCII85_GROUP_BYTES; zero++) out.push(0);
|
|
250
268
|
continue;
|
|
251
269
|
}
|
|
252
270
|
if (byte < ASCII85_MIN_DIGIT || byte > ASCII85_MAX_DIGIT) continue;
|
|
253
271
|
tuple.push(byte - ASCII85_MIN_DIGIT);
|
|
254
272
|
if (tuple.length === ASCII85_GROUP_DIGITS) {
|
|
255
|
-
|
|
273
|
+
for (const decoded of ascii85GroupBytes(tuple, ASCII85_GROUP_BYTES)) out.push(decoded);
|
|
256
274
|
tuple = [];
|
|
257
275
|
}
|
|
258
276
|
}
|
|
259
277
|
if (tuple.length > 1) {
|
|
260
278
|
const padded = tuple.slice();
|
|
261
279
|
while (padded.length < ASCII85_GROUP_DIGITS) padded.push(ASCII85_MAX_DIGIT_VALUE);
|
|
262
|
-
|
|
280
|
+
for (const decoded of ascii85GroupBytes(padded, tuple.length - 1)) out.push(decoded);
|
|
263
281
|
}
|
|
264
|
-
return
|
|
282
|
+
return out.toBytes();
|
|
265
283
|
}
|
|
266
284
|
const ASCII_DIGIT_ZERO = 48;
|
|
267
285
|
const ASCII_DIGIT_NINE = 57;
|
|
@@ -278,21 +296,28 @@ function hexDigitValue(byte) {
|
|
|
278
296
|
if (byte >= ASCII_LOWER_A && byte <= ASCII_LOWER_F) return byte - ASCII_LOWER_A + HEX_LETTER_DIGIT_OFFSET;
|
|
279
297
|
}
|
|
280
298
|
function asciiHexDecode(data) {
|
|
281
|
-
|
|
299
|
+
let digitCount = 0;
|
|
300
|
+
for (const byte of data) {
|
|
301
|
+
if (byte === ASCII_HEX_TERMINATOR) break;
|
|
302
|
+
if (hexDigitValue(byte) !== void 0) digitCount++;
|
|
303
|
+
}
|
|
304
|
+
const out = new Uint8Array(Math.ceil(digitCount / 2));
|
|
305
|
+
let digit = 0;
|
|
282
306
|
for (const byte of data) {
|
|
283
307
|
if (byte === ASCII_HEX_TERMINATOR) break;
|
|
284
308
|
const value = hexDigitValue(byte);
|
|
285
|
-
if (value !== void 0)
|
|
309
|
+
if (value !== void 0) {
|
|
310
|
+
const index = Math.floor(digit / 2);
|
|
311
|
+
out[index] = digit % 2 === 0 ? value << HEX_NIBBLE_BITS : (out[index] ?? 0) | value;
|
|
312
|
+
digit++;
|
|
313
|
+
}
|
|
286
314
|
}
|
|
287
|
-
if (digits.length % 2 === 1) digits.push(0);
|
|
288
|
-
const out = new Uint8Array(digits.length / 2);
|
|
289
|
-
for (let i = 0; i < out.length; i++) out[i] = digits[i * 2] << HEX_NIBBLE_BITS | digits[i * 2 + 1];
|
|
290
315
|
return out;
|
|
291
316
|
}
|
|
292
317
|
const RUN_LENGTH_EOD = 128;
|
|
293
318
|
const RUN_LENGTH_REPEAT_COUNT_BASE = 257;
|
|
294
319
|
function runLengthDecode(data) {
|
|
295
|
-
const out =
|
|
320
|
+
const out = new GrowableBytes(data.length);
|
|
296
321
|
let i = 0;
|
|
297
322
|
while (i < data.length) {
|
|
298
323
|
const length = data[i];
|
|
@@ -308,7 +333,7 @@ function runLengthDecode(data) {
|
|
|
308
333
|
for (let j = 0; j < count; j++) out.push(byte);
|
|
309
334
|
}
|
|
310
335
|
}
|
|
311
|
-
return
|
|
336
|
+
return out.toBytes();
|
|
312
337
|
}
|
|
313
338
|
//#endregion
|
|
314
339
|
export { ascii85Decode, asciiHexDecode, decodeStream, lzwDecode, runLengthDecode };
|
package/dist/images-read.cjs
CHANGED
|
@@ -58,27 +58,51 @@ function resolveColorSpace(csObj, resolver, sink) {
|
|
|
58
58
|
name: family ?? "(unrecognised array colour space)"
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Allocates the buffers one `decodeImageSamples` call needs and counts their bytes, so a test can bound the decode's memory in bytes rather than time.
|
|
63
|
+
*/
|
|
64
|
+
var SampleDecodeWork = class {
|
|
65
|
+
#bytesAllocated = 0;
|
|
66
|
+
/** Bytes of every buffer the decode allocated: its raster, its one reusable row and its lookup table. Each lives until the decode returns, so this is also the decode's peak; a view onto the stream it was given costs nothing. */
|
|
67
|
+
get bytesAllocated() {
|
|
68
|
+
return this.#bytesAllocated;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A zero-filled buffer of `length` bytes, counted.
|
|
72
|
+
* @param length - Its length in bytes.
|
|
73
|
+
* @returns The buffer.
|
|
74
|
+
*/
|
|
75
|
+
allocate(length) {
|
|
76
|
+
this.#bytesAllocated += length;
|
|
77
|
+
return new Uint8Array(length);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
function readSampleRow(data, row, bitsPerComponent, target) {
|
|
81
|
+
const samplesPerRow = target.length;
|
|
82
|
+
if (bitsPerComponent === BITS_PER_BYTE) {
|
|
83
|
+
const rowStart = row * samplesPerRow;
|
|
84
|
+
for (let s = 0; s < samplesPerRow; s++) target[s] = data[rowStart + s] ?? 0;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const rowStart = row * Math.ceil(samplesPerRow * bitsPerComponent / BITS_PER_BYTE);
|
|
88
|
+
const sampleMask = (1 << bitsPerComponent) - 1;
|
|
89
|
+
for (let s = 0; s < samplesPerRow; s++) {
|
|
90
|
+
const bitPos = s * bitsPerComponent;
|
|
91
|
+
const byte = data[rowStart + Math.floor(bitPos / BITS_PER_BYTE)] ?? 0;
|
|
92
|
+
const shift = BITS_PER_BYTE - bitsPerComponent - bitPos % BITS_PER_BYTE;
|
|
93
|
+
target[s] = byte >> shift & sampleMask;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function forEachSampleRow(data, width, height, componentsPerPixel, bitsPerComponent, work, visit) {
|
|
63
97
|
const samplesPerRow = width * componentsPerPixel;
|
|
64
|
-
const
|
|
65
|
-
const out = [];
|
|
98
|
+
const rowSamples = work.allocate(samplesPerRow);
|
|
66
99
|
for (let row = 0; row < height; row++) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for (let s = 0; s < samplesPerRow; s++) {
|
|
70
|
-
let value = 0;
|
|
71
|
-
for (let b = 0; b < bitsPerComponent; b++) {
|
|
72
|
-
const byteIndex = rowStart + Math.floor(bitPos / BITS_PER_BYTE);
|
|
73
|
-
const bitIndex = 7 - bitPos % BITS_PER_BYTE;
|
|
74
|
-
const bit = (data[byteIndex] ?? 0) >> bitIndex & 1;
|
|
75
|
-
value = value << 1 | bit;
|
|
76
|
-
bitPos++;
|
|
77
|
-
}
|
|
78
|
-
out.push(value);
|
|
79
|
-
}
|
|
100
|
+
readSampleRow(data, row, bitsPerComponent, rowSamples);
|
|
101
|
+
visit(rowSamples, row * samplesPerRow);
|
|
80
102
|
}
|
|
81
|
-
|
|
103
|
+
}
|
|
104
|
+
function availableSampleCount(data, sampleCount, bitsPerComponent) {
|
|
105
|
+
return bitsPerComponent === BITS_PER_BYTE ? Math.min(data.length, sampleCount) : sampleCount;
|
|
82
106
|
}
|
|
83
107
|
function scaleToByte(value, maxValue, inverted) {
|
|
84
108
|
const v = inverted ? maxValue - value : value;
|
|
@@ -107,56 +131,89 @@ function sampleFromPalette(base, lookup, offset) {
|
|
|
107
131
|
b: lookup[offset + 2] ?? 0
|
|
108
132
|
};
|
|
109
133
|
}
|
|
110
|
-
function
|
|
134
|
+
function scaleTable(maxValue, inverted, work) {
|
|
135
|
+
const table = work.allocate(maxValue + 1);
|
|
136
|
+
for (let value = 0; value <= maxValue; value++) table[value] = scaleToByte(value, maxValue, inverted);
|
|
137
|
+
return table;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Decodes an image's samples into the 8-bit raster `encodePng` takes, holding at most the raster itself, one row of samples and a lookup table of at most 256 entries beside the stream; nothing is allocated per sample.
|
|
141
|
+
* @param data - The image's decoded stream: rows of `width` pixels padded to a whole byte, sub-byte samples packed most significant bit first.
|
|
142
|
+
* @param width - Pixels per row.
|
|
143
|
+
* @param height - Rows.
|
|
144
|
+
* @param bitsPerComponent - 1, 2, 4 or 8.
|
|
145
|
+
* @param colorSpace - The resolved colour space; never `unsupported`.
|
|
146
|
+
* @param inverted - Whether `/Decode` is `[1 0]`, which a gray, RGB or CMYK sample is inverted by and an indexed one ignores.
|
|
147
|
+
* @param work - Counts the bytes allocated; see SampleDecodeWork.
|
|
148
|
+
* @returns A gray or RGB raster. An 8-bit gray or RGB image with no `/Decode` inversion is already that raster, so its data is a view onto `data` and costs no allocation. An 8-bit stream shorter than the image yields a gray, RGB or indexed raster holding only the pixels the stream covers, and a CMYK one in which each missing sample reads as 0.
|
|
149
|
+
*/
|
|
150
|
+
function decodeImageSamples(data, width, height, bitsPerComponent, colorSpace, inverted, work = new SampleDecodeWork()) {
|
|
111
151
|
const maxValue = (1 << bitsPerComponent) - 1;
|
|
152
|
+
const pixelCount = width * height;
|
|
112
153
|
if (colorSpace.kind === "indexed") {
|
|
113
|
-
const
|
|
154
|
+
const channels = colorSpace.base.kind === "gray" ? 1 : RGB_CHANNEL_COUNT;
|
|
114
155
|
const baseComponents = componentsOf(colorSpace.base);
|
|
115
|
-
const
|
|
116
|
-
let
|
|
117
|
-
for (const index of indices) {
|
|
156
|
+
const palette = work.allocate((maxValue + 1) * channels);
|
|
157
|
+
for (let index = 0; index <= maxValue; index++) {
|
|
118
158
|
const rgb = sampleFromPalette(colorSpace.base, colorSpace.lookup, index * baseComponents);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
out[outIdx++] = rgb.b;
|
|
159
|
+
palette[index * channels] = rgb.r;
|
|
160
|
+
if (channels === RGB_CHANNEL_COUNT) {
|
|
161
|
+
palette[index * channels + 1] = rgb.g;
|
|
162
|
+
palette[index * channels + 2] = rgb.b;
|
|
124
163
|
}
|
|
125
164
|
}
|
|
165
|
+
const out = work.allocate(pixelCount * channels);
|
|
166
|
+
const indexed = availableSampleCount(data, pixelCount, bitsPerComponent);
|
|
167
|
+
forEachSampleRow(data, width, height, 1, bitsPerComponent, work, (indices, firstPixel) => {
|
|
168
|
+
const count = Math.min(indices.length, indexed - firstPixel);
|
|
169
|
+
for (let i = 0; i < count; i++) {
|
|
170
|
+
const entry = indices[i] * channels;
|
|
171
|
+
const at = (firstPixel + i) * channels;
|
|
172
|
+
for (let channel = 0; channel < channels; channel++) out[at + channel] = palette[entry + channel];
|
|
173
|
+
}
|
|
174
|
+
});
|
|
126
175
|
return {
|
|
127
176
|
width,
|
|
128
177
|
height,
|
|
129
|
-
channels
|
|
178
|
+
channels,
|
|
130
179
|
data: out
|
|
131
180
|
};
|
|
132
181
|
}
|
|
133
|
-
if (colorSpace.kind === "gray") {
|
|
134
|
-
const
|
|
135
|
-
|
|
182
|
+
if (colorSpace.kind === "gray" || colorSpace.kind === "rgb") {
|
|
183
|
+
const channels = colorSpace.kind === "gray" ? 1 : RGB_CHANNEL_COUNT;
|
|
184
|
+
const available = availableSampleCount(data, pixelCount * channels, bitsPerComponent);
|
|
185
|
+
if (bitsPerComponent === BITS_PER_BYTE && !inverted) return {
|
|
136
186
|
width,
|
|
137
187
|
height,
|
|
138
|
-
channels
|
|
139
|
-
data:
|
|
188
|
+
channels,
|
|
189
|
+
data: data.subarray(0, available)
|
|
140
190
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
191
|
+
const scale = scaleTable(maxValue, inverted, work);
|
|
192
|
+
const out = work.allocate(available);
|
|
193
|
+
forEachSampleRow(data, width, height, channels, bitsPerComponent, work, (samples, firstSample) => {
|
|
194
|
+
const count = Math.min(samples.length, available - firstSample);
|
|
195
|
+
for (let i = 0; i < count; i++) out[firstSample + i] = scale[samples[i]];
|
|
196
|
+
});
|
|
145
197
|
return {
|
|
146
198
|
width,
|
|
147
199
|
height,
|
|
148
|
-
channels
|
|
200
|
+
channels,
|
|
149
201
|
data: out
|
|
150
202
|
};
|
|
151
203
|
}
|
|
152
|
-
const
|
|
153
|
-
const out =
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
204
|
+
const scale = scaleTable(maxValue, inverted, work);
|
|
205
|
+
const out = work.allocate(pixelCount * RGB_CHANNEL_COUNT);
|
|
206
|
+
forEachSampleRow(data, width, height, CMYK_COMPONENT_COUNT, bitsPerComponent, work, (samples, firstSample) => {
|
|
207
|
+
const firstPixel = firstSample / CMYK_COMPONENT_COUNT;
|
|
208
|
+
for (let px = 0; px < width; px++) {
|
|
209
|
+
const base = px * CMYK_COMPONENT_COUNT;
|
|
210
|
+
const rgb = cmykToRgbByte(scale[samples[base]] / MAX_BYTE_VALUE, scale[samples[base + 1]] / MAX_BYTE_VALUE, scale[samples[base + 2]] / MAX_BYTE_VALUE, scale[samples[base + 3]] / MAX_BYTE_VALUE);
|
|
211
|
+
const outBase = (firstPixel + px) * RGB_CHANNEL_COUNT;
|
|
212
|
+
out[outBase] = rgb.r;
|
|
213
|
+
out[outBase + 1] = rgb.g;
|
|
214
|
+
out[outBase + 2] = rgb.b;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
160
217
|
return {
|
|
161
218
|
width,
|
|
162
219
|
height,
|
|
@@ -327,7 +384,7 @@ function readImageXObject(dict, raw, resolver, sink) {
|
|
|
327
384
|
}
|
|
328
385
|
const decodeArr = require_objects.asArray(require_objects.dictGet(dict, "Decode"));
|
|
329
386
|
const inverted = decodeArr !== void 0 && require_objects.asNumber(decodeArr[0]) === 1 && require_objects.asNumber(decodeArr[1]) === 0;
|
|
330
|
-
const rawImage =
|
|
387
|
+
const rawImage = decodeImageSamples(decoded.bytes, width, height, bitsPerComponent, colorSpace, inverted);
|
|
331
388
|
const alpha = readSoftMaskAlpha(dict, width, height, resolver, sink);
|
|
332
389
|
const withAlpha = alpha !== void 0 ? {
|
|
333
390
|
...rawImage,
|
|
@@ -342,7 +399,9 @@ function readImageXObject(dict, raw, resolver, sink) {
|
|
|
342
399
|
};
|
|
343
400
|
}
|
|
344
401
|
//#endregion
|
|
402
|
+
exports.SampleDecodeWork = SampleDecodeWork;
|
|
345
403
|
exports.cmykToRgbByte = cmykToRgbByte;
|
|
404
|
+
exports.decodeImageSamples = decodeImageSamples;
|
|
346
405
|
exports.readImageXObject = readImageXObject;
|
|
347
406
|
exports.sampleFromPalette = sampleFromPalette;
|
|
348
407
|
exports.scaleToByte = scaleToByte;
|
package/dist/images-read.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { i as PdfDiagnosticSink } from "./diagnostics-BkG_Ej2x.cjs";
|
|
2
2
|
import { n as PdfDict } from "./objects-e2RS3-yT.cjs";
|
|
3
3
|
import { PdfObjectResolver } from "./interpret-types.cjs";
|
|
4
|
+
import { RawImage } from "byte-codec";
|
|
4
5
|
//#region src/images-read.d.ts
|
|
5
6
|
interface ExtractedPdfImage {
|
|
6
7
|
readonly format: "png" | "jpeg";
|
|
@@ -27,6 +28,20 @@ type ResolvedColorSpace = {
|
|
|
27
28
|
readonly kind: "unsupported";
|
|
28
29
|
readonly name: string;
|
|
29
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Allocates the buffers one `decodeImageSamples` call needs and counts their bytes, so a test can bound the decode's memory in bytes rather than time.
|
|
33
|
+
*/
|
|
34
|
+
declare class SampleDecodeWork {
|
|
35
|
+
#private;
|
|
36
|
+
/** Bytes of every buffer the decode allocated: its raster, its one reusable row and its lookup table. Each lives until the decode returns, so this is also the decode's peak; a view onto the stream it was given costs nothing. */
|
|
37
|
+
get bytesAllocated(): number;
|
|
38
|
+
/**
|
|
39
|
+
* A zero-filled buffer of `length` bytes, counted.
|
|
40
|
+
* @param length - Its length in bytes.
|
|
41
|
+
* @returns The buffer.
|
|
42
|
+
*/
|
|
43
|
+
allocate(length: number): Uint8Array<ArrayBuffer>;
|
|
44
|
+
}
|
|
30
45
|
declare function scaleToByte(value: number, maxValue: number, inverted: boolean): number;
|
|
31
46
|
declare function cmykToRgbByte(c: number, m: number, y: number, k: number): {
|
|
32
47
|
r: number;
|
|
@@ -38,6 +53,20 @@ declare function sampleFromPalette(base: ResolvedColorSpace, lookup: Uint8Array<
|
|
|
38
53
|
g: number;
|
|
39
54
|
b: number;
|
|
40
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Decodes an image's samples into the 8-bit raster `encodePng` takes, holding at most the raster itself, one row of samples and a lookup table of at most 256 entries beside the stream; nothing is allocated per sample.
|
|
58
|
+
* @param data - The image's decoded stream: rows of `width` pixels padded to a whole byte, sub-byte samples packed most significant bit first.
|
|
59
|
+
* @param width - Pixels per row.
|
|
60
|
+
* @param height - Rows.
|
|
61
|
+
* @param bitsPerComponent - 1, 2, 4 or 8.
|
|
62
|
+
* @param colorSpace - The resolved colour space; never `unsupported`.
|
|
63
|
+
* @param inverted - Whether `/Decode` is `[1 0]`, which a gray, RGB or CMYK sample is inverted by and an indexed one ignores.
|
|
64
|
+
* @param work - Counts the bytes allocated; see SampleDecodeWork.
|
|
65
|
+
* @returns A gray or RGB raster. An 8-bit gray or RGB image with no `/Decode` inversion is already that raster, so its data is a view onto `data` and costs no allocation. An 8-bit stream shorter than the image yields a gray, RGB or indexed raster holding only the pixels the stream covers, and a CMYK one in which each missing sample reads as 0.
|
|
66
|
+
*/
|
|
67
|
+
declare function decodeImageSamples(data: Uint8Array<ArrayBuffer>, width: number, height: number, bitsPerComponent: number, colorSpace: Exclude<ResolvedColorSpace, {
|
|
68
|
+
readonly kind: "unsupported";
|
|
69
|
+
}>, inverted: boolean, work?: SampleDecodeWork): RawImage;
|
|
41
70
|
declare function readImageXObject(dict: PdfDict, raw: Uint8Array<ArrayBuffer>, resolver: Readonly<PdfObjectResolver>, sink: PdfDiagnosticSink): ExtractedPdfImage | undefined;
|
|
42
71
|
//#endregion
|
|
43
|
-
export { ExtractedPdfImage, cmykToRgbByte, readImageXObject, sampleFromPalette, scaleToByte };
|
|
72
|
+
export { ExtractedPdfImage, ResolvedColorSpace, SampleDecodeWork, cmykToRgbByte, decodeImageSamples, readImageXObject, sampleFromPalette, scaleToByte };
|
package/dist/images-read.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { i as PdfDiagnosticSink } from "./diagnostics-BkG_Ej2x.js";
|
|
2
2
|
import { n as PdfDict } from "./objects-e2RS3-yT.js";
|
|
3
3
|
import { PdfObjectResolver } from "./interpret-types.js";
|
|
4
|
+
import { RawImage } from "byte-codec";
|
|
4
5
|
//#region src/images-read.d.ts
|
|
5
6
|
interface ExtractedPdfImage {
|
|
6
7
|
readonly format: "png" | "jpeg";
|
|
@@ -27,6 +28,20 @@ type ResolvedColorSpace = {
|
|
|
27
28
|
readonly kind: "unsupported";
|
|
28
29
|
readonly name: string;
|
|
29
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Allocates the buffers one `decodeImageSamples` call needs and counts their bytes, so a test can bound the decode's memory in bytes rather than time.
|
|
33
|
+
*/
|
|
34
|
+
declare class SampleDecodeWork {
|
|
35
|
+
#private;
|
|
36
|
+
/** Bytes of every buffer the decode allocated: its raster, its one reusable row and its lookup table. Each lives until the decode returns, so this is also the decode's peak; a view onto the stream it was given costs nothing. */
|
|
37
|
+
get bytesAllocated(): number;
|
|
38
|
+
/**
|
|
39
|
+
* A zero-filled buffer of `length` bytes, counted.
|
|
40
|
+
* @param length - Its length in bytes.
|
|
41
|
+
* @returns The buffer.
|
|
42
|
+
*/
|
|
43
|
+
allocate(length: number): Uint8Array<ArrayBuffer>;
|
|
44
|
+
}
|
|
30
45
|
declare function scaleToByte(value: number, maxValue: number, inverted: boolean): number;
|
|
31
46
|
declare function cmykToRgbByte(c: number, m: number, y: number, k: number): {
|
|
32
47
|
r: number;
|
|
@@ -38,6 +53,20 @@ declare function sampleFromPalette(base: ResolvedColorSpace, lookup: Uint8Array<
|
|
|
38
53
|
g: number;
|
|
39
54
|
b: number;
|
|
40
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Decodes an image's samples into the 8-bit raster `encodePng` takes, holding at most the raster itself, one row of samples and a lookup table of at most 256 entries beside the stream; nothing is allocated per sample.
|
|
58
|
+
* @param data - The image's decoded stream: rows of `width` pixels padded to a whole byte, sub-byte samples packed most significant bit first.
|
|
59
|
+
* @param width - Pixels per row.
|
|
60
|
+
* @param height - Rows.
|
|
61
|
+
* @param bitsPerComponent - 1, 2, 4 or 8.
|
|
62
|
+
* @param colorSpace - The resolved colour space; never `unsupported`.
|
|
63
|
+
* @param inverted - Whether `/Decode` is `[1 0]`, which a gray, RGB or CMYK sample is inverted by and an indexed one ignores.
|
|
64
|
+
* @param work - Counts the bytes allocated; see SampleDecodeWork.
|
|
65
|
+
* @returns A gray or RGB raster. An 8-bit gray or RGB image with no `/Decode` inversion is already that raster, so its data is a view onto `data` and costs no allocation. An 8-bit stream shorter than the image yields a gray, RGB or indexed raster holding only the pixels the stream covers, and a CMYK one in which each missing sample reads as 0.
|
|
66
|
+
*/
|
|
67
|
+
declare function decodeImageSamples(data: Uint8Array<ArrayBuffer>, width: number, height: number, bitsPerComponent: number, colorSpace: Exclude<ResolvedColorSpace, {
|
|
68
|
+
readonly kind: "unsupported";
|
|
69
|
+
}>, inverted: boolean, work?: SampleDecodeWork): RawImage;
|
|
41
70
|
declare function readImageXObject(dict: PdfDict, raw: Uint8Array<ArrayBuffer>, resolver: Readonly<PdfObjectResolver>, sink: PdfDiagnosticSink): ExtractedPdfImage | undefined;
|
|
42
71
|
//#endregion
|
|
43
|
-
export { ExtractedPdfImage, cmykToRgbByte, readImageXObject, sampleFromPalette, scaleToByte };
|
|
72
|
+
export { ExtractedPdfImage, ResolvedColorSpace, SampleDecodeWork, cmykToRgbByte, decodeImageSamples, readImageXObject, sampleFromPalette, scaleToByte };
|
package/dist/images-read.js
CHANGED
|
@@ -57,27 +57,51 @@ function resolveColorSpace(csObj, resolver, sink) {
|
|
|
57
57
|
name: family ?? "(unrecognised array colour space)"
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Allocates the buffers one `decodeImageSamples` call needs and counts their bytes, so a test can bound the decode's memory in bytes rather than time.
|
|
62
|
+
*/
|
|
63
|
+
var SampleDecodeWork = class {
|
|
64
|
+
#bytesAllocated = 0;
|
|
65
|
+
/** Bytes of every buffer the decode allocated: its raster, its one reusable row and its lookup table. Each lives until the decode returns, so this is also the decode's peak; a view onto the stream it was given costs nothing. */
|
|
66
|
+
get bytesAllocated() {
|
|
67
|
+
return this.#bytesAllocated;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A zero-filled buffer of `length` bytes, counted.
|
|
71
|
+
* @param length - Its length in bytes.
|
|
72
|
+
* @returns The buffer.
|
|
73
|
+
*/
|
|
74
|
+
allocate(length) {
|
|
75
|
+
this.#bytesAllocated += length;
|
|
76
|
+
return new Uint8Array(length);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
function readSampleRow(data, row, bitsPerComponent, target) {
|
|
80
|
+
const samplesPerRow = target.length;
|
|
81
|
+
if (bitsPerComponent === BITS_PER_BYTE) {
|
|
82
|
+
const rowStart = row * samplesPerRow;
|
|
83
|
+
for (let s = 0; s < samplesPerRow; s++) target[s] = data[rowStart + s] ?? 0;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const rowStart = row * Math.ceil(samplesPerRow * bitsPerComponent / BITS_PER_BYTE);
|
|
87
|
+
const sampleMask = (1 << bitsPerComponent) - 1;
|
|
88
|
+
for (let s = 0; s < samplesPerRow; s++) {
|
|
89
|
+
const bitPos = s * bitsPerComponent;
|
|
90
|
+
const byte = data[rowStart + Math.floor(bitPos / BITS_PER_BYTE)] ?? 0;
|
|
91
|
+
const shift = BITS_PER_BYTE - bitsPerComponent - bitPos % BITS_PER_BYTE;
|
|
92
|
+
target[s] = byte >> shift & sampleMask;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function forEachSampleRow(data, width, height, componentsPerPixel, bitsPerComponent, work, visit) {
|
|
62
96
|
const samplesPerRow = width * componentsPerPixel;
|
|
63
|
-
const
|
|
64
|
-
const out = [];
|
|
97
|
+
const rowSamples = work.allocate(samplesPerRow);
|
|
65
98
|
for (let row = 0; row < height; row++) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (let s = 0; s < samplesPerRow; s++) {
|
|
69
|
-
let value = 0;
|
|
70
|
-
for (let b = 0; b < bitsPerComponent; b++) {
|
|
71
|
-
const byteIndex = rowStart + Math.floor(bitPos / BITS_PER_BYTE);
|
|
72
|
-
const bitIndex = 7 - bitPos % BITS_PER_BYTE;
|
|
73
|
-
const bit = (data[byteIndex] ?? 0) >> bitIndex & 1;
|
|
74
|
-
value = value << 1 | bit;
|
|
75
|
-
bitPos++;
|
|
76
|
-
}
|
|
77
|
-
out.push(value);
|
|
78
|
-
}
|
|
99
|
+
readSampleRow(data, row, bitsPerComponent, rowSamples);
|
|
100
|
+
visit(rowSamples, row * samplesPerRow);
|
|
79
101
|
}
|
|
80
|
-
|
|
102
|
+
}
|
|
103
|
+
function availableSampleCount(data, sampleCount, bitsPerComponent) {
|
|
104
|
+
return bitsPerComponent === BITS_PER_BYTE ? Math.min(data.length, sampleCount) : sampleCount;
|
|
81
105
|
}
|
|
82
106
|
function scaleToByte(value, maxValue, inverted) {
|
|
83
107
|
const v = inverted ? maxValue - value : value;
|
|
@@ -106,56 +130,89 @@ function sampleFromPalette(base, lookup, offset) {
|
|
|
106
130
|
b: lookup[offset + 2] ?? 0
|
|
107
131
|
};
|
|
108
132
|
}
|
|
109
|
-
function
|
|
133
|
+
function scaleTable(maxValue, inverted, work) {
|
|
134
|
+
const table = work.allocate(maxValue + 1);
|
|
135
|
+
for (let value = 0; value <= maxValue; value++) table[value] = scaleToByte(value, maxValue, inverted);
|
|
136
|
+
return table;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Decodes an image's samples into the 8-bit raster `encodePng` takes, holding at most the raster itself, one row of samples and a lookup table of at most 256 entries beside the stream; nothing is allocated per sample.
|
|
140
|
+
* @param data - The image's decoded stream: rows of `width` pixels padded to a whole byte, sub-byte samples packed most significant bit first.
|
|
141
|
+
* @param width - Pixels per row.
|
|
142
|
+
* @param height - Rows.
|
|
143
|
+
* @param bitsPerComponent - 1, 2, 4 or 8.
|
|
144
|
+
* @param colorSpace - The resolved colour space; never `unsupported`.
|
|
145
|
+
* @param inverted - Whether `/Decode` is `[1 0]`, which a gray, RGB or CMYK sample is inverted by and an indexed one ignores.
|
|
146
|
+
* @param work - Counts the bytes allocated; see SampleDecodeWork.
|
|
147
|
+
* @returns A gray or RGB raster. An 8-bit gray or RGB image with no `/Decode` inversion is already that raster, so its data is a view onto `data` and costs no allocation. An 8-bit stream shorter than the image yields a gray, RGB or indexed raster holding only the pixels the stream covers, and a CMYK one in which each missing sample reads as 0.
|
|
148
|
+
*/
|
|
149
|
+
function decodeImageSamples(data, width, height, bitsPerComponent, colorSpace, inverted, work = new SampleDecodeWork()) {
|
|
110
150
|
const maxValue = (1 << bitsPerComponent) - 1;
|
|
151
|
+
const pixelCount = width * height;
|
|
111
152
|
if (colorSpace.kind === "indexed") {
|
|
112
|
-
const
|
|
153
|
+
const channels = colorSpace.base.kind === "gray" ? 1 : RGB_CHANNEL_COUNT;
|
|
113
154
|
const baseComponents = componentsOf(colorSpace.base);
|
|
114
|
-
const
|
|
115
|
-
let
|
|
116
|
-
for (const index of indices) {
|
|
155
|
+
const palette = work.allocate((maxValue + 1) * channels);
|
|
156
|
+
for (let index = 0; index <= maxValue; index++) {
|
|
117
157
|
const rgb = sampleFromPalette(colorSpace.base, colorSpace.lookup, index * baseComponents);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
out[outIdx++] = rgb.b;
|
|
158
|
+
palette[index * channels] = rgb.r;
|
|
159
|
+
if (channels === RGB_CHANNEL_COUNT) {
|
|
160
|
+
palette[index * channels + 1] = rgb.g;
|
|
161
|
+
palette[index * channels + 2] = rgb.b;
|
|
123
162
|
}
|
|
124
163
|
}
|
|
164
|
+
const out = work.allocate(pixelCount * channels);
|
|
165
|
+
const indexed = availableSampleCount(data, pixelCount, bitsPerComponent);
|
|
166
|
+
forEachSampleRow(data, width, height, 1, bitsPerComponent, work, (indices, firstPixel) => {
|
|
167
|
+
const count = Math.min(indices.length, indexed - firstPixel);
|
|
168
|
+
for (let i = 0; i < count; i++) {
|
|
169
|
+
const entry = indices[i] * channels;
|
|
170
|
+
const at = (firstPixel + i) * channels;
|
|
171
|
+
for (let channel = 0; channel < channels; channel++) out[at + channel] = palette[entry + channel];
|
|
172
|
+
}
|
|
173
|
+
});
|
|
125
174
|
return {
|
|
126
175
|
width,
|
|
127
176
|
height,
|
|
128
|
-
channels
|
|
177
|
+
channels,
|
|
129
178
|
data: out
|
|
130
179
|
};
|
|
131
180
|
}
|
|
132
|
-
if (colorSpace.kind === "gray") {
|
|
133
|
-
const
|
|
134
|
-
|
|
181
|
+
if (colorSpace.kind === "gray" || colorSpace.kind === "rgb") {
|
|
182
|
+
const channels = colorSpace.kind === "gray" ? 1 : RGB_CHANNEL_COUNT;
|
|
183
|
+
const available = availableSampleCount(data, pixelCount * channels, bitsPerComponent);
|
|
184
|
+
if (bitsPerComponent === BITS_PER_BYTE && !inverted) return {
|
|
135
185
|
width,
|
|
136
186
|
height,
|
|
137
|
-
channels
|
|
138
|
-
data:
|
|
187
|
+
channels,
|
|
188
|
+
data: data.subarray(0, available)
|
|
139
189
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
190
|
+
const scale = scaleTable(maxValue, inverted, work);
|
|
191
|
+
const out = work.allocate(available);
|
|
192
|
+
forEachSampleRow(data, width, height, channels, bitsPerComponent, work, (samples, firstSample) => {
|
|
193
|
+
const count = Math.min(samples.length, available - firstSample);
|
|
194
|
+
for (let i = 0; i < count; i++) out[firstSample + i] = scale[samples[i]];
|
|
195
|
+
});
|
|
144
196
|
return {
|
|
145
197
|
width,
|
|
146
198
|
height,
|
|
147
|
-
channels
|
|
199
|
+
channels,
|
|
148
200
|
data: out
|
|
149
201
|
};
|
|
150
202
|
}
|
|
151
|
-
const
|
|
152
|
-
const out =
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
203
|
+
const scale = scaleTable(maxValue, inverted, work);
|
|
204
|
+
const out = work.allocate(pixelCount * RGB_CHANNEL_COUNT);
|
|
205
|
+
forEachSampleRow(data, width, height, CMYK_COMPONENT_COUNT, bitsPerComponent, work, (samples, firstSample) => {
|
|
206
|
+
const firstPixel = firstSample / CMYK_COMPONENT_COUNT;
|
|
207
|
+
for (let px = 0; px < width; px++) {
|
|
208
|
+
const base = px * CMYK_COMPONENT_COUNT;
|
|
209
|
+
const rgb = cmykToRgbByte(scale[samples[base]] / MAX_BYTE_VALUE, scale[samples[base + 1]] / MAX_BYTE_VALUE, scale[samples[base + 2]] / MAX_BYTE_VALUE, scale[samples[base + 3]] / MAX_BYTE_VALUE);
|
|
210
|
+
const outBase = (firstPixel + px) * RGB_CHANNEL_COUNT;
|
|
211
|
+
out[outBase] = rgb.r;
|
|
212
|
+
out[outBase + 1] = rgb.g;
|
|
213
|
+
out[outBase + 2] = rgb.b;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
159
216
|
return {
|
|
160
217
|
width,
|
|
161
218
|
height,
|
|
@@ -326,7 +383,7 @@ function readImageXObject(dict, raw, resolver, sink) {
|
|
|
326
383
|
}
|
|
327
384
|
const decodeArr = asArray(dictGet(dict, "Decode"));
|
|
328
385
|
const inverted = decodeArr !== void 0 && asNumber(decodeArr[0]) === 1 && asNumber(decodeArr[1]) === 0;
|
|
329
|
-
const rawImage =
|
|
386
|
+
const rawImage = decodeImageSamples(decoded.bytes, width, height, bitsPerComponent, colorSpace, inverted);
|
|
330
387
|
const alpha = readSoftMaskAlpha(dict, width, height, resolver, sink);
|
|
331
388
|
const withAlpha = alpha !== void 0 ? {
|
|
332
389
|
...rawImage,
|
|
@@ -341,4 +398,4 @@ function readImageXObject(dict, raw, resolver, sink) {
|
|
|
341
398
|
};
|
|
342
399
|
}
|
|
343
400
|
//#endregion
|
|
344
|
-
export { cmykToRgbByte, readImageXObject, sampleFromPalette, scaleToByte };
|
|
401
|
+
export { SampleDecodeWork, cmykToRgbByte, decodeImageSamples, readImageXObject, sampleFromPalette, scaleToByte };
|
package/dist/layout.cjs
CHANGED
|
@@ -270,7 +270,7 @@ const LayoutStructureElementSchema = zod.z.lazy(() => zod.z.object({
|
|
|
270
270
|
children: zod.z.array(LayoutStructureElementSchema)
|
|
271
271
|
}));
|
|
272
272
|
/**
|
|
273
|
-
* The document-level surfaces a read can leave out (`ReadPdfOptions.surfaces`), each named by the `LayoutDocument` field it fills: `outline`, `destinations`, `form`, `layers` and `
|
|
273
|
+
* The document-level surfaces a read can leave out (`ReadPdfOptions.surfaces`), each named by the `LayoutDocument` field it fills: `outline`, `destinations`, `form`, `layers`, `structure` and `images` are those fields, and `residue` is the package-level `source` channel. A `LayoutDocument` lists the ones its read skipped in `skippedSurfaces`.
|
|
274
274
|
*/
|
|
275
275
|
const LayoutDocumentSurfaceSchema = zod.z.enum([
|
|
276
276
|
"outline",
|
|
@@ -278,7 +278,8 @@ const LayoutDocumentSurfaceSchema = zod.z.enum([
|
|
|
278
278
|
"form",
|
|
279
279
|
"layers",
|
|
280
280
|
"structure",
|
|
281
|
-
"residue"
|
|
281
|
+
"residue",
|
|
282
|
+
"images"
|
|
282
283
|
]);
|
|
283
284
|
const LayoutDocumentSchema = zod.z.object({
|
|
284
285
|
formatVersion: zod.z.literal(1),
|
package/dist/layout.d.cts
CHANGED
|
@@ -785,10 +785,11 @@ interface LayoutStructureElement {
|
|
|
785
785
|
readonly children: readonly LayoutStructureElement[];
|
|
786
786
|
}
|
|
787
787
|
/**
|
|
788
|
-
* The document-level surfaces a read can leave out (`ReadPdfOptions.surfaces`), each named by the `LayoutDocument` field it fills: `outline`, `destinations`, `form`, `layers` and `
|
|
788
|
+
* The document-level surfaces a read can leave out (`ReadPdfOptions.surfaces`), each named by the `LayoutDocument` field it fills: `outline`, `destinations`, `form`, `layers`, `structure` and `images` are those fields, and `residue` is the package-level `source` channel. A `LayoutDocument` lists the ones its read skipped in `skippedSurfaces`.
|
|
789
789
|
*/
|
|
790
790
|
declare const LayoutDocumentSurfaceSchema: z.ZodEnum<{
|
|
791
791
|
structure: "structure";
|
|
792
|
+
images: "images";
|
|
792
793
|
destinations: "destinations";
|
|
793
794
|
outline: "outline";
|
|
794
795
|
layers: "layers";
|
|
@@ -1115,6 +1116,7 @@ declare const LayoutDocumentSchema: z.ZodObject<{
|
|
|
1115
1116
|
}, z.core.$strict>>>;
|
|
1116
1117
|
skippedSurfaces: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
1117
1118
|
structure: "structure";
|
|
1119
|
+
images: "images";
|
|
1118
1120
|
destinations: "destinations";
|
|
1119
1121
|
outline: "outline";
|
|
1120
1122
|
layers: "layers";
|
package/dist/layout.d.ts
CHANGED
|
@@ -785,10 +785,11 @@ interface LayoutStructureElement {
|
|
|
785
785
|
readonly children: readonly LayoutStructureElement[];
|
|
786
786
|
}
|
|
787
787
|
/**
|
|
788
|
-
* The document-level surfaces a read can leave out (`ReadPdfOptions.surfaces`), each named by the `LayoutDocument` field it fills: `outline`, `destinations`, `form`, `layers` and `
|
|
788
|
+
* The document-level surfaces a read can leave out (`ReadPdfOptions.surfaces`), each named by the `LayoutDocument` field it fills: `outline`, `destinations`, `form`, `layers`, `structure` and `images` are those fields, and `residue` is the package-level `source` channel. A `LayoutDocument` lists the ones its read skipped in `skippedSurfaces`.
|
|
789
789
|
*/
|
|
790
790
|
declare const LayoutDocumentSurfaceSchema: z.ZodEnum<{
|
|
791
791
|
structure: "structure";
|
|
792
|
+
images: "images";
|
|
792
793
|
destinations: "destinations";
|
|
793
794
|
outline: "outline";
|
|
794
795
|
layers: "layers";
|
|
@@ -1115,6 +1116,7 @@ declare const LayoutDocumentSchema: z.ZodObject<{
|
|
|
1115
1116
|
}, z.core.$strict>>>;
|
|
1116
1117
|
skippedSurfaces: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
1117
1118
|
structure: "structure";
|
|
1119
|
+
images: "images";
|
|
1118
1120
|
destinations: "destinations";
|
|
1119
1121
|
outline: "outline";
|
|
1120
1122
|
layers: "layers";
|
package/dist/layout.js
CHANGED
|
@@ -269,7 +269,7 @@ const LayoutStructureElementSchema = z.lazy(() => z.object({
|
|
|
269
269
|
children: z.array(LayoutStructureElementSchema)
|
|
270
270
|
}));
|
|
271
271
|
/**
|
|
272
|
-
* The document-level surfaces a read can leave out (`ReadPdfOptions.surfaces`), each named by the `LayoutDocument` field it fills: `outline`, `destinations`, `form`, `layers` and `
|
|
272
|
+
* The document-level surfaces a read can leave out (`ReadPdfOptions.surfaces`), each named by the `LayoutDocument` field it fills: `outline`, `destinations`, `form`, `layers`, `structure` and `images` are those fields, and `residue` is the package-level `source` channel. A `LayoutDocument` lists the ones its read skipped in `skippedSurfaces`.
|
|
273
273
|
*/
|
|
274
274
|
const LayoutDocumentSurfaceSchema = z.enum([
|
|
275
275
|
"outline",
|
|
@@ -277,7 +277,8 @@ const LayoutDocumentSurfaceSchema = z.enum([
|
|
|
277
277
|
"form",
|
|
278
278
|
"layers",
|
|
279
279
|
"structure",
|
|
280
|
-
"residue"
|
|
280
|
+
"residue",
|
|
281
|
+
"images"
|
|
281
282
|
]);
|
|
282
283
|
const LayoutDocumentSchema = z.object({
|
|
283
284
|
formatVersion: z.literal(1),
|
package/dist/read-convert.cjs
CHANGED
|
@@ -7,14 +7,25 @@ const require_bytes_crc32 = require("./bytes/crc32.cjs");
|
|
|
7
7
|
const require_images_read = require("./images-read.cjs");
|
|
8
8
|
let byte_codec = require("byte-codec");
|
|
9
9
|
//#region src/read-convert.ts
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Converts one interpreted item to a layout item in page space.
|
|
12
|
+
* @param item - The item the content-stream interpreter extracted.
|
|
13
|
+
* @param pageMatrix - Maps the interpreter's user space into the page's output space.
|
|
14
|
+
* @param fontResolver - Resolves a text item's font.
|
|
15
|
+
* @param imageRegistry - Where images are decoded into, or undefined for a read that skips images, which converts no image item and decodes no image stream.
|
|
16
|
+
* @param resolver - Resolves indirect objects an image names.
|
|
17
|
+
* @param sink - Receives diagnostics.
|
|
18
|
+
* @returns The layout item, or undefined for an item that converts to none: an image that could not be decoded, or any image when `imageRegistry` is undefined.
|
|
19
|
+
*/
|
|
20
|
+
function convertExtractedItem(item, pageMatrix, fontResolver, imageRegistry, resolver, sink) {
|
|
11
21
|
if (item.kind === "text") return convertText(item, pageMatrix, fontResolver);
|
|
12
22
|
if (item.kind === "rect") return convertRect(item, pageMatrix);
|
|
13
23
|
if (item.kind === "ellipse") return convertEllipse(item, pageMatrix);
|
|
14
24
|
if (item.kind === "line") return convertLine(item, pageMatrix);
|
|
15
25
|
if (item.kind === "path") return convertPath(item, pageMatrix);
|
|
16
|
-
if (
|
|
17
|
-
return
|
|
26
|
+
if (imageRegistry === void 0) return;
|
|
27
|
+
if (item.kind === "image") return convertImage(item, pageMatrix, imageRegistry.images, imageRegistry.imageIdCache, resolver, sink);
|
|
28
|
+
return convertInlineImage(item, pageMatrix, imageRegistry.images, resolver, sink);
|
|
18
29
|
}
|
|
19
30
|
function convertText(item, pageMatrix, fontResolver) {
|
|
20
31
|
const font = fontResolver.resolve(item.fontResourceName, item.resources);
|
package/dist/read-convert.d.cts
CHANGED
|
@@ -8,7 +8,24 @@ import { DestinationRegistry } from "./navigation.cjs";
|
|
|
8
8
|
import { ExtractedPdfImage } from "./images-read.cjs";
|
|
9
9
|
import { Color } from "document-schema.js";
|
|
10
10
|
//#region src/read-convert.d.ts
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Where a read's images go: the registry each decoded image is stored in once, and the id each image XObject dictionary already decoded to (null for one that could not be).
|
|
13
|
+
*/
|
|
14
|
+
interface ImageRegistry {
|
|
15
|
+
readonly images: Record<string, LayoutImageAsset>;
|
|
16
|
+
readonly imageIdCache: Map<PdfDict, string | null>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Converts one interpreted item to a layout item in page space.
|
|
20
|
+
* @param item - The item the content-stream interpreter extracted.
|
|
21
|
+
* @param pageMatrix - Maps the interpreter's user space into the page's output space.
|
|
22
|
+
* @param fontResolver - Resolves a text item's font.
|
|
23
|
+
* @param imageRegistry - Where images are decoded into, or undefined for a read that skips images, which converts no image item and decodes no image stream.
|
|
24
|
+
* @param resolver - Resolves indirect objects an image names.
|
|
25
|
+
* @param sink - Receives diagnostics.
|
|
26
|
+
* @returns The layout item, or undefined for an item that converts to none: an image that could not be decoded, or any image when `imageRegistry` is undefined.
|
|
27
|
+
*/
|
|
28
|
+
declare function convertExtractedItem(item: ExtractedItem, pageMatrix: Matrix, fontResolver: FontResolverService, imageRegistry: ImageRegistry | undefined, resolver: Readonly<PdfObjectResolver>, sink: PdfDiagnosticSink): LayoutItem | undefined;
|
|
12
29
|
declare function convertText(item: ExtractedTextRun, pageMatrix: Matrix, fontResolver: FontResolverService): LayoutText | undefined;
|
|
13
30
|
declare function paintFields(paint: ExtractedPaint): {
|
|
14
31
|
fill?: Color;
|
|
@@ -55,4 +72,4 @@ declare function convertImage(item: ExtractedImage, pageMatrix: Matrix, images:
|
|
|
55
72
|
declare function readLinkAnnotations(page: PdfDict, pageMatrix: Matrix, resolver: Readonly<PdfObjectResolver>, destinationRegistry: DestinationRegistry): (LayoutLink | LayoutInternalLink)[];
|
|
56
73
|
declare function readPageNotes(page: PdfDict, resolver: Readonly<PdfObjectResolver>): string | undefined;
|
|
57
74
|
//#endregion
|
|
58
|
-
export { convertEllipse, convertExtractedItem, convertImage, convertLine, convertPath, convertRect, convertText, imagePlacementFrom, paintFields, readLinkAnnotations, readPageNotes, registerExtractedImage, resolveCachedImageId, transformBox, transformSubpath };
|
|
75
|
+
export { ImageRegistry, convertEllipse, convertExtractedItem, convertImage, convertLine, convertPath, convertRect, convertText, imagePlacementFrom, paintFields, readLinkAnnotations, readPageNotes, registerExtractedImage, resolveCachedImageId, transformBox, transformSubpath };
|
package/dist/read-convert.d.ts
CHANGED
|
@@ -8,7 +8,24 @@ import { DestinationRegistry } from "./navigation.js";
|
|
|
8
8
|
import { ExtractedPdfImage } from "./images-read.js";
|
|
9
9
|
import { Color } from "document-schema.js";
|
|
10
10
|
//#region src/read-convert.d.ts
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Where a read's images go: the registry each decoded image is stored in once, and the id each image XObject dictionary already decoded to (null for one that could not be).
|
|
13
|
+
*/
|
|
14
|
+
interface ImageRegistry {
|
|
15
|
+
readonly images: Record<string, LayoutImageAsset>;
|
|
16
|
+
readonly imageIdCache: Map<PdfDict, string | null>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Converts one interpreted item to a layout item in page space.
|
|
20
|
+
* @param item - The item the content-stream interpreter extracted.
|
|
21
|
+
* @param pageMatrix - Maps the interpreter's user space into the page's output space.
|
|
22
|
+
* @param fontResolver - Resolves a text item's font.
|
|
23
|
+
* @param imageRegistry - Where images are decoded into, or undefined for a read that skips images, which converts no image item and decodes no image stream.
|
|
24
|
+
* @param resolver - Resolves indirect objects an image names.
|
|
25
|
+
* @param sink - Receives diagnostics.
|
|
26
|
+
* @returns The layout item, or undefined for an item that converts to none: an image that could not be decoded, or any image when `imageRegistry` is undefined.
|
|
27
|
+
*/
|
|
28
|
+
declare function convertExtractedItem(item: ExtractedItem, pageMatrix: Matrix, fontResolver: FontResolverService, imageRegistry: ImageRegistry | undefined, resolver: Readonly<PdfObjectResolver>, sink: PdfDiagnosticSink): LayoutItem | undefined;
|
|
12
29
|
declare function convertText(item: ExtractedTextRun, pageMatrix: Matrix, fontResolver: FontResolverService): LayoutText | undefined;
|
|
13
30
|
declare function paintFields(paint: ExtractedPaint): {
|
|
14
31
|
fill?: Color;
|
|
@@ -55,4 +72,4 @@ declare function convertImage(item: ExtractedImage, pageMatrix: Matrix, images:
|
|
|
55
72
|
declare function readLinkAnnotations(page: PdfDict, pageMatrix: Matrix, resolver: Readonly<PdfObjectResolver>, destinationRegistry: DestinationRegistry): (LayoutLink | LayoutInternalLink)[];
|
|
56
73
|
declare function readPageNotes(page: PdfDict, resolver: Readonly<PdfObjectResolver>): string | undefined;
|
|
57
74
|
//#endregion
|
|
58
|
-
export { convertEllipse, convertExtractedItem, convertImage, convertLine, convertPath, convertRect, convertText, imagePlacementFrom, paintFields, readLinkAnnotations, readPageNotes, registerExtractedImage, resolveCachedImageId, transformBox, transformSubpath };
|
|
75
|
+
export { ImageRegistry, convertEllipse, convertExtractedItem, convertImage, convertLine, convertPath, convertRect, convertText, imagePlacementFrom, paintFields, readLinkAnnotations, readPageNotes, registerExtractedImage, resolveCachedImageId, transformBox, transformSubpath };
|
package/dist/read-convert.js
CHANGED
|
@@ -6,14 +6,25 @@ import { crc32 } from "./bytes/crc32.js";
|
|
|
6
6
|
import { readImageXObject } from "./images-read.js";
|
|
7
7
|
import { bytesToBase64 } from "byte-codec";
|
|
8
8
|
//#region src/read-convert.ts
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Converts one interpreted item to a layout item in page space.
|
|
11
|
+
* @param item - The item the content-stream interpreter extracted.
|
|
12
|
+
* @param pageMatrix - Maps the interpreter's user space into the page's output space.
|
|
13
|
+
* @param fontResolver - Resolves a text item's font.
|
|
14
|
+
* @param imageRegistry - Where images are decoded into, or undefined for a read that skips images, which converts no image item and decodes no image stream.
|
|
15
|
+
* @param resolver - Resolves indirect objects an image names.
|
|
16
|
+
* @param sink - Receives diagnostics.
|
|
17
|
+
* @returns The layout item, or undefined for an item that converts to none: an image that could not be decoded, or any image when `imageRegistry` is undefined.
|
|
18
|
+
*/
|
|
19
|
+
function convertExtractedItem(item, pageMatrix, fontResolver, imageRegistry, resolver, sink) {
|
|
10
20
|
if (item.kind === "text") return convertText(item, pageMatrix, fontResolver);
|
|
11
21
|
if (item.kind === "rect") return convertRect(item, pageMatrix);
|
|
12
22
|
if (item.kind === "ellipse") return convertEllipse(item, pageMatrix);
|
|
13
23
|
if (item.kind === "line") return convertLine(item, pageMatrix);
|
|
14
24
|
if (item.kind === "path") return convertPath(item, pageMatrix);
|
|
15
|
-
if (
|
|
16
|
-
return
|
|
25
|
+
if (imageRegistry === void 0) return;
|
|
26
|
+
if (item.kind === "image") return convertImage(item, pageMatrix, imageRegistry.images, imageRegistry.imageIdCache, resolver, sink);
|
|
27
|
+
return convertInlineImage(item, pageMatrix, imageRegistry.images, resolver, sink);
|
|
17
28
|
}
|
|
18
29
|
function convertText(item, pageMatrix, fontResolver) {
|
|
19
30
|
const font = fontResolver.resolve(item.fontResourceName, item.resources);
|
package/dist/read.cjs
CHANGED
|
@@ -59,11 +59,14 @@ function readPdf(bytes, options) {
|
|
|
59
59
|
sink
|
|
60
60
|
});
|
|
61
61
|
const images = {};
|
|
62
|
-
const imageIdCache = /* @__PURE__ */ new Map();
|
|
63
62
|
const pageDicts = doc.pages();
|
|
64
63
|
const selection = options?.pages === void 0 ? void 0 : resolvePageSelection(options.pages, pageDicts.length);
|
|
65
64
|
const skippedSurfaces = skippedSurfacesOf(options?.surfaces);
|
|
66
65
|
const reads = (surface) => !skippedSurfaces.includes(surface);
|
|
66
|
+
const imageRegistry = reads("images") ? {
|
|
67
|
+
images,
|
|
68
|
+
imageIdCache: /* @__PURE__ */ new Map()
|
|
69
|
+
} : void 0;
|
|
67
70
|
const destinationRegistry = reads("destinations") ? require_navigation.createDestinationRegistry(doc.catalog, doc, (obj) => doc.pageIndex(obj), sink) : UNREAD_DESTINATION_REGISTRY;
|
|
68
71
|
const outline = reads("outline") ? require_navigation.readOutline(doc.catalog, destinationRegistry, doc, sink) : void 0;
|
|
69
72
|
const attachments = selection === void 0 ? require_attachments.readAttachments(doc.catalog, pageDicts, doc, sink) : [];
|
|
@@ -75,7 +78,7 @@ function readPdf(bytes, options) {
|
|
|
75
78
|
const pages = pageDicts.flatMap((pageDict, index) => {
|
|
76
79
|
if (selection !== void 0 && !selection.has(index)) return [];
|
|
77
80
|
require_util_abort.throwIfAborted(signal);
|
|
78
|
-
const read = readPage(index, pageDict, doc, fontResolver,
|
|
81
|
+
const read = readPage(index, pageDict, doc, fontResolver, imageRegistry, destinationRegistry, optionalContent?.layerNameOf ?? noLayerName, structure?.ownerOf ?? noStructureOwner, sink);
|
|
79
82
|
if (read.boxRow !== void 0) pageBoxRows.push(read.boxRow);
|
|
80
83
|
return [selection === void 0 ? read.page : {
|
|
81
84
|
...read.page,
|
|
@@ -309,7 +312,7 @@ function pageBoxResidueEntry(pageIndex, page, mediaBox, cropBox) {
|
|
|
309
312
|
for (const [key, value] of declaredBoxes) if (value !== void 0) entries[key] = value;
|
|
310
313
|
return require_objects.pdfDict(entries);
|
|
311
314
|
}
|
|
312
|
-
function readPage(pageIndex, page, resolver, fontResolver,
|
|
315
|
+
function readPage(pageIndex, page, resolver, fontResolver, imageRegistry, destinationRegistry, layerNameOf, structureOwnerOf, sink) {
|
|
313
316
|
const resources = resolver.resolveDict(require_objects.dictGet(page, "Resources"));
|
|
314
317
|
const mediaBox = readMediaBox(page);
|
|
315
318
|
let cropBox = readDeclaredPageBox(page, "CropBox") ?? mediaBox;
|
|
@@ -338,7 +341,7 @@ function readPage(pageIndex, page, resolver, fontResolver, images, imageIdCache,
|
|
|
338
341
|
layerNameOf
|
|
339
342
|
});
|
|
340
343
|
for (const item of extracted) {
|
|
341
|
-
const converted = require_read_convert.convertExtractedItem(item, pageMatrix, fontResolver,
|
|
344
|
+
const converted = require_read_convert.convertExtractedItem(item, pageMatrix, fontResolver, imageRegistry, resolver, sink);
|
|
342
345
|
if (converted === void 0 || !itemIntersectsVisibleRegion(converted, widthPt, heightPt)) continue;
|
|
343
346
|
const owner = item.mcid === void 0 ? void 0 : structureOwnerOf(pageIndex, item.mcid);
|
|
344
347
|
items.push(owner === void 0 ? converted : withStructure(converted, owner));
|
package/dist/read.d.cts
CHANGED
|
@@ -18,7 +18,7 @@ interface ReadPdfOptions {
|
|
|
18
18
|
/**
|
|
19
19
|
* Which document-level surfaces to read. Each is read unless its entry is `false`, so omitting the setting, or an entry, reads it as before. Independent of `pages`: it applies to a full read and a page-selected read alike.
|
|
20
20
|
*
|
|
21
|
-
* A surface turned off is never parsed (its part of the catalog is not walked, so a malformed outline, form, layer list or structure tree reports no diagnostic) and its field is absent from the result whatever the file holds; the result lists it in `skippedSurfaces`, which is how a caller tells "not read" from "none". What each one covers, and what page items lose without it:
|
|
21
|
+
* A surface turned off is never parsed (its part of the catalog is not walked, so a malformed outline, form, layer list or structure tree reports no diagnostic, and no image a page draws is decoded or diagnosed) and its field is absent from the result whatever the file holds (`images` is left empty instead); the result lists it in `skippedSurfaces`, which is how a caller tells "not read" from "none". What each one covers, and what page items lose without it:
|
|
22
22
|
*
|
|
23
23
|
* - `outline`: the `/Outlines` bookmark tree (`outline`). Page items do not depend on it, except through minted names: a destination stated as a direct array is named with the first unused `destN` in the order the reader meets it, and the outline is read before any page, so an outline entry's direct destination takes a lower number than any link's. Without the outline, the names minted for page links' direct destinations are renumbered, and `destinations` loses the entries only the outline minted; each link still resolves to the same target.
|
|
24
24
|
* - `destinations`: the named destinations of the catalog `/Dests` dictionary and the `/Names /Dests` name tree, and the registry links and outline entries name their targets through (`destinations`). Without it nothing can name an internal target, so a link whose target is internal (a `/Dest`, or a `/GoTo` action, named or direct) is left out of its page's items, and a read outline entry carries no `destination`; a link to a URI is kept as usual.
|
|
@@ -26,6 +26,7 @@ interface ReadPdfOptions {
|
|
|
26
26
|
* - `layers`: optional content, the `/OCProperties` groups and their default visibility (`layers`). Without it no item carries a `layer`, since an `/OC` membership resolves to no name; the items themselves are all still read, so content the default view hides is indistinguishable from visible content.
|
|
27
27
|
* - `structure`: the `/StructTreeRoot` element tree and its `/ParentTree` ownership map (`structure`). Without it no item carries a `structure` owner, and the parent-tree diagnostics are not reported.
|
|
28
28
|
* - `residue`: the package-level residue rows (`source`), including the per-page `page-boxes` row. Page items do not depend on it; `metadata` still reads the XMP packet it falls back to.
|
|
29
|
+
* - `images`: the image registry (`images`), which holds every image XObject and inline image a read page draws, decoded to PNG or passed through as JPEG. Without it no image's stream is decoded, `images` is an empty record (every `LayoutDocument` carries one), and each page's `image` items are left out, since they would name assets that were never read. Decoding an image costs memory in proportion to its pixels, so a read that wants only a page's text leaves this out.
|
|
29
30
|
*
|
|
30
31
|
* Apart from those stated losses and the renumbering a skipped outline causes, every page item is exactly what a read with the surface on produces.
|
|
31
32
|
*/
|
package/dist/read.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ interface ReadPdfOptions {
|
|
|
18
18
|
/**
|
|
19
19
|
* Which document-level surfaces to read. Each is read unless its entry is `false`, so omitting the setting, or an entry, reads it as before. Independent of `pages`: it applies to a full read and a page-selected read alike.
|
|
20
20
|
*
|
|
21
|
-
* A surface turned off is never parsed (its part of the catalog is not walked, so a malformed outline, form, layer list or structure tree reports no diagnostic) and its field is absent from the result whatever the file holds; the result lists it in `skippedSurfaces`, which is how a caller tells "not read" from "none". What each one covers, and what page items lose without it:
|
|
21
|
+
* A surface turned off is never parsed (its part of the catalog is not walked, so a malformed outline, form, layer list or structure tree reports no diagnostic, and no image a page draws is decoded or diagnosed) and its field is absent from the result whatever the file holds (`images` is left empty instead); the result lists it in `skippedSurfaces`, which is how a caller tells "not read" from "none". What each one covers, and what page items lose without it:
|
|
22
22
|
*
|
|
23
23
|
* - `outline`: the `/Outlines` bookmark tree (`outline`). Page items do not depend on it, except through minted names: a destination stated as a direct array is named with the first unused `destN` in the order the reader meets it, and the outline is read before any page, so an outline entry's direct destination takes a lower number than any link's. Without the outline, the names minted for page links' direct destinations are renumbered, and `destinations` loses the entries only the outline minted; each link still resolves to the same target.
|
|
24
24
|
* - `destinations`: the named destinations of the catalog `/Dests` dictionary and the `/Names /Dests` name tree, and the registry links and outline entries name their targets through (`destinations`). Without it nothing can name an internal target, so a link whose target is internal (a `/Dest`, or a `/GoTo` action, named or direct) is left out of its page's items, and a read outline entry carries no `destination`; a link to a URI is kept as usual.
|
|
@@ -26,6 +26,7 @@ interface ReadPdfOptions {
|
|
|
26
26
|
* - `layers`: optional content, the `/OCProperties` groups and their default visibility (`layers`). Without it no item carries a `layer`, since an `/OC` membership resolves to no name; the items themselves are all still read, so content the default view hides is indistinguishable from visible content.
|
|
27
27
|
* - `structure`: the `/StructTreeRoot` element tree and its `/ParentTree` ownership map (`structure`). Without it no item carries a `structure` owner, and the parent-tree diagnostics are not reported.
|
|
28
28
|
* - `residue`: the package-level residue rows (`source`), including the per-page `page-boxes` row. Page items do not depend on it; `metadata` still reads the XMP packet it falls back to.
|
|
29
|
+
* - `images`: the image registry (`images`), which holds every image XObject and inline image a read page draws, decoded to PNG or passed through as JPEG. Without it no image's stream is decoded, `images` is an empty record (every `LayoutDocument` carries one), and each page's `image` items are left out, since they would name assets that were never read. Decoding an image costs memory in proportion to its pixels, so a read that wants only a page's text leaves this out.
|
|
29
30
|
*
|
|
30
31
|
* Apart from those stated losses and the renumbering a skipped outline causes, every page item is exactly what a read with the surface on produces.
|
|
31
32
|
*/
|
package/dist/read.js
CHANGED
|
@@ -58,11 +58,14 @@ function readPdf(bytes, options) {
|
|
|
58
58
|
sink
|
|
59
59
|
});
|
|
60
60
|
const images = {};
|
|
61
|
-
const imageIdCache = /* @__PURE__ */ new Map();
|
|
62
61
|
const pageDicts = doc.pages();
|
|
63
62
|
const selection = options?.pages === void 0 ? void 0 : resolvePageSelection(options.pages, pageDicts.length);
|
|
64
63
|
const skippedSurfaces = skippedSurfacesOf(options?.surfaces);
|
|
65
64
|
const reads = (surface) => !skippedSurfaces.includes(surface);
|
|
65
|
+
const imageRegistry = reads("images") ? {
|
|
66
|
+
images,
|
|
67
|
+
imageIdCache: /* @__PURE__ */ new Map()
|
|
68
|
+
} : void 0;
|
|
66
69
|
const destinationRegistry = reads("destinations") ? createDestinationRegistry(doc.catalog, doc, (obj) => doc.pageIndex(obj), sink) : UNREAD_DESTINATION_REGISTRY;
|
|
67
70
|
const outline = reads("outline") ? readOutline(doc.catalog, destinationRegistry, doc, sink) : void 0;
|
|
68
71
|
const attachments = selection === void 0 ? readAttachments(doc.catalog, pageDicts, doc, sink) : [];
|
|
@@ -74,7 +77,7 @@ function readPdf(bytes, options) {
|
|
|
74
77
|
const pages = pageDicts.flatMap((pageDict, index) => {
|
|
75
78
|
if (selection !== void 0 && !selection.has(index)) return [];
|
|
76
79
|
throwIfAborted(signal);
|
|
77
|
-
const read = readPage(index, pageDict, doc, fontResolver,
|
|
80
|
+
const read = readPage(index, pageDict, doc, fontResolver, imageRegistry, destinationRegistry, optionalContent?.layerNameOf ?? noLayerName, structure?.ownerOf ?? noStructureOwner, sink);
|
|
78
81
|
if (read.boxRow !== void 0) pageBoxRows.push(read.boxRow);
|
|
79
82
|
return [selection === void 0 ? read.page : {
|
|
80
83
|
...read.page,
|
|
@@ -308,7 +311,7 @@ function pageBoxResidueEntry(pageIndex, page, mediaBox, cropBox) {
|
|
|
308
311
|
for (const [key, value] of declaredBoxes) if (value !== void 0) entries[key] = value;
|
|
309
312
|
return pdfDict(entries);
|
|
310
313
|
}
|
|
311
|
-
function readPage(pageIndex, page, resolver, fontResolver,
|
|
314
|
+
function readPage(pageIndex, page, resolver, fontResolver, imageRegistry, destinationRegistry, layerNameOf, structureOwnerOf, sink) {
|
|
312
315
|
const resources = resolver.resolveDict(dictGet(page, "Resources"));
|
|
313
316
|
const mediaBox = readMediaBox(page);
|
|
314
317
|
let cropBox = readDeclaredPageBox(page, "CropBox") ?? mediaBox;
|
|
@@ -337,7 +340,7 @@ function readPage(pageIndex, page, resolver, fontResolver, images, imageIdCache,
|
|
|
337
340
|
layerNameOf
|
|
338
341
|
});
|
|
339
342
|
for (const item of extracted) {
|
|
340
|
-
const converted = convertExtractedItem(item, pageMatrix, fontResolver,
|
|
343
|
+
const converted = convertExtractedItem(item, pageMatrix, fontResolver, imageRegistry, resolver, sink);
|
|
341
344
|
if (converted === void 0 || !itemIntersectsVisibleRegion(converted, widthPt, heightPt)) continue;
|
|
342
345
|
const owner = item.mcid === void 0 ? void 0 : structureOwnerOf(pageIndex, item.mcid);
|
|
343
346
|
items.push(owner === void 0 ? converted : withStructure(converted, owner));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pdf-codec",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.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": {
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
|
|
101
101
|
"dependencies": {
|
|
102
102
|
"byte-codec": "2.0.2",
|
|
103
|
-
"document-schema.js": "7.16.
|
|
103
|
+
"document-schema.js": "7.16.2",
|
|
104
104
|
"fflate": "0.8.3",
|
|
105
105
|
"zod": "4.4.3"
|
|
106
106
|
},
|