byte-codec 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/bytes/crc32-BVCBBYa3.d.cts +4 -0
  2. package/dist/bytes/crc32-BVCBBYa3.d.ts +4 -0
  3. package/dist/bytes/crc32.cjs +21 -0
  4. package/dist/bytes/crc32.js +20 -0
  5. package/dist/bytes/flate-CbSD2OU9.d.cts +12 -0
  6. package/dist/bytes/flate-CbSD2OU9.d.ts +12 -0
  7. package/dist/bytes/flate.cjs +53 -0
  8. package/dist/bytes/flate.js +49 -0
  9. package/dist/bytes/reader-CDx_NmAQ.d.cts +20 -0
  10. package/dist/bytes/reader-CDx_NmAQ.d.ts +20 -0
  11. package/dist/bytes/reader.cjs +60 -0
  12. package/dist/bytes/reader.js +58 -0
  13. package/dist/bytes/writer-B9uaqo-B.d.cts +13 -0
  14. package/dist/bytes/writer-B9uaqo-B.d.ts +13 -0
  15. package/dist/bytes/writer.cjs +37 -0
  16. package/dist/bytes/writer.js +35 -0
  17. package/dist/image/jpeg-info-Dh6D8PRh.d.cts +12 -0
  18. package/dist/image/jpeg-info-Dh6D8PRh.d.ts +12 -0
  19. package/dist/image/jpeg-info.cjs +71 -0
  20. package/dist/image/jpeg-info.js +70 -0
  21. package/dist/image/png-decode-Dvizumot.d.cts +14 -0
  22. package/dist/image/png-decode-Dvizumot.d.ts +14 -0
  23. package/dist/image/png-decode.cjs +181 -0
  24. package/dist/image/png-decode.js +180 -0
  25. package/dist/image/png-encode-C5Jj9gBX.d.ts +9 -0
  26. package/dist/image/png-encode-CpwBOmkg.d.cts +9 -0
  27. package/dist/image/png-encode.cjs +64 -0
  28. package/dist/image/png-encode.js +63 -0
  29. package/dist/image/png-filter-5L1Q0PDF.d.cts +6 -0
  30. package/dist/image/png-filter-5L1Q0PDF.d.ts +6 -0
  31. package/dist/image/png-filter.cjs +95 -0
  32. package/dist/image/png-filter.js +93 -0
  33. package/dist/index-VKR9nd94.d.ts +9 -0
  34. package/dist/index-v-4chATX.d.cts +9 -0
  35. package/dist/index.cjs +23 -0
  36. package/dist/index.js +9 -0
  37. package/package.json +5 -1
  38. package/.github/workflows/ci.yml +0 -254
  39. package/.github/workflows/dependabot-auto-merge.yml +0 -64
  40. package/.github/workflows/sibling-dependency-update.yml +0 -174
  41. package/.husky/commit-msg +0 -1
  42. package/.husky/pre-commit +0 -2
  43. package/.husky/pre-push +0 -2
  44. package/CHANGELOG.md +0 -12
  45. package/commitlint.config.ts +0 -8
  46. package/eslint.config.ts +0 -19
  47. package/release.config.ts +0 -65
  48. package/src/bytes/crc32.ts +0 -24
  49. package/src/bytes/flate.ts +0 -69
  50. package/src/bytes/reader.ts +0 -73
  51. package/src/bytes/writer.ts +0 -45
  52. package/src/image/jpeg-info.ts +0 -90
  53. package/src/image/png-decode.ts +0 -263
  54. package/src/image/png-encode.ts +0 -74
  55. package/src/image/png-filter.ts +0 -142
  56. package/src/index.test.ts +0 -33
  57. package/src/index.ts +0 -9
  58. package/tsconfig.json +0 -18
  59. package/tsdown.config.ts +0 -10
