byte-codec 1.3.0 → 1.4.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
@@ -42,16 +42,16 @@ To run a single test file, pass its path to vitest directly, e.g. `pnpm exec vit
42
42
 
43
43
  ## What it provides
44
44
 
45
- | Module | Exports |
46
- | ------------------ | ----------------------------------------------------------------------------------------------------------- |
47
- | `bytes/writer` | `ByteWriter` (chunked growable byte-output builder), `concatBytes` |
48
- | `bytes/reader` | `ByteReader` (sequential big/little-endian byte reader), `isAsciiWhitespace` |
49
- | `bytes/crc32` | `crc32` (IEEE 802.3 / ZIP / PNG polynomial table-driven CRC-32) |
50
- | `bytes/flate` | `deflate`, `inflate`, `inflateTolerant` (fflate-backed DEFLATE compression/decompression with a safety cap) |
51
- | `image/png-encode` | `encodePng` (raw RGB/RGBA pixels → PNG bytes) |
52
- | `image/png-decode` | `decodePng` (PNG bytes → raw pixels), `RawImage` |
53
- | `image/png-filter` | `filterScanlines`, `unfilterScanlines` (the five PNG scanline filters) |
54
- | `image/jpeg-info` | `readJpegInfo` (JPEG header reader: dimensions, components, progressive flag — no sample decoding) |
45
+ | Module | Exports |
46
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
47
+ | `bytes/writer` | `ByteWriter` (chunked growable byte-output builder), `concatBytes` |
48
+ | `bytes/reader` | `ByteReader` (sequential big/little-endian byte reader), `isAsciiWhitespace` |
49
+ | `bytes/crc32` | `crc32` (IEEE 802.3 / ZIP / PNG polynomial table-driven CRC-32) |
50
+ | `bytes/flate` | `deflate`, `inflate`, `inflateTolerant` (fflate-backed DEFLATE compression/decompression with a safety cap) |
51
+ | `image/png-encode` | `encodePng` (raw RGB/RGBA pixels → PNG bytes; a truecolour source image whose pixels reduce to 256 or fewer distinct colours is encoded both as indexed colour, PNG colour type 3 with a PLTE and, where needed, a tRNS chunk, and as plain truecolour, and whichever comes out smaller is returned -- indexed colour wins for the large flat-colour images typical of diagrams and screenshots, but the PLTE/tRNS chunk overhead can make it larger for small images, so the encoder measures rather than assumes, at the cost of a full second encode for every eligible image; throws if either `width` or `height` is not a positive integer no greater than the PNG spec's own IHDR limit, or if their product exceeds a practical per-call pixel-count ceiling, since PNG's IHDR chunk has no valid encoding for zero, negative, fractional, or non-finite dimensions and this bounds an otherwise-unbounded encode to a fixed worst case) |
52
+ | `image/png-decode` | `decodePng` (PNG bytes → raw pixels, handling every PNG colour type including indexed/palette via its own PLTE/tRNS lookup), `RawImage` |
53
+ | `image/png-filter` | `filterScanlines`, `unfilterScanlines` (the five PNG scanline filters) |
54
+ | `image/jpeg-info` | `readJpegInfo` (JPEG header reader: dimensions, components, progressive flag — no sample decoding) |
55
55
 
56
56
  ## Conventions
57
57
 
package/dist/index.cjs CHANGED
@@ -258,6 +258,9 @@ const PNG_SIGNATURE$1 = new Uint8Array([
258
258
  26,
259
259
  10
260
260
  ]);
261
+ const MAX_PALETTE_ENTRIES = 256;
262
+ const PNG_MAX_DIMENSION = 2147483647;
263
+ const PNG_MAX_PIXELS = 1e8;
261
264
  function u32be(value) {
262
265
  const bytes = /* @__PURE__ */ new Uint8Array(4);
263
266
  new DataView(bytes.buffer).setUint32(0, value);
@@ -270,11 +273,73 @@ function writeChunk(writer, type, data) {
270
273
  writer.writeBytes(data);
271
274
  writer.writeBytes(u32be(crc32(concatBytes([typeBytes, data]))));
272
275
  }
276
+ function writeIhdr(writer, width, height, colorType) {
277
+ const ihdr = /* @__PURE__ */ new Uint8Array(13);
278
+ const ihdrView = new DataView(ihdr.buffer);
279
+ ihdrView.setUint32(0, width);
280
+ ihdrView.setUint32(4, height);
281
+ ihdr[8] = 8;
282
+ ihdr[9] = colorType;
283
+ ihdr[10] = 0;
284
+ ihdr[11] = 0;
285
+ ihdr[12] = 0;
286
+ writeChunk(writer, "IHDR", ihdr);
287
+ }
273
288
  function colorTypeFor(image) {
274
289
  if (image.channels === 1) return image.alpha === void 0 ? 0 : 4;
275
290
  return image.alpha === void 0 ? 2 : 6;
276
291
  }
277
- function encodePng(image, options = {}) {
292
+ function detectPalette(image) {
293
+ const { width, height, data, alpha } = image;
294
+ const pixelCount = width * height;
295
+ const colorToIndex = /* @__PURE__ */ new Map();
296
+ const indices = new Uint8Array(pixelCount);
297
+ const paletteRgb = [];
298
+ const paletteAlpha = [];
299
+ for (let i = 0; i < pixelCount; i++) {
300
+ const base = i * 3;
301
+ const r = data[base] ?? 0;
302
+ const g = data[base + 1] ?? 0;
303
+ const b = data[base + 2] ?? 0;
304
+ const a = alpha === void 0 ? 255 : alpha[i] ?? 0;
305
+ const key = r + g * 256 + b * 65536 + a * 16777216;
306
+ let index = colorToIndex.get(key);
307
+ if (index === void 0) {
308
+ if (colorToIndex.size >= MAX_PALETTE_ENTRIES) return;
309
+ index = colorToIndex.size;
310
+ colorToIndex.set(key, index);
311
+ paletteRgb.push(r, g, b);
312
+ paletteAlpha.push(a);
313
+ }
314
+ indices[i] = index;
315
+ }
316
+ const palette = Uint8Array.from(paletteRgb);
317
+ if (alpha === void 0) return {
318
+ indices,
319
+ palette
320
+ };
321
+ let trnsLength = paletteAlpha.length;
322
+ while (trnsLength > 1 && paletteAlpha[trnsLength - 1] === 255) trnsLength--;
323
+ return {
324
+ indices,
325
+ palette,
326
+ trns: Uint8Array.from(paletteAlpha.slice(0, trnsLength))
327
+ };
328
+ }
329
+ function buildPng(writeBody) {
330
+ const writer = new ByteWriter();
331
+ writer.writeBytes(PNG_SIGNATURE$1);
332
+ writeBody(writer);
333
+ writeChunk(writer, "IEND", /* @__PURE__ */ new Uint8Array(0));
334
+ return writer.toBytes();
335
+ }
336
+ function writeIndexedPng(writer, width, height, encoding, options) {
337
+ writeIhdr(writer, width, height, 3);
338
+ writeChunk(writer, "PLTE", encoding.palette);
339
+ if (encoding.trns !== void 0) writeChunk(writer, "tRNS", encoding.trns);
340
+ writeChunk(writer, "IDAT", deflate(filterScanlines(encoding.indices, height, width, 1, options.filter ?? "adaptive")));
341
+ }
342
+ function writeTruecolorPng(writer, image, options) {
278
343
  const { width, height, channels, data, alpha } = image;
279
344
  const outChannels = alpha === void 0 ? channels : channels + 1;
280
345
  const bytesPerRow = width * outChannels;
@@ -286,22 +351,24 @@ function encodePng(image, options = {}) {
286
351
  for (let c = 0; c < channels; c++) interleaved[dstBase + c] = data[srcBase + c];
287
352
  if (alpha !== void 0) interleaved[dstBase + channels] = alpha[i];
288
353
  }
289
- const compressed = deflate(filterScanlines(interleaved, height, bytesPerRow, outChannels, options.filter ?? "adaptive"));
290
- const ihdr = /* @__PURE__ */ new Uint8Array(13);
291
- const ihdrView = new DataView(ihdr.buffer);
292
- ihdrView.setUint32(0, width);
293
- ihdrView.setUint32(4, height);
294
- ihdr[8] = 8;
295
- ihdr[9] = colorTypeFor(image);
296
- ihdr[10] = 0;
297
- ihdr[11] = 0;
298
- ihdr[12] = 0;
299
- const writer = new ByteWriter();
300
- writer.writeBytes(PNG_SIGNATURE$1);
301
- writeChunk(writer, "IHDR", ihdr);
302
- writeChunk(writer, "IDAT", compressed);
303
- writeChunk(writer, "IEND", /* @__PURE__ */ new Uint8Array(0));
304
- return writer.toBytes();
354
+ writeIhdr(writer, width, height, colorTypeFor(image));
355
+ writeChunk(writer, "IDAT", deflate(filterScanlines(interleaved, height, bytesPerRow, outChannels, options.filter ?? "adaptive")));
356
+ }
357
+ function encodePng(image, options = {}) {
358
+ if (!Number.isInteger(image.width) || image.width <= 0 || image.width > 2147483647 || !Number.isInteger(image.height) || image.height <= 0 || image.height > 2147483647) throw new Error(`cannot encode a PNG with an invalid dimension (width=${image.width}, height=${image.height}); the PNG spec's IHDR section requires both width and height to be positive integers no greater than ${PNG_MAX_DIMENSION}`);
359
+ const pixelCount = image.width * image.height;
360
+ if (pixelCount > 1e8) throw new Error(`cannot encode a PNG with ${pixelCount} pixels (width=${image.width}, height=${image.height}); each dimension is individually within the PNG spec's own limit, but this encoder bounds their product to ${PNG_MAX_PIXELS} to avoid an unbounded per-pixel scan and allocation`);
361
+ const paletteEncoding = image.channels === 3 ? detectPalette(image) : void 0;
362
+ if (paletteEncoding === void 0) return buildPng((writer) => {
363
+ writeTruecolorPng(writer, image, options);
364
+ });
365
+ const indexed = buildPng((writer) => {
366
+ writeIndexedPng(writer, image.width, image.height, paletteEncoding, options);
367
+ });
368
+ const truecolor = buildPng((writer) => {
369
+ writeTruecolorPng(writer, image, options);
370
+ });
371
+ return indexed.length <= truecolor.length ? indexed : truecolor;
305
372
  }
