byte-codec 0.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.
- package/README.md +43 -0
- package/dist/bytes/crc32-BVCBBYa3.d.cts +4 -0
- package/dist/bytes/crc32-BVCBBYa3.d.ts +4 -0
- package/dist/bytes/crc32.cjs +21 -0
- package/dist/bytes/crc32.js +20 -0
- package/dist/bytes/flate-CbSD2OU9.d.cts +12 -0
- package/dist/bytes/flate-CbSD2OU9.d.ts +12 -0
- package/dist/bytes/flate.cjs +53 -0
- package/dist/bytes/flate.js +49 -0
- package/dist/bytes/reader-CDx_NmAQ.d.cts +20 -0
- package/dist/bytes/reader-CDx_NmAQ.d.ts +20 -0
- package/dist/bytes/reader.cjs +60 -0
- package/dist/bytes/reader.js +58 -0
- package/dist/bytes/writer-B9uaqo-B.d.cts +13 -0
- package/dist/bytes/writer-B9uaqo-B.d.ts +13 -0
- package/dist/bytes/writer.cjs +37 -0
- package/dist/bytes/writer.js +35 -0
- package/dist/image/jpeg-info-Dh6D8PRh.d.cts +12 -0
- package/dist/image/jpeg-info-Dh6D8PRh.d.ts +12 -0
- package/dist/image/jpeg-info.cjs +71 -0
- package/dist/image/jpeg-info.js +70 -0
- package/dist/image/png-decode-Dvizumot.d.cts +14 -0
- package/dist/image/png-decode-Dvizumot.d.ts +14 -0
- package/dist/image/png-decode.cjs +181 -0
- package/dist/image/png-decode.js +180 -0
- package/dist/image/png-encode-C5Jj9gBX.d.ts +9 -0
- package/dist/image/png-encode-CpwBOmkg.d.cts +9 -0
- package/dist/image/png-encode.cjs +64 -0
- package/dist/image/png-encode.js +63 -0
- package/dist/image/png-filter-5L1Q0PDF.d.cts +6 -0
- package/dist/image/png-filter-5L1Q0PDF.d.ts +6 -0
- package/dist/image/png-filter.cjs +95 -0
- package/dist/image/png-filter.js +93 -0
- package/dist/index-VKR9nd94.d.ts +9 -0
- package/dist/index-v-4chATX.d.cts +9 -0
- package/dist/index.cjs +23 -0
- package/dist/index.js +9 -0
- package/package.json +67 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/image/png-decode.d.ts
|
|
2
|
+
interface RawImage {
|
|
3
|
+
readonly width: number;
|
|
4
|
+
readonly height: number;
|
|
5
|
+
readonly channels: 1 | 3;
|
|
6
|
+
readonly data: Uint8Array<ArrayBuffer>;
|
|
7
|
+
readonly alpha?: Uint8Array<ArrayBuffer>;
|
|
8
|
+
}
|
|
9
|
+
interface PngDecodeOptions {
|
|
10
|
+
readonly onWarning?: (message: string) => void;
|
|
11
|
+
}
|
|
12
|
+
declare function decodePng(bytes: Uint8Array<ArrayBuffer>, options?: PngDecodeOptions): RawImage;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { PngDecodeOptions, RawImage, decodePng };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_bytes_writer = require("../bytes/writer.cjs");
|
|
3
|
+
const require_bytes_crc32 = require("../bytes/crc32.cjs");
|
|
4
|
+
const require_bytes_flate = require("../bytes/flate.cjs");
|
|
5
|
+
const require_image_png_filter = require("./png-filter.cjs");
|
|
6
|
+
//#region src/image/png-decode.ts
|
|
7
|
+
const PNG_SIGNATURE = [
|
|
8
|
+
137,
|
|
9
|
+
80,
|
|
10
|
+
78,
|
|
11
|
+
71,
|
|
12
|
+
13,
|
|
13
|
+
10,
|
|
14
|
+
26,
|
|
15
|
+
10
|
|
16
|
+
];
|
|
17
|
+
function requireDataView(bytes) {
|
|
18
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
19
|
+
}
|
|
20
|
+
function readChunks(bytes, onWarning) {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
const view = requireDataView(bytes);
|
|
23
|
+
let offset = PNG_SIGNATURE.length;
|
|
24
|
+
while (offset + 8 <= bytes.length) {
|
|
25
|
+
const length = view.getUint32(offset);
|
|
26
|
+
const typeBytes = bytes.subarray(offset + 4, offset + 8);
|
|
27
|
+
const type = new TextDecoder("latin1").decode(typeBytes);
|
|
28
|
+
const dataStart = offset + 8;
|
|
29
|
+
const dataEnd = dataStart + length;
|
|
30
|
+
if (dataEnd + 4 > bytes.length) throw new Error(`PNG chunk '${type}' declares a length that runs past the end of the file`);
|
|
31
|
+
const data = bytes.subarray(dataStart, dataEnd);
|
|
32
|
+
if (onWarning !== void 0) {
|
|
33
|
+
if (view.getUint32(dataEnd) !== require_bytes_crc32.crc32(require_bytes_writer.concatBytes([typeBytes, data]))) onWarning(`PNG chunk '${type}' failed its CRC32 check`);
|
|
34
|
+
}
|
|
35
|
+
chunks.push({
|
|
36
|
+
type,
|
|
37
|
+
data
|
|
38
|
+
});
|
|
39
|
+
offset = dataEnd + 4;
|
|
40
|
+
if (type === "IEND") break;
|
|
41
|
+
}
|
|
42
|
+
return chunks;
|
|
43
|
+
}
|
|
44
|
+
function parseIhdr(data) {
|
|
45
|
+
const view = requireDataView(data);
|
|
46
|
+
return {
|
|
47
|
+
width: view.getUint32(0),
|
|
48
|
+
height: view.getUint32(4),
|
|
49
|
+
bitDepth: data[8],
|
|
50
|
+
colorType: data[9],
|
|
51
|
+
interlace: data[12]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function channelsForColorType(colorType) {
|
|
55
|
+
if (colorType === 0) return 1;
|
|
56
|
+
if (colorType === 2) return 3;
|
|
57
|
+
if (colorType === 3) return 1;
|
|
58
|
+
if (colorType === 4) return 2;
|
|
59
|
+
if (colorType === 6) return 4;
|
|
60
|
+
throw new Error(`unsupported PNG colour type: ${colorType}`);
|
|
61
|
+
}
|
|
62
|
+
function filterBpp(bitDepth, channels) {
|
|
63
|
+
return Math.max(1, Math.ceil(bitDepth * channels / 8));
|
|
64
|
+
}
|
|
65
|
+
function unpackRow(rowBytes, width, channels, bitDepth) {
|
|
66
|
+
const sampleCount = width * channels;
|
|
67
|
+
const samples = new Array(sampleCount);
|
|
68
|
+
if (bitDepth === 8) for (let i = 0; i < sampleCount; i++) samples[i] = rowBytes[i];
|
|
69
|
+
else if (bitDepth === 16) for (let i = 0; i < sampleCount; i++) samples[i] = rowBytes[i * 2];
|
|
70
|
+
else {
|
|
71
|
+
const mask = (1 << bitDepth) - 1;
|
|
72
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
73
|
+
const bitOffset = i * bitDepth;
|
|
74
|
+
const byteIndex = bitOffset >> 3;
|
|
75
|
+
const shift = 8 - bitDepth - (bitOffset & 7);
|
|
76
|
+
samples[i] = rowBytes[byteIndex] >> shift & mask;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return samples;
|
|
80
|
+
}
|
|
81
|
+
function readTrnsGrayValue(trns) {
|
|
82
|
+
return requireDataView(trns).getUint16(0);
|
|
83
|
+
}
|
|
84
|
+
function readTrnsRgbKey(trns) {
|
|
85
|
+
const view = requireDataView(trns);
|
|
86
|
+
return [
|
|
87
|
+
view.getUint16(0),
|
|
88
|
+
view.getUint16(2),
|
|
89
|
+
view.getUint16(4)
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
function scaleToByte(sample, bitDepth) {
|
|
93
|
+
if (bitDepth === 16) return sample;
|
|
94
|
+
const maxSample = (1 << bitDepth) - 1;
|
|
95
|
+
return Math.round(sample * 255 / maxSample);
|
|
96
|
+
}
|
|
97
|
+
function decodePng(bytes, options = {}) {
|
|
98
|
+
for (let i = 0; i < PNG_SIGNATURE.length; i++) if (bytes[i] !== PNG_SIGNATURE[i]) throw new Error("not a valid PNG file: bad signature");
|
|
99
|
+
const chunks = readChunks(bytes, options.onWarning);
|
|
100
|
+
const ihdrChunk = chunks[0];
|
|
101
|
+
if (ihdrChunk?.type !== "IHDR") throw new Error("PNG file does not begin with an IHDR chunk");
|
|
102
|
+
const ihdr = parseIhdr(ihdrChunk.data);
|
|
103
|
+
if (ihdr.interlace !== 0) throw new Error("Adam7-interlaced PNG images are not supported");
|
|
104
|
+
const channels = channelsForColorType(ihdr.colorType);
|
|
105
|
+
const bpp = filterBpp(ihdr.bitDepth, channels);
|
|
106
|
+
const bytesPerRow = Math.ceil(ihdr.width * channels * ihdr.bitDepth / 8);
|
|
107
|
+
const idatChunks = chunks.filter((c) => c.type === "IDAT").map((c) => c.data);
|
|
108
|
+
if (idatChunks.length === 0) throw new Error("PNG file has no IDAT chunks");
|
|
109
|
+
const compressed = require_bytes_writer.concatBytes(idatChunks);
|
|
110
|
+
const { bytes: inflated, recovered } = require_bytes_flate.inflateTolerant(compressed);
|
|
111
|
+
if (recovered && options.onWarning !== void 0) options.onWarning("PNG IDAT stream required tolerant recovery (truncated or malformed)");
|
|
112
|
+
const unfiltered = require_image_png_filter.unfilterScanlines(inflated, ihdr.height, bytesPerRow, bpp);
|
|
113
|
+
const palette = ihdr.colorType === 3 ? chunks.find((c) => c.type === "PLTE")?.data : void 0;
|
|
114
|
+
if (ihdr.colorType === 3 && palette === void 0) throw new Error("indexed-colour PNG has no PLTE chunk");
|
|
115
|
+
const trns = chunks.find((c) => c.type === "tRNS")?.data;
|
|
116
|
+
return buildRawImage(ihdr, channels, bytesPerRow, unfiltered, palette, trns);
|
|
117
|
+
}
|
|
118
|
+
function buildRawImage(ihdr, channels, bytesPerRow, unfiltered, palette, trns) {
|
|
119
|
+
const { width, height, bitDepth, colorType } = ihdr;
|
|
120
|
+
const outChannels = colorType === 0 || colorType === 4 ? 1 : 3;
|
|
121
|
+
const data = new Uint8Array(width * height * outChannels);
|
|
122
|
+
const alpha = colorType === 4 || colorType === 6 || trns !== void 0 ? new Uint8Array(width * height).fill(255) : void 0;
|
|
123
|
+
const trnsGray = colorType === 0 && trns !== void 0 ? readTrnsGrayValue(trns) : void 0;
|
|
124
|
+
const trnsRgb = colorType === 2 && trns !== void 0 ? readTrnsRgbKey(trns) : void 0;
|
|
125
|
+
for (let y = 0; y < height; y++) {
|
|
126
|
+
const rowStart = y * bytesPerRow;
|
|
127
|
+
const samples = unpackRow(unfiltered.subarray(rowStart, rowStart + bytesPerRow), width, channels, bitDepth);
|
|
128
|
+
for (let x = 0; x < width; x++) {
|
|
129
|
+
const pixelBase = x * channels;
|
|
130
|
+
const outBase = (y * width + x) * outChannels;
|
|
131
|
+
const alphaIndex = y * width + x;
|
|
132
|
+
if (colorType === 0) {
|
|
133
|
+
const g = samples[pixelBase];
|
|
134
|
+
data[outBase] = scaleToByte(g, bitDepth);
|
|
135
|
+
if (alpha !== void 0 && trnsGray !== void 0) alpha[alphaIndex] = g === trnsGray ? 0 : 255;
|
|
136
|
+
} else if (colorType === 2) {
|
|
137
|
+
const r = samples[pixelBase];
|
|
138
|
+
const g = samples[pixelBase + 1];
|
|
139
|
+
const b = samples[pixelBase + 2];
|
|
140
|
+
data[outBase] = r;
|
|
141
|
+
data[outBase + 1] = g;
|
|
142
|
+
data[outBase + 2] = b;
|
|
143
|
+
if (alpha !== void 0 && trnsRgb !== void 0) {
|
|
144
|
+
const [kr, kg, kb] = trnsRgb;
|
|
145
|
+
alpha[alphaIndex] = r === kr && g === kg && b === kb ? 0 : 255;
|
|
146
|
+
}
|
|
147
|
+
} else if (colorType === 3) {
|
|
148
|
+
const index = samples[pixelBase];
|
|
149
|
+
if (palette === void 0) throw new Error("indexed-colour PNG has no PLTE chunk");
|
|
150
|
+
data[outBase] = palette[index * 3];
|
|
151
|
+
data[outBase + 1] = palette[index * 3 + 1];
|
|
152
|
+
data[outBase + 2] = palette[index * 3 + 2];
|
|
153
|
+
if (alpha !== void 0 && trns !== void 0) alpha[alphaIndex] = index < trns.length ? trns[index] : 255;
|
|
154
|
+
} else if (colorType === 4) {
|
|
155
|
+
const g = samples[pixelBase];
|
|
156
|
+
const a = samples[pixelBase + 1];
|
|
157
|
+
data[outBase] = scaleToByte(g, bitDepth);
|
|
158
|
+
if (alpha !== void 0) alpha[alphaIndex] = scaleToByte(a, bitDepth);
|
|
159
|
+
} else {
|
|
160
|
+
data[outBase] = samples[pixelBase];
|
|
161
|
+
data[outBase + 1] = samples[pixelBase + 1];
|
|
162
|
+
data[outBase + 2] = samples[pixelBase + 2];
|
|
163
|
+
if (alpha !== void 0) alpha[alphaIndex] = samples[pixelBase + 3];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return alpha === void 0 ? {
|
|
168
|
+
width,
|
|
169
|
+
height,
|
|
170
|
+
channels: outChannels,
|
|
171
|
+
data
|
|
172
|
+
} : {
|
|
173
|
+
width,
|
|
174
|
+
height,
|
|
175
|
+
channels: outChannels,
|
|
176
|
+
data,
|
|
177
|
+
alpha
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
exports.decodePng = decodePng;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { concatBytes } from "../bytes/writer.js";
|
|
2
|
+
import { crc32 } from "../bytes/crc32.js";
|
|
3
|
+
import { inflateTolerant } from "../bytes/flate.js";
|
|
4
|
+
import { unfilterScanlines } from "./png-filter.js";
|
|
5
|
+
//#region src/image/png-decode.ts
|
|
6
|
+
const PNG_SIGNATURE = [
|
|
7
|
+
137,
|
|
8
|
+
80,
|
|
9
|
+
78,
|
|
10
|
+
71,
|
|
11
|
+
13,
|
|
12
|
+
10,
|
|
13
|
+
26,
|
|
14
|
+
10
|
|
15
|
+
];
|
|
16
|
+
function requireDataView(bytes) {
|
|
17
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
18
|
+
}
|
|
19
|
+
function readChunks(bytes, onWarning) {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
const view = requireDataView(bytes);
|
|
22
|
+
let offset = PNG_SIGNATURE.length;
|
|
23
|
+
while (offset + 8 <= bytes.length) {
|
|
24
|
+
const length = view.getUint32(offset);
|
|
25
|
+
const typeBytes = bytes.subarray(offset + 4, offset + 8);
|
|
26
|
+
const type = new TextDecoder("latin1").decode(typeBytes);
|
|
27
|
+
const dataStart = offset + 8;
|
|
28
|
+
const dataEnd = dataStart + length;
|
|
29
|
+
if (dataEnd + 4 > bytes.length) throw new Error(`PNG chunk '${type}' declares a length that runs past the end of the file`);
|
|
30
|
+
const data = bytes.subarray(dataStart, dataEnd);
|
|
31
|
+
if (onWarning !== void 0) {
|
|
32
|
+
if (view.getUint32(dataEnd) !== crc32(concatBytes([typeBytes, data]))) onWarning(`PNG chunk '${type}' failed its CRC32 check`);
|
|
33
|
+
}
|
|
34
|
+
chunks.push({
|
|
35
|
+
type,
|
|
36
|
+
data
|
|
37
|
+
});
|
|
38
|
+
offset = dataEnd + 4;
|
|
39
|
+
if (type === "IEND") break;
|
|
40
|
+
}
|
|
41
|
+
return chunks;
|
|
42
|
+
}
|
|
43
|
+
function parseIhdr(data) {
|
|
44
|
+
const view = requireDataView(data);
|
|
45
|
+
return {
|
|
46
|
+
width: view.getUint32(0),
|
|
47
|
+
height: view.getUint32(4),
|
|
48
|
+
bitDepth: data[8],
|
|
49
|
+
colorType: data[9],
|
|
50
|
+
interlace: data[12]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function channelsForColorType(colorType) {
|
|
54
|
+
if (colorType === 0) return 1;
|
|
55
|
+
if (colorType === 2) return 3;
|
|
56
|
+
if (colorType === 3) return 1;
|
|
57
|
+
if (colorType === 4) return 2;
|
|
58
|
+
if (colorType === 6) return 4;
|
|
59
|
+
throw new Error(`unsupported PNG colour type: ${colorType}`);
|
|
60
|
+
}
|
|
61
|
+
function filterBpp(bitDepth, channels) {
|
|
62
|
+
return Math.max(1, Math.ceil(bitDepth * channels / 8));
|
|
63
|
+
}
|
|
64
|
+
function unpackRow(rowBytes, width, channels, bitDepth) {
|
|
65
|
+
const sampleCount = width * channels;
|
|
66
|
+
const samples = new Array(sampleCount);
|
|
67
|
+
if (bitDepth === 8) for (let i = 0; i < sampleCount; i++) samples[i] = rowBytes[i];
|
|
68
|
+
else if (bitDepth === 16) for (let i = 0; i < sampleCount; i++) samples[i] = rowBytes[i * 2];
|
|
69
|
+
else {
|
|
70
|
+
const mask = (1 << bitDepth) - 1;
|
|
71
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
72
|
+
const bitOffset = i * bitDepth;
|
|
73
|
+
const byteIndex = bitOffset >> 3;
|
|
74
|
+
const shift = 8 - bitDepth - (bitOffset & 7);
|
|
75
|
+
samples[i] = rowBytes[byteIndex] >> shift & mask;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return samples;
|
|
79
|
+
}
|
|
80
|
+
function readTrnsGrayValue(trns) {
|
|
81
|
+
return requireDataView(trns).getUint16(0);
|
|
82
|
+
}
|
|
83
|
+
function readTrnsRgbKey(trns) {
|
|
84
|
+
const view = requireDataView(trns);
|
|
85
|
+
return [
|
|
86
|
+
view.getUint16(0),
|
|
87
|
+
view.getUint16(2),
|
|
88
|
+
view.getUint16(4)
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
function scaleToByte(sample, bitDepth) {
|
|
92
|
+
if (bitDepth === 16) return sample;
|
|
93
|
+
const maxSample = (1 << bitDepth) - 1;
|
|
94
|
+
return Math.round(sample * 255 / maxSample);
|
|
95
|
+
}
|
|
96
|
+
function decodePng(bytes, options = {}) {
|
|
97
|
+
for (let i = 0; i < PNG_SIGNATURE.length; i++) if (bytes[i] !== PNG_SIGNATURE[i]) throw new Error("not a valid PNG file: bad signature");
|
|
98
|
+
const chunks = readChunks(bytes, options.onWarning);
|
|
99
|
+
const ihdrChunk = chunks[0];
|
|
100
|
+
if (ihdrChunk?.type !== "IHDR") throw new Error("PNG file does not begin with an IHDR chunk");
|
|
101
|
+
const ihdr = parseIhdr(ihdrChunk.data);
|
|
102
|
+
if (ihdr.interlace !== 0) throw new Error("Adam7-interlaced PNG images are not supported");
|
|
103
|
+
const channels = channelsForColorType(ihdr.colorType);
|
|
104
|
+
const bpp = filterBpp(ihdr.bitDepth, channels);
|
|
105
|
+
const bytesPerRow = Math.ceil(ihdr.width * channels * ihdr.bitDepth / 8);
|
|
106
|
+
const idatChunks = chunks.filter((c) => c.type === "IDAT").map((c) => c.data);
|
|
107
|
+
if (idatChunks.length === 0) throw new Error("PNG file has no IDAT chunks");
|
|
108
|
+
const compressed = concatBytes(idatChunks);
|
|
109
|
+
const { bytes: inflated, recovered } = inflateTolerant(compressed);
|
|
110
|
+
if (recovered && options.onWarning !== void 0) options.onWarning("PNG IDAT stream required tolerant recovery (truncated or malformed)");
|
|
111
|
+
const unfiltered = unfilterScanlines(inflated, ihdr.height, bytesPerRow, bpp);
|
|
112
|
+
const palette = ihdr.colorType === 3 ? chunks.find((c) => c.type === "PLTE")?.data : void 0;
|
|
113
|
+
if (ihdr.colorType === 3 && palette === void 0) throw new Error("indexed-colour PNG has no PLTE chunk");
|
|
114
|
+
const trns = chunks.find((c) => c.type === "tRNS")?.data;
|
|
115
|
+
return buildRawImage(ihdr, channels, bytesPerRow, unfiltered, palette, trns);
|
|
116
|
+
}
|
|
117
|
+
function buildRawImage(ihdr, channels, bytesPerRow, unfiltered, palette, trns) {
|
|
118
|
+
const { width, height, bitDepth, colorType } = ihdr;
|
|
119
|
+
const outChannels = colorType === 0 || colorType === 4 ? 1 : 3;
|
|
120
|
+
const data = new Uint8Array(width * height * outChannels);
|
|
121
|
+
const alpha = colorType === 4 || colorType === 6 || trns !== void 0 ? new Uint8Array(width * height).fill(255) : void 0;
|
|
122
|
+
const trnsGray = colorType === 0 && trns !== void 0 ? readTrnsGrayValue(trns) : void 0;
|
|
123
|
+
const trnsRgb = colorType === 2 && trns !== void 0 ? readTrnsRgbKey(trns) : void 0;
|
|
124
|
+
for (let y = 0; y < height; y++) {
|
|
125
|
+
const rowStart = y * bytesPerRow;
|
|
126
|
+
const samples = unpackRow(unfiltered.subarray(rowStart, rowStart + bytesPerRow), width, channels, bitDepth);
|
|
127
|
+
for (let x = 0; x < width; x++) {
|
|
128
|
+
const pixelBase = x * channels;
|
|
129
|
+
const outBase = (y * width + x) * outChannels;
|
|
130
|
+
const alphaIndex = y * width + x;
|
|
131
|
+
if (colorType === 0) {
|
|
132
|
+
const g = samples[pixelBase];
|
|
133
|
+
data[outBase] = scaleToByte(g, bitDepth);
|
|
134
|
+
if (alpha !== void 0 && trnsGray !== void 0) alpha[alphaIndex] = g === trnsGray ? 0 : 255;
|
|
135
|
+
} else if (colorType === 2) {
|
|
136
|
+
const r = samples[pixelBase];
|
|
137
|
+
const g = samples[pixelBase + 1];
|
|
138
|
+
const b = samples[pixelBase + 2];
|
|
139
|
+
data[outBase] = r;
|
|
140
|
+
data[outBase + 1] = g;
|
|
141
|
+
data[outBase + 2] = b;
|
|
142
|
+
if (alpha !== void 0 && trnsRgb !== void 0) {
|
|
143
|
+
const [kr, kg, kb] = trnsRgb;
|
|
144
|
+
alpha[alphaIndex] = r === kr && g === kg && b === kb ? 0 : 255;
|
|
145
|
+
}
|
|
146
|
+
} else if (colorType === 3) {
|
|
147
|
+
const index = samples[pixelBase];
|
|
148
|
+
if (palette === void 0) throw new Error("indexed-colour PNG has no PLTE chunk");
|
|
149
|
+
data[outBase] = palette[index * 3];
|
|
150
|
+
data[outBase + 1] = palette[index * 3 + 1];
|
|
151
|
+
data[outBase + 2] = palette[index * 3 + 2];
|
|
152
|
+
if (alpha !== void 0 && trns !== void 0) alpha[alphaIndex] = index < trns.length ? trns[index] : 255;
|
|
153
|
+
} else if (colorType === 4) {
|
|
154
|
+
const g = samples[pixelBase];
|
|
155
|
+
const a = samples[pixelBase + 1];
|
|
156
|
+
data[outBase] = scaleToByte(g, bitDepth);
|
|
157
|
+
if (alpha !== void 0) alpha[alphaIndex] = scaleToByte(a, bitDepth);
|
|
158
|
+
} else {
|
|
159
|
+
data[outBase] = samples[pixelBase];
|
|
160
|
+
data[outBase + 1] = samples[pixelBase + 1];
|
|
161
|
+
data[outBase + 2] = samples[pixelBase + 2];
|
|
162
|
+
if (alpha !== void 0) alpha[alphaIndex] = samples[pixelBase + 3];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return alpha === void 0 ? {
|
|
167
|
+
width,
|
|
168
|
+
height,
|
|
169
|
+
channels: outChannels,
|
|
170
|
+
data
|
|
171
|
+
} : {
|
|
172
|
+
width,
|
|
173
|
+
height,
|
|
174
|
+
channels: outChannels,
|
|
175
|
+
data,
|
|
176
|
+
alpha
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
export { decodePng };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { RawImage } from "./png-decode-Dvizumot.js";
|
|
2
|
+
|
|
3
|
+
//#region src/image/png-encode.d.ts
|
|
4
|
+
interface PngEncodeOptions {
|
|
5
|
+
readonly filter?: 'none' | 'adaptive';
|
|
6
|
+
}
|
|
7
|
+
declare function encodePng(image: RawImage, options?: PngEncodeOptions): Uint8Array<ArrayBuffer>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { PngEncodeOptions, encodePng };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { RawImage } from "./png-decode-Dvizumot.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/image/png-encode.d.ts
|
|
4
|
+
interface PngEncodeOptions {
|
|
5
|
+
readonly filter?: 'none' | 'adaptive';
|
|
6
|
+
}
|
|
7
|
+
declare function encodePng(image: RawImage, options?: PngEncodeOptions): Uint8Array<ArrayBuffer>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { PngEncodeOptions, encodePng };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_bytes_writer = require("../bytes/writer.cjs");
|
|
3
|
+
const require_bytes_crc32 = require("../bytes/crc32.cjs");
|
|
4
|
+
const require_bytes_flate = require("../bytes/flate.cjs");
|
|
5
|
+
const require_image_png_filter = require("./png-filter.cjs");
|
|
6
|
+
//#region src/image/png-encode.ts
|
|
7
|
+
const PNG_SIGNATURE = new Uint8Array([
|
|
8
|
+
137,
|
|
9
|
+
80,
|
|
10
|
+
78,
|
|
11
|
+
71,
|
|
12
|
+
13,
|
|
13
|
+
10,
|
|
14
|
+
26,
|
|
15
|
+
10
|
|
16
|
+
]);
|
|
17
|
+
function u32be(value) {
|
|
18
|
+
const bytes = /* @__PURE__ */ new Uint8Array(4);
|
|
19
|
+
new DataView(bytes.buffer).setUint32(0, value);
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
function writeChunk(writer, type, data) {
|
|
23
|
+
const typeBytes = new TextEncoder().encode(type);
|
|
24
|
+
writer.writeBytes(u32be(data.length));
|
|
25
|
+
writer.writeBytes(typeBytes);
|
|
26
|
+
writer.writeBytes(data);
|
|
27
|
+
writer.writeBytes(u32be(require_bytes_crc32.crc32(require_bytes_writer.concatBytes([typeBytes, data]))));
|
|
28
|
+
}
|
|
29
|
+
function colorTypeFor(image) {
|
|
30
|
+
if (image.channels === 1) return image.alpha === void 0 ? 0 : 4;
|
|
31
|
+
return image.alpha === void 0 ? 2 : 6;
|
|
32
|
+
}
|
|
33
|
+
function encodePng(image, options = {}) {
|
|
34
|
+
const { width, height, channels, data, alpha } = image;
|
|
35
|
+
const outChannels = alpha === void 0 ? channels : channels + 1;
|
|
36
|
+
const bytesPerRow = width * outChannels;
|
|
37
|
+
const pixelCount = width * height;
|
|
38
|
+
const interleaved = new Uint8Array(pixelCount * outChannels);
|
|
39
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
40
|
+
const srcBase = i * channels;
|
|
41
|
+
const dstBase = i * outChannels;
|
|
42
|
+
for (let c = 0; c < channels; c++) interleaved[dstBase + c] = data[srcBase + c];
|
|
43
|
+
if (alpha !== void 0) interleaved[dstBase + channels] = alpha[i];
|
|
44
|
+
}
|
|
45
|
+
const filtered = require_image_png_filter.filterScanlines(interleaved, height, bytesPerRow, outChannels, options.filter ?? "adaptive");
|
|
46
|
+
const compressed = require_bytes_flate.deflate(filtered);
|
|
47
|
+
const ihdr = /* @__PURE__ */ new Uint8Array(13);
|
|
48
|
+
const ihdrView = new DataView(ihdr.buffer);
|
|
49
|
+
ihdrView.setUint32(0, width);
|
|
50
|
+
ihdrView.setUint32(4, height);
|
|
51
|
+
ihdr[8] = 8;
|
|
52
|
+
ihdr[9] = colorTypeFor(image);
|
|
53
|
+
ihdr[10] = 0;
|
|
54
|
+
ihdr[11] = 0;
|
|
55
|
+
ihdr[12] = 0;
|
|
56
|
+
const writer = new require_bytes_writer.ByteWriter();
|
|
57
|
+
writer.writeBytes(PNG_SIGNATURE);
|
|
58
|
+
writeChunk(writer, "IHDR", ihdr);
|
|
59
|
+
writeChunk(writer, "IDAT", compressed);
|
|
60
|
+
writeChunk(writer, "IEND", /* @__PURE__ */ new Uint8Array(0));
|
|
61
|
+
return writer.toBytes();
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
exports.encodePng = encodePng;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ByteWriter, concatBytes } from "../bytes/writer.js";
|
|
2
|
+
import { crc32 } from "../bytes/crc32.js";
|
|
3
|
+
import { deflate } from "../bytes/flate.js";
|
|
4
|
+
import { filterScanlines } from "./png-filter.js";
|
|
5
|
+
//#region src/image/png-encode.ts
|
|
6
|
+
const PNG_SIGNATURE = new Uint8Array([
|
|
7
|
+
137,
|
|
8
|
+
80,
|
|
9
|
+
78,
|
|
10
|
+
71,
|
|
11
|
+
13,
|
|
12
|
+
10,
|
|
13
|
+
26,
|
|
14
|
+
10
|
|
15
|
+
]);
|
|
16
|
+
function u32be(value) {
|
|
17
|
+
const bytes = /* @__PURE__ */ new Uint8Array(4);
|
|
18
|
+
new DataView(bytes.buffer).setUint32(0, value);
|
|
19
|
+
return bytes;
|
|
20
|
+
}
|
|
21
|
+
function writeChunk(writer, type, data) {
|
|
22
|
+
const typeBytes = new TextEncoder().encode(type);
|
|
23
|
+
writer.writeBytes(u32be(data.length));
|
|
24
|
+
writer.writeBytes(typeBytes);
|
|
25
|
+
writer.writeBytes(data);
|
|
26
|
+
writer.writeBytes(u32be(crc32(concatBytes([typeBytes, data]))));
|
|
27
|
+
}
|
|
28
|
+
function colorTypeFor(image) {
|
|
29
|
+
if (image.channels === 1) return image.alpha === void 0 ? 0 : 4;
|
|
30
|
+
return image.alpha === void 0 ? 2 : 6;
|
|
31
|
+
}
|
|
32
|
+
function encodePng(image, options = {}) {
|
|
33
|
+
const { width, height, channels, data, alpha } = image;
|
|
34
|
+
const outChannels = alpha === void 0 ? channels : channels + 1;
|
|
35
|
+
const bytesPerRow = width * outChannels;
|
|
36
|
+
const pixelCount = width * height;
|
|
37
|
+
const interleaved = new Uint8Array(pixelCount * outChannels);
|
|
38
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
39
|
+
const srcBase = i * channels;
|
|
40
|
+
const dstBase = i * outChannels;
|
|
41
|
+
for (let c = 0; c < channels; c++) interleaved[dstBase + c] = data[srcBase + c];
|
|
42
|
+
if (alpha !== void 0) interleaved[dstBase + channels] = alpha[i];
|
|
43
|
+
}
|
|
44
|
+
const filtered = filterScanlines(interleaved, height, bytesPerRow, outChannels, options.filter ?? "adaptive");
|
|
45
|
+
const compressed = deflate(filtered);
|
|
46
|
+
const ihdr = /* @__PURE__ */ new Uint8Array(13);
|
|
47
|
+
const ihdrView = new DataView(ihdr.buffer);
|
|
48
|
+
ihdrView.setUint32(0, width);
|
|
49
|
+
ihdrView.setUint32(4, height);
|
|
50
|
+
ihdr[8] = 8;
|
|
51
|
+
ihdr[9] = colorTypeFor(image);
|
|
52
|
+
ihdr[10] = 0;
|
|
53
|
+
ihdr[11] = 0;
|
|
54
|
+
ihdr[12] = 0;
|
|
55
|
+
const writer = new ByteWriter();
|
|
56
|
+
writer.writeBytes(PNG_SIGNATURE);
|
|
57
|
+
writeChunk(writer, "IHDR", ihdr);
|
|
58
|
+
writeChunk(writer, "IDAT", compressed);
|
|
59
|
+
writeChunk(writer, "IEND", /* @__PURE__ */ new Uint8Array(0));
|
|
60
|
+
return writer.toBytes();
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { encodePng };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/image/png-filter.d.ts
|
|
2
|
+
type PngFilterType = 0 | 1 | 2 | 3 | 4;
|
|
3
|
+
declare function unfilterScanlines(data: Uint8Array<ArrayBuffer>, height: number, bytesPerRow: number, bpp: number): Uint8Array<ArrayBuffer>;
|
|
4
|
+
declare function filterScanlines(raw: Uint8Array<ArrayBuffer>, height: number, bytesPerRow: number, bpp: number, strategy?: 'none' | 'adaptive'): Uint8Array<ArrayBuffer>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { PngFilterType, filterScanlines, unfilterScanlines };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/image/png-filter.d.ts
|
|
2
|
+
type PngFilterType = 0 | 1 | 2 | 3 | 4;
|
|
3
|
+
declare function unfilterScanlines(data: Uint8Array<ArrayBuffer>, height: number, bytesPerRow: number, bpp: number): Uint8Array<ArrayBuffer>;
|
|
4
|
+
declare function filterScanlines(raw: Uint8Array<ArrayBuffer>, height: number, bytesPerRow: number, bpp: number, strategy?: 'none' | 'adaptive'): Uint8Array<ArrayBuffer>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { PngFilterType, filterScanlines, unfilterScanlines };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/image/png-filter.ts
|
|
3
|
+
function paethPredictor(a, b, c) {
|
|
4
|
+
const p = a + b - c;
|
|
5
|
+
const pa = Math.abs(p - a);
|
|
6
|
+
const pb = Math.abs(p - b);
|
|
7
|
+
const pc = Math.abs(p - c);
|
|
8
|
+
if (pa <= pb && pa <= pc) return a;
|
|
9
|
+
if (pb <= pc) return b;
|
|
10
|
+
return c;
|
|
11
|
+
}
|
|
12
|
+
function isPngFilterType(value) {
|
|
13
|
+
return value === 0 || value === 1 || value === 2 || value === 3 || value === 4;
|
|
14
|
+
}
|
|
15
|
+
function predictorValue(filterType, a, b, c) {
|
|
16
|
+
if (filterType === 1) return a;
|
|
17
|
+
if (filterType === 2) return b;
|
|
18
|
+
if (filterType === 3) return Math.floor((a + b) / 2);
|
|
19
|
+
if (filterType === 4) return paethPredictor(a, b, c);
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
function unfilterScanlines(data, height, bytesPerRow, bpp) {
|
|
23
|
+
const stride = bytesPerRow + 1;
|
|
24
|
+
if (data.length < height * stride) throw new Error(`PNG scanline data too short: expected at least ${height * stride} bytes, got ${data.length}`);
|
|
25
|
+
const out = new Uint8Array(height * bytesPerRow);
|
|
26
|
+
for (let y = 0; y < height; y++) {
|
|
27
|
+
const filterByte = data[y * stride];
|
|
28
|
+
if (filterByte === void 0 || !isPngFilterType(filterByte)) throw new Error(`unknown PNG filter type: ${String(filterByte)}`);
|
|
29
|
+
const rowStart = y * stride + 1;
|
|
30
|
+
const outRowStart = y * bytesPerRow;
|
|
31
|
+
const prevOutRowStart = y > 0 ? outRowStart - bytesPerRow : void 0;
|
|
32
|
+
for (let x = 0; x < bytesPerRow; x++) {
|
|
33
|
+
const raw = data[rowStart + x];
|
|
34
|
+
const a = x >= bpp ? out[outRowStart + x - bpp] : 0;
|
|
35
|
+
const b = prevOutRowStart === void 0 ? 0 : out[prevOutRowStart + x];
|
|
36
|
+
const c = x >= bpp && prevOutRowStart !== void 0 ? out[prevOutRowStart + x - bpp] : 0;
|
|
37
|
+
out[outRowStart + x] = raw + predictorValue(filterByte, a, b, c) & 255;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function sumOfAbsSigned(bytes) {
|
|
43
|
+
let sum = 0;
|
|
44
|
+
for (const byte of bytes) sum += byte < 128 ? byte : 256 - byte;
|
|
45
|
+
return sum;
|
|
46
|
+
}
|
|
47
|
+
function filterRowInto(raw, rowStart, prevRowStart, bytesPerRow, bpp, filterType, out, outOffset) {
|
|
48
|
+
for (let x = 0; x < bytesPerRow; x++) {
|
|
49
|
+
const rawByte = raw[rowStart + x];
|
|
50
|
+
const a = x >= bpp ? raw[rowStart + x - bpp] : 0;
|
|
51
|
+
const b = prevRowStart === void 0 ? 0 : raw[prevRowStart + x];
|
|
52
|
+
const c = x >= bpp && prevRowStart !== void 0 ? raw[prevRowStart + x - bpp] : 0;
|
|
53
|
+
out[outOffset + x] = rawByte - predictorValue(filterType, a, b, c) & 255;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const ALL_FILTER_TYPES = [
|
|
57
|
+
0,
|
|
58
|
+
1,
|
|
59
|
+
2,
|
|
60
|
+
3,
|
|
61
|
+
4
|
|
62
|
+
];
|
|
63
|
+
function filterScanlines(raw, height, bytesPerRow, bpp, strategy = "adaptive") {
|
|
64
|
+
const stride = bytesPerRow + 1;
|
|
65
|
+
const out = new Uint8Array(height * stride);
|
|
66
|
+
const candidate = new Uint8Array(bytesPerRow);
|
|
67
|
+
for (let y = 0; y < height; y++) {
|
|
68
|
+
const rowStart = y * bytesPerRow;
|
|
69
|
+
const prevRowStart = y > 0 ? rowStart - bytesPerRow : void 0;
|
|
70
|
+
const outRowStart = y * stride;
|
|
71
|
+
if (strategy === "none") {
|
|
72
|
+
out[outRowStart] = 0;
|
|
73
|
+
filterRowInto(raw, rowStart, prevRowStart, bytesPerRow, bpp, 0, out, outRowStart + 1);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
let bestType = 0;
|
|
77
|
+
let bestSum = Number.POSITIVE_INFINITY;
|
|
78
|
+
let best;
|
|
79
|
+
for (const filterType of ALL_FILTER_TYPES) {
|
|
80
|
+
filterRowInto(raw, rowStart, prevRowStart, bytesPerRow, bpp, filterType, candidate, 0);
|
|
81
|
+
const sum = sumOfAbsSigned(candidate);
|
|
82
|
+
if (sum < bestSum) {
|
|
83
|
+
bestSum = sum;
|
|
84
|
+
bestType = filterType;
|
|
85
|
+
best = candidate.slice();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
out[outRowStart] = bestType;
|
|
89
|
+
if (best !== void 0) out.set(best, outRowStart + 1);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
exports.filterScanlines = filterScanlines;
|
|
95
|
+
exports.unfilterScanlines = unfilterScanlines;
|