pdf-codec 5.5.1 → 5.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/{interpret-BDVrFWhd.cjs → interpret-2_N_lh0h.cjs} +2 -3
- package/dist/{interpret---0lZPlv.js → interpret-BeMKZGBm.js} +2 -3
- package/dist/interpret-colors.cjs +1 -1
- package/dist/interpret-colors.js +1 -1
- package/dist/interpret.cjs +1 -1
- package/dist/interpret.js +1 -1
- 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/{raster-HUE5bw5s.cjs → raster-B8i3Tgjj.cjs} +1 -1
- package/dist/{raster-DUmwfi3F.js → raster-CIc6ot36.js} +1 -1
- package/dist/raster-text.cjs +1 -1
- package/dist/raster-text.js +1 -1
- package/dist/raster.cjs +1 -1
- package/dist/raster.js +1 -1
- 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 +8 -5
- package/dist/read.d.cts +2 -1
- package/dist/read.d.ts +2 -1
- package/dist/read.js +8 -5
- package/package.json +1 -1
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 };
|