306
373
  //#endregion
307
374
  //#region src/image/png-decode.ts
@@ -550,6 +617,8 @@ function readJpegInfo(bytes) {
550
617
  exports.ByteReader = ByteReader;
551
618
  exports.ByteWriter = ByteWriter;
552
619
  exports.MAX_INFLATE_OUTPUT_BYTES = MAX_INFLATE_OUTPUT_BYTES;
620
+ exports.PNG_MAX_DIMENSION = PNG_MAX_DIMENSION;
621
+ exports.PNG_MAX_PIXELS = PNG_MAX_PIXELS;
553
622
  exports.concatBytes = concatBytes;
554
623
  exports.crc32 = crc32;
555
624
  exports.decodePng = decodePng;
package/dist/index.d.cts CHANGED
@@ -57,6 +57,8 @@ interface PngDecodeOptions {
57
57
  declare function decodePng(bytes: Uint8Array<ArrayBuffer>, options?: PngDecodeOptions): RawImage;
58
58
  //#endregion
59
59
  //#region src/image/png-encode.d.ts
60
+ declare const PNG_MAX_DIMENSION = 2147483647;
61
+ declare const PNG_MAX_PIXELS = 100000000;
60
62
  interface PngEncodeOptions {
61
63
  readonly filter?: "none" | "adaptive";
62
64
  }
@@ -78,4 +80,4 @@ interface JpegInfo {
78
80
  }
79
81
  declare function readJpegInfo(bytes: Uint8Array<ArrayBuffer>): JpegInfo;
80
82
  //#endregion
81
- export { ByteReader, ByteWriter, DeflateLevel, InflateResult, JpegInfo, MAX_INFLATE_OUTPUT_BYTES, PngDecodeOptions, PngEncodeOptions, PngFilterType, RawImage, concatBytes, crc32, decodePng, deflate, encodePng, filterScanlines, inflate, inflateTolerant, isAsciiWhitespace, readJpegInfo, unfilterScanlines };
83
+ export { ByteReader, ByteWriter, DeflateLevel, InflateResult, JpegInfo, MAX_INFLATE_OUTPUT_BYTES, PNG_MAX_DIMENSION, PNG_MAX_PIXELS, PngDecodeOptions, PngEncodeOptions, PngFilterType, RawImage, concatBytes, crc32, decodePng, deflate, encodePng, filterScanlines, inflate, inflateTolerant, isAsciiWhitespace, readJpegInfo, unfilterScanlines };
package/dist/index.d.ts CHANGED
@@ -57,6 +57,8 @@ interface PngDecodeOptions {
57
57
  declare function decodePng(bytes: Uint8Array<ArrayBuffer>, options?: PngDecodeOptions): RawImage;
58
58
  //#endregion
59
59
  //#region src/image/png-encode.d.ts
60
+ declare const PNG_MAX_DIMENSION = 2147483647;
61
+ declare const PNG_MAX_PIXELS = 100000000;
60
62
  interface PngEncodeOptions {
61
63
  readonly filter?: "none" | "adaptive";
62
64
  }
@@ -78,4 +80,4 @@ interface JpegInfo {
78
80
  }
79
81
  declare function readJpegInfo(bytes: Uint8Array<ArrayBuffer>): JpegInfo;
80
82
  //#endregion
81
- export { ByteReader, ByteWriter, DeflateLevel, InflateResult, JpegInfo, MAX_INFLATE_OUTPUT_BYTES, PngDecodeOptions, PngEncodeOptions, PngFilterType, RawImage, concatBytes, crc32, decodePng, deflate, encodePng, filterScanlines, inflate, inflateTolerant, isAsciiWhitespace, readJpegInfo, unfilterScanlines };
83
+ export { ByteReader, ByteWriter, DeflateLevel, InflateResult, JpegInfo, MAX_INFLATE_OUTPUT_BYTES, PNG_MAX_DIMENSION, PNG_MAX_PIXELS, PngDecodeOptions, PngEncodeOptions, PngFilterType, RawImage, concatBytes, crc32, decodePng, deflate, encodePng, filterScanlines, inflate, inflateTolerant, isAsciiWhitespace, readJpegInfo, unfilterScanlines };
package/dist/index.js CHANGED
@@ -257,6 +257,9 @@ const PNG_SIGNATURE$1 = new Uint8Array([
257
257
  26,
258
258
  10
259
259
  ]);
260
+ const MAX_PALETTE_ENTRIES = 256;
261
+ const PNG_MAX_DIMENSION = 2147483647;
262
+ const PNG_MAX_PIXELS = 1e8;
260
263
  function u32be(value) {
261
264
  const bytes = /* @__PURE__ */ new Uint8Array(4);
262
265
  new DataView(bytes.buffer).setUint32(0, value);
@@ -269,11 +272,73 @@ function writeChunk(writer, type, data) {
269
272
  writer.writeBytes(data);
270
273
  writer.writeBytes(u32be(crc32(concatBytes([typeBytes, data]))));
271
274
  }
275
+ function writeIhdr(writer, width, height, colorType) {
276
+ const ihdr = /* @__PURE__ */ new Uint8Array(13);
277
+ const ihdrView = new DataView(ihdr.buffer);
278
+ ihdrView.setUint32(0, width);
279
+ ihdrView.setUint32(4, height);
280
+ ihdr[8] = 8;
281
+ ihdr[9] = colorType;
282
+ ihdr[10] = 0;
283
+ ihdr[11] = 0;
284
+ ihdr[12] = 0;
285
+ writeChunk(writer, "IHDR", ihdr);
286
+ }
272
287
  function colorTypeFor(image) {
273
288
  if (image.channels === 1) return image.alpha === void 0 ? 0 : 4;
274
289
  return image.alpha === void 0 ? 2 : 6;
275
290
  }
276
- function encodePng(image, options = {}) {
291
+ function detectPalette(image) {
292
+ const { width, height, data, alpha } = image;
293
+ const pixelCount = width * height;
294
+ const colorToIndex = /* @__PURE__ */ new Map();
295
+ const indices = new Uint8Array(pixelCount);
296
+ const paletteRgb = [];
297
+ const paletteAlpha = [];
298
+ for (let i = 0; i < pixelCount; i++) {
299
+ const base = i * 3;
300
+ const r = data[base] ?? 0;
301
+ const g = data[base + 1] ?? 0;
302
+ const b = data[base + 2] ?? 0;
303
+ const a = alpha === void 0 ? 255 : alpha[i] ?? 0;
304
+ const key = r + g * 256 + b * 65536 + a * 16777216;
305
+ let index = colorToIndex.get(key);
306
+ if (index === void 0) {
307
+ if (colorToIndex.size >= MAX_PALETTE_ENTRIES) return;
308
+ index = colorToIndex.size;
309
+ colorToIndex.set(key, index);
310
+ paletteRgb.push(r, g, b);
311
+ paletteAlpha.push(a);
312
+ }
313
+ indices[i] = index;
314
+ }
315
+ const palette = Uint8Array.from(paletteRgb);
316
+ if (alpha === void 0) return {
317
+ indices,
318
+ palette
319
+ };
320
+ let trnsLength = paletteAlpha.length;
321
+ while (trnsLength > 1 && paletteAlpha[trnsLength - 1] === 255) trnsLength--;
322
+ return {
323
+ indices,
324
+ palette,
325
+ trns: Uint8Array.from(paletteAlpha.slice(0, trnsLength))
326
+ };
327
+ }
328
+ function buildPng(writeBody) {
329
+ const writer = new ByteWriter();
330
+ writer.writeBytes(PNG_SIGNATURE$1);
331
+ writeBody(writer);
332
+ writeChunk(writer, "IEND", /* @__PURE__ */ new Uint8Array(0));
333
+ return writer.toBytes();
334
+ }
335
+ function writeIndexedPng(writer, width, height, encoding, options) {
336
+ writeIhdr(writer, width, height, 3);
337
+ writeChunk(writer, "PLTE", encoding.palette);
338
+ if (encoding.trns !== void 0) writeChunk(writer, "tRNS", encoding.trns);
339
+ writeChunk(writer, "IDAT", deflate(filterScanlines(encoding.indices, height, width, 1, options.filter ?? "adaptive")));
340
+ }
341
+ function writeTruecolorPng(writer, image, options) {
277
342
  const { width, height, channels, data, alpha } = image;
278
343
  const outChannels = alpha === void 0 ? channels : channels + 1;
279
344
  const bytesPerRow = width * outChannels;
@@ -285,22 +350,24 @@ function encodePng(image, options = {}) {
285
350
  for (let c = 0; c < channels; c++) interleaved[dstBase + c] = data[srcBase + c];
286
351
  if (alpha !== void 0) interleaved[dstBase + channels] = alpha[i];
287
352
  }
288
- const compressed = deflate(filterScanlines(interleaved, height, bytesPerRow, outChannels, options.filter ?? "adaptive"));
289
- const ihdr = /* @__PURE__ */ new Uint8Array(13);
290
- const ihdrView = new DataView(ihdr.buffer);
291
- ihdrView.setUint32(0, width);
292
- ihdrView.setUint32(4, height);
293
- ihdr[8] = 8;
294
- ihdr[9] = colorTypeFor(image);
295
- ihdr[10] = 0;
296
- ihdr[11] = 0;
297
- ihdr[12] = 0;
298
- const writer = new ByteWriter();
299
- writer.writeBytes(PNG_SIGNATURE$1);
300
- writeChunk(writer, "IHDR", ihdr);
301
- writeChunk(writer, "IDAT", compressed);
302
- writeChunk(writer, "IEND", /* @__PURE__ */ new Uint8Array(0));
303
- return writer.toBytes();
353
+ writeIhdr(writer, width, height, colorTypeFor(image));
354
+ writeChunk(writer, "IDAT", deflate(filterScanlines(interleaved, height, bytesPerRow, outChannels, options.filter ?? "adaptive")));
355
+ }
356
+ function encodePng(image, options = {}) {
357
+ if (!Number.isInteger(image.width) || image.width <= 0 || image.width > 2147483647 || !Number.isInteger(image.height) || image.height <= 0 || image.height > 2147483647) throw new Error(`cannot encode a PNG with an invalid dimension (width=${image.width}, height=${image.height}); the PNG spec's IHDR section requires both width and height to be positive integers no greater than ${PNG_MAX_DIMENSION}`);
358
+ const pixelCount = image.width * image.height;
359
+ if (pixelCount > 1e8) throw new Error(`cannot encode a PNG with ${pixelCount} pixels (width=${image.width}, height=${image.height}); each dimension is individually within the PNG spec's own limit, but this encoder bounds their product to ${PNG_MAX_PIXELS} to avoid an unbounded per-pixel scan and allocation`);
360
+ const paletteEncoding = image.channels === 3 ? detectPalette(image) : void 0;
361
+ if (paletteEncoding === void 0) return buildPng((writer) => {
362
+ writeTruecolorPng(writer, image, options);
363
+ });
364
+ const indexed = buildPng((writer) => {
365
+ writeIndexedPng(writer, image.width, image.height, paletteEncoding, options);
366
+ });
367
+ const truecolor = buildPng((writer) => {
368
+ writeTruecolorPng(writer, image, options);
369
+ });
370
+ return indexed.length <= truecolor.length ? indexed : truecolor;
304
371
  }
305
372
  //#endregion
306
373
  //#region src/image/png-decode.ts
@@ -546,4 +613,4 @@ function readJpegInfo(bytes) {
546
613
  throw new Error("no SOF marker found in JPEG file");
547
614
  }
548
615
  //#endregion
549
- export { ByteReader, ByteWriter, MAX_INFLATE_OUTPUT_BYTES, concatBytes, crc32, decodePng, deflate, encodePng, filterScanlines, inflate, inflateTolerant, isAsciiWhitespace, readJpegInfo, unfilterScanlines };
616
+ export { ByteReader, ByteWriter, MAX_INFLATE_OUTPUT_BYTES, PNG_MAX_DIMENSION, PNG_MAX_PIXELS, concatBytes, crc32, decodePng, deflate, encodePng, filterScanlines, inflate, inflateTolerant, isAsciiWhitespace, readJpegInfo, unfilterScanlines };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "byte-codec",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Generic byte-level primitives (ByteWriter, ByteReader, CRC-32, deflate/inflate) and PNG/JPEG image encoding/decoding with zero PDF knowledge — the shared utility package for the documents.js family.",
5
5
  "type": "module",
6
6
  "repository": {