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 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 Uint8Array.from(out);
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
- out.push(0, 0, 0, 0);
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
- out.push(...ascii85GroupBytes(tuple, ASCII85_GROUP_BYTES));
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
- out.push(...ascii85GroupBytes(padded, tuple.length - 1));
281
+ for (const decoded of ascii85GroupBytes(padded, tuple.length - 1)) out.push(decoded);
264
282
  }
265
- return Uint8Array.from(out);
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
- const digits = [];
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) digits.push(value);
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 Uint8Array.from(out);
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 Uint8Array.from(out);
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
- out.push(0, 0, 0, 0);
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
- out.push(...ascii85GroupBytes(tuple, ASCII85_GROUP_BYTES));
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
- out.push(...ascii85GroupBytes(padded, tuple.length - 1));
280
+ for (const decoded of ascii85GroupBytes(padded, tuple.length - 1)) out.push(decoded);
263
281
  }
264
- return Uint8Array.from(out);
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
- const digits = [];
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) digits.push(value);
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 Uint8Array.from(out);
336
+ return out.toBytes();
312
337
  }
313
338
  //#endregion
314
339
  export { ascii85Decode, asciiHexDecode, decodeStream, lzwDecode, runLengthDecode };
@@ -58,27 +58,51 @@ function resolveColorSpace(csObj, resolver, sink) {
58
58
  name: family ?? "(unrecognised array colour space)"
59
59
  };
60
60
  }
61
- function unpackSamples(data, width, height, componentsPerPixel, bitsPerComponent) {
62
- if (bitsPerComponent === BITS_PER_BYTE) return Array.from(data.subarray(0, width * height * componentsPerPixel));
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 bytesPerRow = Math.ceil(samplesPerRow * bitsPerComponent / BITS_PER_BYTE);
65
- const out = [];
98
+ const rowSamples = work.allocate(samplesPerRow);
66
99
  for (let row = 0; row < height; row++) {
67
- const rowStart = row * bytesPerRow;
68
- let bitPos = 0;
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
- return out;
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 buildRawImage(data, width, height, bitsPerComponent, colorSpace, inverted) {
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 indices = unpackSamples(data, width, height, 1, bitsPerComponent);
154
+ const channels = colorSpace.base.kind === "gray" ? 1 : RGB_CHANNEL_COUNT;
114
155
  const baseComponents = componentsOf(colorSpace.base);
115
- const out = new Uint8Array(width * height * (colorSpace.base.kind === "gray" ? 1 : RGB_CHANNEL_COUNT));
116
- let outIdx = 0;
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
- if (colorSpace.base.kind === "gray") out[outIdx++] = rgb.r;
120
- else {
121
- out[outIdx++] = rgb.r;
122
- out[outIdx++] = rgb.g;
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: colorSpace.base.kind === "gray" ? 1 : RGB_CHANNEL_COUNT,
178
+ channels,
130
179
  data: out
131
180
  };
132
181
  }
133
- if (colorSpace.kind === "gray") {
134
- const samples = unpackSamples(data, width, height, 1, bitsPerComponent);
135
- return {
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: 1,
139
- data: Uint8Array.from(samples, (v) => scaleToByte(v, maxValue, inverted))
188
+ channels,
189
+ data: data.subarray(0, available)
140
190
  };
141
- }
142
- if (colorSpace.kind === "rgb") {
143
- const samples = unpackSamples(data, width, height, RGB_CHANNEL_COUNT, bitsPerComponent);
144
- const out = Uint8Array.from(samples, (v) => scaleToByte(v, maxValue, inverted));
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: RGB_CHANNEL_COUNT,
200
+ channels,
149
201
  data: out
150
202
  };
151
203
  }
152
- const samples = unpackSamples(data, width, height, CMYK_COMPONENT_COUNT, bitsPerComponent);
153
- const out = new Uint8Array(width * height * RGB_CHANNEL_COUNT);
154
- for (let px = 0; px < width * height; px++) {
155
- const rgb = cmykToRgbByte(scaleToByte(samples[px * CMYK_COMPONENT_COUNT] ?? 0, maxValue, inverted) / MAX_BYTE_VALUE, scaleToByte(samples[px * CMYK_COMPONENT_COUNT + 1] ?? 0, maxValue, inverted) / MAX_BYTE_VALUE, scaleToByte(samples[px * CMYK_COMPONENT_COUNT + 2] ?? 0, maxValue, inverted) / MAX_BYTE_VALUE, scaleToByte(samples[px * CMYK_COMPONENT_COUNT + 3] ?? 0, maxValue, inverted) / MAX_BYTE_VALUE);
156
- out[px * RGB_CHANNEL_COUNT] = rgb.r;
157
- out[px * RGB_CHANNEL_COUNT + 1] = rgb.g;
158
- out[px * RGB_CHANNEL_COUNT + 2] = rgb.b;
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 = buildRawImage(decoded.bytes, width, height, bitsPerComponent, colorSpace, inverted);
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;
@@ -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 };
@@ -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 };
@@ -57,27 +57,51 @@ function resolveColorSpace(csObj, resolver, sink) {
57
57
  name: family ?? "(unrecognised array colour space)"
58
58
  };
59
59
  }