@@ -1,263 +0,0 @@
1
- import { crc32 } from '../bytes/crc32';
2
- import { inflateTolerant } from '../bytes/flate';
3
- import { concatBytes } from '../bytes/writer';
4
- import { unfilterScanlines } from './png-filter';
5
-
6
- // Normalising every PNG colour type down to 8-bit gray-or-RGB plus a separate alpha plane is deliberate: it is exactly the shape a PDF Image XObject wants (/DeviceGray or /DeviceRGB, /BitsPerComponent 8, alpha as a separate /SMask /DeviceGray XObject), so the PDF writer does zero rearranging of whatever this decoder produces.
7
- export interface RawImage {
8
- readonly width: number;
9
- readonly height: number;
10
- readonly channels: 1 | 3;
11
- readonly data: Uint8Array<ArrayBuffer>;
12
- readonly alpha?: Uint8Array<ArrayBuffer>;
13
- }
14
-
15
- export interface PngDecodeOptions {
16
- readonly onWarning?: (message: string) => void;
17
- }
18
-
19
- const PNG_SIGNATURE: readonly number[] = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
20
-
21
- interface PngChunk {
22
- readonly type: string;
23
- readonly data: Uint8Array<ArrayBuffer>;
24
- }
25
-
26
- function requireDataView(bytes: Uint8Array<ArrayBuffer>): DataView {
27
- return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
28
- }
29
-
30
- function readChunks(bytes: Uint8Array<ArrayBuffer>, onWarning: ((m: string) => void) | undefined): PngChunk[] {
31
- const chunks: PngChunk[] = [];
32
- const view = requireDataView(bytes);
33
- let offset = PNG_SIGNATURE.length;
34
- while (offset + 8 <= bytes.length) {
35
- const length = view.getUint32(offset);
36
- const typeBytes = bytes.subarray(offset + 4, offset + 8);
37
- const type = new TextDecoder('latin1').decode(typeBytes);
38
- const dataStart = offset + 8;
39
- const dataEnd = dataStart + length;
40
- if (dataEnd + 4 > bytes.length) {
41
- throw new Error(`PNG chunk '${type}' declares a length that runs past the end of the file`);
42
- }
43
- const data = bytes.subarray(dataStart, dataEnd);
44
- if (onWarning !== undefined) {
45
- const storedCrc = view.getUint32(dataEnd);
46
- const computedCrc = crc32(concatBytes([typeBytes, data]));
47
- if (storedCrc !== computedCrc) {
48
- onWarning(`PNG chunk '${type}' failed its CRC32 check`);
49
- }
50
- }
51
- chunks.push({ type, data });
52
- offset = dataEnd + 4;
53
- if (type === 'IEND') {
54
- break;
55
- }
56
- }
57
- return chunks;
58
- }
59
-
60
- interface Ihdr {
61
- readonly width: number;
62
- readonly height: number;
63
- readonly bitDepth: number;
64
- readonly colorType: number;
65
- readonly interlace: number;
66
- }
67
-
68
- function parseIhdr(data: Uint8Array<ArrayBuffer>): Ihdr {
69
- const view = requireDataView(data);
70
- return {
71
- width: view.getUint32(0),
72
- height: view.getUint32(4),
73
- bitDepth: data[8]!,
74
- colorType: data[9]!,
75
- interlace: data[12]!,
76
- };
77
- }
78
-
79
- function channelsForColorType(colorType: number): number {
80
- if (colorType === 0) {
81
- return 1; // grayscale
82
- }
83
- if (colorType === 2) {
84
- return 3; // truecolor
85
- }
86
- if (colorType === 3) {
87
- return 1; // palette index
88
- }
89
- if (colorType === 4) {
90
- return 2; // grayscale + alpha
91
- }
92
- if (colorType === 6) {
93
- return 4; // truecolor + alpha
94
- }
95
- throw new Error(`unsupported PNG colour type: ${colorType}`);
96
- }
97
-
98
- // PNG's own "bpp" for filtering purposes: bytes per complete pixel, rounded up, minimum 1.
99
- function filterBpp(bitDepth: number, channels: number): number {
100
- return Math.max(1, Math.ceil((bitDepth * channels) / 8));
101
- }
102
-
103
- // Unpacks one already-unfiltered scanline into one number per sample (raw, unscaled -- 0..2^bitDepth-1 for bit depths under 16, or the 16-bit value's high byte for bitDepth 16, per this decoder's documented 16-bit handling: reduce every depth down to an 8-bit-equivalent raw sample here, and scale to a full 0..255 display range later only for grayscale, where sub-8-bit depths need it).
104
- function unpackRow(rowBytes: Uint8Array<ArrayBuffer>, width: number, channels: number, bitDepth: number): number[] {
105
- const sampleCount = width * channels;
106
- const samples: number[] = new Array<number>(sampleCount);
107
- if (bitDepth === 8) {
108
- for (let i = 0; i < sampleCount; i++) {
109
- samples[i] = rowBytes[i]!;
110
- }
111
- } else if (bitDepth === 16) {
112
- for (let i = 0; i < sampleCount; i++) {
113
- samples[i] = rowBytes[i * 2]!; // high byte only
114
- }
115
- } else {
116
- const mask = (1 << bitDepth) - 1;
117
- for (let i = 0; i < sampleCount; i++) {
118
- const bitOffset = i * bitDepth;
119
- const byteIndex = bitOffset >> 3;
120
- const shift = 8 - bitDepth - (bitOffset & 7);
121
- samples[i] = (rowBytes[byteIndex]! >> shift) & mask;
122
- }
123
- }
124
- return samples;
125
- }
126
-
127
- function readTrnsGrayValue(trns: Uint8Array<ArrayBuffer>): number {
128
- return requireDataView(trns).getUint16(0);
129
- }
130
-
131
- function readTrnsRgbKey(trns: Uint8Array<ArrayBuffer>): readonly [number, number, number] {
132
- const view = requireDataView(trns);
133
- return [view.getUint16(0), view.getUint16(2), view.getUint16(4)];
134
- }
135
-
136
- function scaleToByte(sample: number, bitDepth: number): number {
137
- if (bitDepth === 16) {
138
- return sample; // already the high byte, i.e. already 0..255
139
- }
140
- const maxSample = (1 << bitDepth) - 1;
141
- return Math.round((sample * 255) / maxSample);
142
- }
143
-
144
- // Decodes PNG file bytes into raw, normalised pixel data. Supports colour types 0/2/3/4/6 (gray, truecolor, indexed+PLTE, gray+alpha, truecolor+alpha) at bit depths 1/2/4/8/16 as applicable, plus tRNS transparency for all three non-alpha colour types. Adam7-interlaced sources are rejected explicitly (diagnostic-worthy but essentially never produced by Office/mainstream tooling) rather than silently decoded wrong.
145
- export function decodePng(bytes: Uint8Array<ArrayBuffer>, options: PngDecodeOptions = {}): RawImage {
146
- for (let i = 0; i < PNG_SIGNATURE.length; i++) {
147
- if (bytes[i] !== PNG_SIGNATURE[i]) {
148
- throw new Error('not a valid PNG file: bad signature');
149
- }
150
- }
151
-
152
- const chunks = readChunks(bytes, options.onWarning);
153
- const ihdrChunk = chunks[0];
154
- if (ihdrChunk?.type !== 'IHDR') {
155
- throw new Error('PNG file does not begin with an IHDR chunk');
156
- }
157
- const ihdr = parseIhdr(ihdrChunk.data);
158
- if (ihdr.interlace !== 0) {
159
- throw new Error('Adam7-interlaced PNG images are not supported');
160
- }
161
-
162
- const channels = channelsForColorType(ihdr.colorType);
163
- const bpp = filterBpp(ihdr.bitDepth, channels);
164
- const bytesPerRow = Math.ceil((ihdr.width * channels * ihdr.bitDepth) / 8);
165
-
166
- const idatChunks = chunks.filter((c) => c.type === 'IDAT').map((c) => c.data);
167
- if (idatChunks.length === 0) {
168
- throw new Error('PNG file has no IDAT chunks');
169
- }
170
- // Every IDAT chunk must be concatenated before inflating -- multi-IDAT files are routine (Office emits them), and inflating only the first chunk is the single most common PNG-decoder bug.
171
- const compressed = concatBytes(idatChunks);
172
- const { bytes: inflated, recovered } = inflateTolerant(compressed);
173
- if (recovered && options.onWarning !== undefined) {
174
- options.onWarning('PNG IDAT stream required tolerant recovery (truncated or malformed)');
175
- }
176
- const unfiltered = unfilterScanlines(inflated, ihdr.height, bytesPerRow, bpp);
177
-
178
- const palette = ihdr.colorType === 3 ? chunks.find((c) => c.type === 'PLTE')?.data : undefined;
179
- if (ihdr.colorType === 3 && palette === undefined) {
180
- throw new Error('indexed-colour PNG has no PLTE chunk');
181
- }
182
- const trns = chunks.find((c) => c.type === 'tRNS')?.data;
183
-
184
- return buildRawImage(ihdr, channels, bytesPerRow, unfiltered, palette, trns);
185
- }
186
-
187
- function buildRawImage(
188
- ihdr: Ihdr,
189
- channels: number,
190
- bytesPerRow: number,
191
- unfiltered: Uint8Array<ArrayBuffer>,
192
- palette: Uint8Array<ArrayBuffer> | undefined,
193
- trns: Uint8Array<ArrayBuffer> | undefined,
194
- ): RawImage {
195
- const { width, height, bitDepth, colorType } = ihdr;
196
- const outChannels: 1 | 3 = colorType === 0 || colorType === 4 ? 1 : 3;
197
- const data = new Uint8Array(width * height * outChannels);
198
- const hasAlpha = colorType === 4 || colorType === 6 || trns !== undefined;
199
- const alpha = hasAlpha ? new Uint8Array(width * height).fill(255) : undefined;
200
-
201
- const trnsGray = colorType === 0 && trns !== undefined ? readTrnsGrayValue(trns) : undefined;
202
- const trnsRgb = colorType === 2 && trns !== undefined ? readTrnsRgbKey(trns) : undefined;
203
-
204
- for (let y = 0; y < height; y++) {
205
- const rowStart = y * bytesPerRow;
206
- const rowBytes = unfiltered.subarray(rowStart, rowStart + bytesPerRow);
207
- const samples = unpackRow(rowBytes, width, channels, bitDepth);
208
- for (let x = 0; x < width; x++) {
209
- const pixelBase = x * channels;
210
- const outBase = (y * width + x) * outChannels;
211
- const alphaIndex = y * width + x;
212
-
213
- if (colorType === 0) {
214
- const g = samples[pixelBase]!;
215
- data[outBase] = scaleToByte(g, bitDepth);
216
- if (alpha !== undefined && trnsGray !== undefined) {
217
- alpha[alphaIndex] = g === trnsGray ? 0 : 255;
218
- }
219
- } else if (colorType === 2) {
220
- const r = samples[pixelBase]!;
221
- const g = samples[pixelBase + 1]!;
222
- const b = samples[pixelBase + 2]!;
223
- data[outBase] = r;
224
- data[outBase + 1] = g;
225
- data[outBase + 2] = b;
226
- if (alpha !== undefined && trnsRgb !== undefined) {
227
- const [kr, kg, kb] = trnsRgb;
228
- alpha[alphaIndex] = r === kr && g === kg && b === kb ? 0 : 255;
229
- }
230
- } else if (colorType === 3) {
231
- const index = samples[pixelBase]!;
232
- if (palette === undefined) {
233
- throw new Error('indexed-colour PNG has no PLTE chunk');
234
- }
235
- data[outBase] = palette[index * 3]!;
236
- data[outBase + 1] = palette[index * 3 + 1]!;
237
- data[outBase + 2] = palette[index * 3 + 2]!;
238
- if (alpha !== undefined && trns !== undefined) {
239
- alpha[alphaIndex] = index < trns.length ? trns[index]! : 255;
240
- }
241
- } else if (colorType === 4) {
242
- const g = samples[pixelBase]!;
243
- const a = samples[pixelBase + 1]!;
244
- data[outBase] = scaleToByte(g, bitDepth);
245
- if (alpha !== undefined) {
246
- alpha[alphaIndex] = scaleToByte(a, bitDepth);
247
- }
248
- } else {
249
- // colorType === 6: truecolor + alpha
250
- data[outBase] = samples[pixelBase]!;
251
- data[outBase + 1] = samples[pixelBase + 1]!;
252
- data[outBase + 2] = samples[pixelBase + 2]!;
253
- if (alpha !== undefined) {
254
- alpha[alphaIndex] = samples[pixelBase + 3]!;
255
- }
256
- }
257
- }
258
- }
259
-
260
- return alpha === undefined
261
- ? { width, height, channels: outChannels, data }
262
- : { width, height, channels: outChannels, data, alpha };
263
- }
@@ -1,74 +0,0 @@
1
- import { crc32 } from '../bytes/crc32';
2
- import { deflate } from '../bytes/flate';
3
- import { ByteWriter, concatBytes } from '../bytes/writer';
4
- import type { RawImage } from './png-decode';
5
- import { filterScanlines } from './png-filter';
6
-
7
- const PNG_SIGNATURE = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
8
-
9
- export interface PngEncodeOptions {
10
- // 'adaptive' (the default) picks, per row, whichever of the five PNG filters minimises the sum of the filtered bytes' absolute values -- the PNG spec's own recommended heuristic. 'none' always emits filter type 0, useful for deterministic, human-auditable test output.
11
- readonly filter?: 'none' | 'adaptive';
12
- }
13
-
14
- function u32be(value: number): Uint8Array<ArrayBuffer> {
15
- const bytes = new Uint8Array(4);
16
- new DataView(bytes.buffer).setUint32(0, value);
17
- return bytes;
18
- }
19
-
20
- function writeChunk(writer: ByteWriter, type: string, data: Uint8Array<ArrayBuffer>): void {
21
- const typeBytes = new TextEncoder().encode(type);
22
- writer.writeBytes(u32be(data.length));
23
- writer.writeBytes(typeBytes);
24
- writer.writeBytes(data);
25
- writer.writeBytes(u32be(crc32(concatBytes([typeBytes, data]))));
26
- }
27
-
28
- // IHDR colour type: 0 gray, 2 truecolor(RGB), 4 gray+alpha, 6 truecolor+alpha(RGBA). RawImage's channels/alpha combination maps onto these four (never 3, palette -- this encoder never emits an indexed-colour image, since RawImage carries no palette of its own).
29
- function colorTypeFor(image: RawImage): number {
30
- if (image.channels === 1) {
31
- return image.alpha === undefined ? 0 : 4;
32
- }
33
- return image.alpha === undefined ? 2 : 6;
34
- }
35
-
36
- // Encodes normalised raw pixel data (8 bits per channel, optionally with a separate alpha plane) into PNG file bytes -- the exact inverse of decodePng's RawImage shape.
37
- export function encodePng(image: RawImage, options: PngEncodeOptions = {}): Uint8Array<ArrayBuffer> {
38
- const { width, height, channels, data, alpha } = image;
39
- const outChannels = alpha === undefined ? channels : channels + 1;
40
- const bytesPerRow = width * outChannels;
41
- const pixelCount = width * height;
42
-
43
- const interleaved = new Uint8Array(pixelCount * outChannels);
44
- for (let i = 0; i < pixelCount; i++) {
45
- const srcBase = i * channels;
46
- const dstBase = i * outChannels;
47
- for (let c = 0; c < channels; c++) {
48
- interleaved[dstBase + c] = data[srcBase + c]!;
49
- }
50
- if (alpha !== undefined) {
51
- interleaved[dstBase + channels] = alpha[i]!;
52
- }
53
- }
54
-
55
- const filtered = filterScanlines(interleaved, height, bytesPerRow, outChannels, options.filter ?? 'adaptive');
56
- const compressed = deflate(filtered);
57
-
58
- const ihdr = new Uint8Array(13);
59
- const ihdrView = new DataView(ihdr.buffer);
60
- ihdrView.setUint32(0, width);
61
- ihdrView.setUint32(4, height);
62
- ihdr[8] = 8; // bit depth: always 8, since RawImage is always 8 bits per channel
63
- ihdr[9] = colorTypeFor(image);
64
- ihdr[10] = 0; // compression method: always 0 (deflate)
65
- ihdr[11] = 0; // filter method: always 0 (the five-filter adaptive scheme)
66
- ihdr[12] = 0; // interlace method: 0 (no interlacing)
67
-
68
- const writer = new ByteWriter();
69
- writer.writeBytes(PNG_SIGNATURE);
70
- writeChunk(writer, 'IHDR', ihdr);
71
- writeChunk(writer, 'IDAT', compressed);
72
- writeChunk(writer, 'IEND', new Uint8Array(0));
73
- return writer.toBytes();
74
- }
@@ -1,142 +0,0 @@
1
- // The five PNG scanline (un)filters (PNG spec section 9.2), shared by the PDF cross-reference stream predictor path (src/pdf/predictors.ts): xref streams are almost always /Predictor 12, which is exactly PNG's "Up" filter applied to fixed-width rows, so this module sits on the critical path for reading modern PDFs, not just for PNG images.
2
- export type PngFilterType = 0 | 1 | 2 | 3 | 4; // None, Sub, Up, Average, Paeth
3
-
4
- function paethPredictor(a: number, b: number, c: number): number {
5
- const p = a + b - c;
6
- const pa = Math.abs(p - a);
7
- const pb = Math.abs(p - b);
8
- const pc = Math.abs(p - c);
9
- if (pa <= pb && pa <= pc) {
10
- return a;
11
- }
12
- if (pb <= pc) {
13
- return b;
14
- }
15
- return c;
16
- }
17
-
18
- // The value a filter type predicts from the left (a), above (b), and above-left (c) samples -- added back in during unfiltering, or subtracted out during filtering. Returning a value from a pure function (rather than assigning inside a switch) sidesteps having to prove a switch over a literal union is exhaustive to a variable declared without an initialiser.
19
- function isPngFilterType(value: number): value is PngFilterType {
20
- return value === 0 || value === 1 || value === 2 || value === 3 || value === 4;
21
- }
22
-
23
- function predictorValue(filterType: PngFilterType, a: number, b: number, c: number): number {
24
- if (filterType === 1) {
25
- return a;
26
- }
27
- if (filterType === 2) {
28
- return b;
29
- }
30
- if (filterType === 3) {
31
- return Math.floor((a + b) / 2);
32
- }
33
- if (filterType === 4) {
34
- return paethPredictor(a, b, c);
35
- }
36
- return 0; // None
37
- }
38
-
39
- // Reverses PNG's per-scanline filtering. `data` is the inflated IDAT payload: height rows, each prefixed by one filter-type byte followed by `bytesPerRow` filtered sample bytes. Returns the raw (unfiltered) pixel bytes, height * bytesPerRow long, with the filter-type bytes stripped.
40
- export function unfilterScanlines(
41
- data: Uint8Array<ArrayBuffer>,
42
- height: number,
43
- bytesPerRow: number,
44
- bpp: number,
45
- ): Uint8Array<ArrayBuffer> {
46
- const stride = bytesPerRow + 1;
47
- if (data.length < height * stride) {
48
- throw new Error(
49
- `PNG scanline data too short: expected at least ${height * stride} bytes, got ${data.length}`,
50
- );
51
- }
52
- const out = new Uint8Array(height * bytesPerRow);
53
- for (let y = 0; y < height; y++) {
54
- const filterByte = data[y * stride];
55
- if (filterByte === undefined || !isPngFilterType(filterByte)) {
56
- throw new Error(`unknown PNG filter type: ${String(filterByte)}`);
57
- }
58
- const rowStart = y * stride + 1;
59
- const outRowStart = y * bytesPerRow;
60
- const prevOutRowStart = y > 0 ? outRowStart - bytesPerRow : undefined;
61
- for (let x = 0; x < bytesPerRow; x++) {
62
- const raw = data[rowStart + x]!;
63
- const a = x >= bpp ? out[outRowStart + x - bpp]! : 0;
64
- const b = prevOutRowStart === undefined ? 0 : out[prevOutRowStart + x]!;
65
- const c = x >= bpp && prevOutRowStart !== undefined ? out[prevOutRowStart + x - bpp]! : 0;
66
- out[outRowStart + x] = (raw + predictorValue(filterByte, a, b, c)) & 0xff;
67
- }
68
- }
69
- return out;
70
- }
71
-
72
- function sumOfAbsSigned(bytes: Uint8Array<ArrayBuffer>): number {
73
- let sum = 0;
74
- for (const byte of bytes) {
75
- sum += byte < 128 ? byte : 256 - byte;
76
- }
77
- return sum;
78
- }
79
-
80
- function filterRowInto(
81
- raw: Uint8Array<ArrayBuffer>,
82
- rowStart: number,
83
- prevRowStart: number | undefined,
84
- bytesPerRow: number,
85
- bpp: number,
86
- filterType: PngFilterType,
87
- out: Uint8Array<ArrayBuffer>,
88
- outOffset: number,
89
- ): void {
90
- for (let x = 0; x < bytesPerRow; x++) {
91
- const rawByte = raw[rowStart + x]!;
92
- const a = x >= bpp ? raw[rowStart + x - bpp]! : 0;
93
- const b = prevRowStart === undefined ? 0 : raw[prevRowStart + x]!;
94
- const c = x >= bpp && prevRowStart !== undefined ? raw[prevRowStart + x - bpp]! : 0;
95
- out[outOffset + x] = (rawByte - predictorValue(filterType, a, b, c)) & 0xff;
96
- }
97
- }
98
-
99
- const ALL_FILTER_TYPES: readonly PngFilterType[] = [0, 1, 2, 3, 4];
100
-
101
- // Filters raw (unfiltered) pixel bytes into PNG's per-scanline IDAT payload shape. `strategy: 'none'` always emits filter type 0 (useful for deterministic, human-auditable test output); `'adaptive'` (the default) picks, per row, whichever of the five filters minimises the sum of the filtered bytes' absolute values interpreted as signed -- the heuristic the PNG spec itself recommends.
102
- export function filterScanlines(
103
- raw: Uint8Array<ArrayBuffer>,
104
- height: number,
105
- bytesPerRow: number,
106
- bpp: number,
107
- strategy: 'none' | 'adaptive' = 'adaptive',
108
- ): Uint8Array<ArrayBuffer> {
109
- const stride = bytesPerRow + 1;
110
- const out = new Uint8Array(height * stride);
111
- const candidate = new Uint8Array(bytesPerRow);
112
-
113
- for (let y = 0; y < height; y++) {
114
- const rowStart = y * bytesPerRow;
115
- const prevRowStart = y > 0 ? rowStart - bytesPerRow : undefined;
116
- const outRowStart = y * stride;
117
-
118
- if (strategy === 'none') {
119
- out[outRowStart] = 0;
120
- filterRowInto(raw, rowStart, prevRowStart, bytesPerRow, bpp, 0, out, outRowStart + 1);
121
- continue;
122
- }
123
-
124
- let bestType: PngFilterType = 0;
125
- let bestSum = Number.POSITIVE_INFINITY;
126
- let best: Uint8Array<ArrayBuffer> | undefined;
127
- for (const filterType of ALL_FILTER_TYPES) {
128
- filterRowInto(raw, rowStart, prevRowStart, bytesPerRow, bpp, filterType, candidate, 0);
129
- const sum = sumOfAbsSigned(candidate);
130
- if (sum < bestSum) {
131
- bestSum = sum;
132
- bestType = filterType;
133
- best = candidate.slice();
134
- }
135
- }
136
- out[outRowStart] = bestType;
137
- if (best !== undefined) {
138
- out.set(best, outRowStart + 1);
139
- }
140
- }
141
- return out;
142
- }
package/src/index.test.ts DELETED
@@ -1,33 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { crc32, encodePng, decodePng, readJpegInfo, ByteWriter, concatBytes } from './index';
3
-
4
- describe('byte-codec smoke', () => {
5
- it('crc32 produces a consistent hash for known input', () => {
6
- expect(crc32(new Uint8Array([1, 2, 3, 4]))).toBe(crc32(new Uint8Array([1, 2, 3, 4])));
7
- });
8
-
9
- it('encodePng then decodePng round-trips a small image', () => {
10
- const pixels = new Uint8Array([255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0]);
11
- const png = encodePng({ width: 2, height: 2, channels: 3, data: pixels });
12
- const decoded = decodePng(png);
13
- expect(decoded.width).toBe(2);
14
- expect(decoded.height).toBe(2);
15
- });
16
-
17
- it('ByteWriter accumulates and produces concatenated output', () => {
18
- const w = new ByteWriter();
19
- w.writeByte(1);
20
- w.writeByte(2);
21
- w.writeByte(3);
22
- expect(Array.from(w.toBytes())).toEqual([1, 2, 3]);
23
- });
24
-
25
- it('concatBytes joins arrays', () => {
26
- const result = concatBytes([new Uint8Array([1, 2]), new Uint8Array([3, 4])]);
27
- expect(Array.from(result)).toEqual([1, 2, 3, 4]);
28
- });
29
-
30
- it('readJpegInfo throws for non-JPEG input', () => {
31
- expect(() => readJpegInfo(new Uint8Array([0, 0, 0]))).toThrow(/JPEG/);
32
- });
33
- });
package/src/index.ts DELETED
@@ -1,9 +0,0 @@
1
- // The shared byte/image utility package for the documents.js family: generic byte-level primitives (ByteWriter, ByteReader, CRC-32, deflate/inflate) and PNG/JPEG image encoding/decoding with zero PDF knowledge. Extracted from pdf-codec (where they lived as a directory-isolated subgraph with no PDF imports) so both pdf-codec and documents.js consume them from a neutral home rather than one fetching byte utilities from a backend.
2
- export * from './bytes/writer';
3
- export * from './bytes/reader';
4
- export * from './bytes/crc32';
5
- export * from './bytes/flate';
6
- export * from './image/png-encode';
7
- export * from './image/png-decode';
8
- export * from './image/png-filter';
9
- export * from './image/jpeg-info';
package/tsconfig.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "strict": true,
4
- "noUncheckedIndexedAccess": true,
5
- "target": "ES2024",
6
- "module": "ESNext",
7
- "moduleResolution": "bundler",
8
- "lib": ["ES2024"],
9
- "types": ["node"],
10
- "noEmit": true,
11
- "skipLibCheck": true,
12
- "isolatedModules": true,
13
- "verbatimModuleSyntax": true,
14
- "forceConsistentCasingInFileNames": true,
15
- "esModuleInterop": true
16
- },
17
- "include": ["src"]
18
- }
package/tsdown.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from 'tsdown';
2
-
3
- export default defineConfig({
4
- entry: ['src/**/*.ts', '!src/**/*.test.ts'],
5
- root: 'src',
6
- format: ['esm', 'cjs'],
7
- dts: true,
8
- platform: 'neutral',
9
- clean: true,
10
- });