60
- function unpackSamples(data, width, height, componentsPerPixel, bitsPerComponent) {
61
- if (bitsPerComponent === BITS_PER_BYTE) return Array.from(data.subarray(0, width * height * componentsPerPixel));
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 bytesPerRow = Math.ceil(samplesPerRow * bitsPerComponent / BITS_PER_BYTE);
64
- const out = [];
97
+ const rowSamples = work.allocate(samplesPerRow);
65
98
  for (let row = 0; row < height; row++) {
66
- const rowStart = row * bytesPerRow;
67
- let bitPos = 0;
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
- return out;
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 buildRawImage(data, width, height, bitsPerComponent, colorSpace, inverted) {
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 indices = unpackSamples(data, width, height, 1, bitsPerComponent);
153
+ const channels = colorSpace.base.kind === "gray" ? 1 : RGB_CHANNEL_COUNT;
113
154
  const baseComponents = componentsOf(colorSpace.base);
114
- const out = new Uint8Array(width * height * (colorSpace.base.kind === "gray" ? 1 : RGB_CHANNEL_COUNT));
115
- let outIdx = 0;
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
- if (colorSpace.base.kind === "gray") out[outIdx++] = rgb.r;
119
- else {
120
- out[outIdx++] = rgb.r;
121
- out[outIdx++] = rgb.g;
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: colorSpace.base.kind === "gray" ? 1 : RGB_CHANNEL_COUNT,
177
+ channels,
129
178
  data: out
130
179
  };
131
180
  }
132
- if (colorSpace.kind === "gray") {
133
- const samples = unpackSamples(data, width, height, 1, bitsPerComponent);
134
- return {
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: 1,
138
- data: Uint8Array.from(samples, (v) => scaleToByte(v, maxValue, inverted))
187
+ channels,
188
+ data: data.subarray(0, available)
139
189
  };
140
- }
141
- if (colorSpace.kind === "rgb") {
142
- const samples = unpackSamples(data, width, height, RGB_CHANNEL_COUNT, bitsPerComponent);
143
- const out = Uint8Array.from(samples, (v) => scaleToByte(v, maxValue, inverted));
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: RGB_CHANNEL_COUNT,
199
+ channels,
148
200
  data: out
149
201
  };
150
202
  }
151
- const samples = unpackSamples(data, width, height, CMYK_COMPONENT_COUNT, bitsPerComponent);
152
- const out = new Uint8Array(width * height * RGB_CHANNEL_COUNT);
153
- for (let px = 0; px < width * height; px++) {
154
- const rgb = cmykToRgbByte(scaleToByte(samples[px * CMYK_COMPONENT_COUNT] ?? 0, maxValue, inverted) / MAX_BYTE_VALUE, scaleToByte(samples[px * CMYK_COMPONENT_COUNT + 1] ?? 0, maxValue, inverted) / MAX_BYTE_VALUE, scaleToByte(samples[px * CMYK_COMPONENT_COUNT + 2] ?? 0, maxValue, inverted) / MAX_BYTE_VALUE, scaleToByte(samples[px * CMYK_COMPONENT_COUNT + 3] ?? 0, maxValue, inverted) / MAX_BYTE_VALUE);
155
- out[px * RGB_CHANNEL_COUNT] = rgb.r;
156
- out[px * RGB_CHANNEL_COUNT + 1] = rgb.g;
157
- out[px * RGB_CHANNEL_COUNT + 2] = rgb.b;
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 = buildRawImage(decoded.bytes, width, height, bitsPerComponent, colorSpace, inverted);
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 `structure` are those fields, and `residue` is the package-level `source` channel. A `LayoutDocument` lists the ones its read skipped in `skippedSurfaces`.
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 `structure` are those fields, and `residue` is the package-level `source` channel. A `LayoutDocument` lists the ones its read skipped in `skippedSurfaces`.
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 `structure` are those fields, and `residue` is the package-level `source` channel. A `LayoutDocument` lists the ones its read skipped in `skippedSurfaces`.
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 `structure` are those fields, and `residue` is the package-level `source` channel. A `LayoutDocument` lists the ones its read skipped in `skippedSurfaces`.
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),
@@ -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
- function convertExtractedItem(item, pageMatrix, fontResolver, images, imageIdCache, resolver, sink) {
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 (item.kind === "image") return convertImage(item, pageMatrix, images, imageIdCache, resolver, sink);
17
- return convertInlineImage(item, pageMatrix, images, resolver, sink);
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);
@@ -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
- declare function convertExtractedItem(item: ExtractedItem, pageMatrix: Matrix, fontResolver: FontResolverService, images: Record<string, LayoutImageAsset>, imageIdCache: Map<PdfDict, string | null>, resolver: Readonly<PdfObjectResolver>, sink: PdfDiagnosticSink): LayoutItem | undefined;
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 };
@@ -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
- declare function convertExtractedItem(item: ExtractedItem, pageMatrix: Matrix, fontResolver: FontResolverService, images: Record<string, LayoutImageAsset>, imageIdCache: Map<PdfDict, string | null>, resolver: Readonly<PdfObjectResolver>, sink: PdfDiagnosticSink): LayoutItem | undefined;
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 };
@@ -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
- function convertExtractedItem(item, pageMatrix, fontResolver, images, imageIdCache, resolver, sink) {
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 (item.kind === "image") return convertImage(item, pageMatrix, images, imageIdCache, resolver, sink);
16
- return convertInlineImage(item, pageMatrix, images, resolver, sink);
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, images, imageIdCache, destinationRegistry, optionalContent?.layerNameOf ?? noLayerName, structure?.ownerOf ?? noStructureOwner, sink);
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, images, imageIdCache, destinationRegistry, layerNameOf, structureOwnerOf, sink) {
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, images, imageIdCache, resolver, sink);
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, images, imageIdCache, destinationRegistry, optionalContent?.layerNameOf ?? noLayerName, structure?.ownerOf ?? noStructureOwner, sink);
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, images, imageIdCache, destinationRegistry, layerNameOf, structureOwnerOf, sink) {
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, images, imageIdCache, resolver, sink);
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.5.0",
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.1",
103
+ "document-schema.js": "7.16.2",
104
104
  "fflate": "0.8.3",
105
105
  "zod": "4.4.3"
106
106
  },