imagescript 1.3.1 → 3.0.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/LICENSE +4 -1
- package/README.md +64 -26
- package/THIRD_PARTY_NOTICES.md +74 -0
- package/dist/codecs/crc.d.ts +9 -0
- package/dist/codecs/crc.js +29 -0
- package/dist/codecs/exif.d.ts +11 -0
- package/dist/codecs/exif.js +27 -0
- package/dist/codecs/gif.d.ts +44 -0
- package/dist/codecs/gif.js +401 -0
- package/dist/codecs/jpeg.d.ts +35 -0
- package/dist/codecs/jpeg.js +980 -0
- package/dist/codecs/png.d.ts +44 -0
- package/dist/codecs/png.js +442 -0
- package/dist/codecs/quantize.d.ts +25 -0
- package/dist/codecs/quantize.js +302 -0
- package/dist/codecs/tiff.d.ts +6 -0
- package/dist/codecs/tiff.js +546 -0
- package/dist/codecs/zlib.d.ts +22 -0
- package/dist/codecs/zlib.js +607 -0
- package/dist/frame.d.ts +57 -0
- package/dist/frame.js +65 -0
- package/dist/gif.d.ts +44 -0
- package/dist/gif.js +93 -0
- package/dist/image-type.d.ts +20 -0
- package/dist/image-type.js +49 -0
- package/dist/image.d.ts +521 -0
- package/dist/image.js +1118 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +13 -0
- package/dist/package.json +1 -0
- package/dist/render/font.d.ts +63 -0
- package/dist/render/font.js +857 -0
- package/dist/render/svg.d.ts +11 -0
- package/dist/render/svg.js +1717 -0
- package/dist/render/text.d.ts +61 -0
- package/dist/render/text.js +317 -0
- package/dist/text-layout.d.ts +46 -0
- package/dist/text-layout.js +35 -0
- package/dist/util/blur.d.ts +13 -0
- package/dist/util/blur.js +146 -0
- package/dist/util/mem.d.ts +10 -0
- package/dist/util/mem.js +23 -0
- package/dist/util/resample.d.ts +12 -0
- package/dist/util/resample.js +99 -0
- package/dist/util/utf8.d.ts +5 -0
- package/dist/util/utf8.js +55 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/licenses/Apache-2.0.txt +202 -0
- package/licenses/README.ijg +260 -0
- package/package.json +31 -7
- package/ImageScript.d.ts +0 -954
- package/ImageScript.js +0 -1669
- package/bench/_.mjs +0 -166
- package/bench/all.mjs +0 -4
- package/bench/blur.mjs +0 -59
- package/bench/flip.mjs +0 -63
- package/bench/overlay.mjs +0 -35
- package/bench/package.json +0 -7
- package/bench/readme.md +0 -64
- package/bench/resize.mjs +0 -52
- package/codecs/LICENSE +0 -21
- package/codecs/lib.d.ts +0 -50
- package/codecs/lib.js +0 -6
- package/codecs/lib.mjs +0 -1
- package/codecs/node/bin/arm64-darwin.node +0 -0
- package/codecs/node/bin/arm64-linux.node +0 -0
- package/codecs/node/bin/x64-darwin.node +0 -0
- package/codecs/node/bin/x64-linux.node +0 -0
- package/codecs/node/bin/x64-win32.node +0 -0
- package/codecs/node/index.js +0 -3
- package/codecs/node/index.mjs +0 -6
- package/codecs/package.json +0 -14
- package/codecs/readme.md +0 -38
- package/codecs/wasm/bin/codecs.wasm +0 -0
- package/codecs/wasm/bin/simd.wasm +0 -0
- package/codecs/wasm/bundle/browser.js +0 -1
- package/codecs/wasm/index.js +0 -1
- package/codecs/wasm/index.mjs +0 -1
- package/png/README.md +0 -1
- package/png/node.js +0 -1137
- package/png/src/crc.mjs +0 -49
- package/png/src/mem.mjs +0 -24
- package/png/src/png.mjs +0 -254
- package/png/src/zlib.mjs +0 -618
- package/tsconfig.json +0 -21
- package/utils/mem.js +0 -26
- package/v2/codecs/magic.js +0 -25
- package/v2/framebuffer.d.ts +0 -67
- package/v2/framebuffer.js +0 -2352
- package/v2/framebuffer.mjs +0 -126
- package/v2/framebuffer.rs +0 -1264
- package/v2/index.d.ts +0 -84
- package/v2/index.js +0 -261
- package/v2/ops/blur.mjs +0 -303
- package/v2/ops/color.mjs +0 -277
- package/v2/ops/crop.mjs +0 -64
- package/v2/ops/fill.mjs +0 -33
- package/v2/ops/flip.mjs +0 -33
- package/v2/ops/index.mjs +0 -9
- package/v2/ops/iterator.mjs +0 -34
- package/v2/ops/overlay.mjs +0 -63
- package/v2/ops/resize.mjs +0 -166
- package/v2/ops/rotate.mjs +0 -117
- package/v2/util/mem.mjs +0 -24
- package/wasm/any/font.wasm +0 -0
- package/wasm/any/gif.wasm +0 -0
- package/wasm/any/jpeg.wasm +0 -0
- package/wasm/any/png.wasm +0 -0
- package/wasm/any/svg.wasm +0 -0
- package/wasm/any/tiff.wasm +0 -0
- package/wasm/node/font.js +0 -142
- package/wasm/node/gif.js +0 -76
- package/wasm/node/jpeg.js +0 -92
- package/wasm/node/png.js +0 -49
- package/wasm/node/svg.js +0 -56
- package/wasm/node/tiff.js +0 -47
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface EncodeOptions {
|
|
2
|
+
/** tEXt chunks, empty values are skipped */
|
|
3
|
+
text?: Record<string, string | undefined>;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
channels: number;
|
|
7
|
+
depth?: number;
|
|
8
|
+
level?: number;
|
|
9
|
+
/** packed RGB palette, makes the data palette indices (1 channel) */
|
|
10
|
+
palette?: Uint8Array;
|
|
11
|
+
/** alpha per palette entry */
|
|
12
|
+
transparency?: Uint8Array;
|
|
13
|
+
/** whether to paeth filter the rows (pays off for truecolor and greyscale images) */
|
|
14
|
+
filter?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface Decoded {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
/** 8-bit RGBA */
|
|
20
|
+
pixels: Uint8Array;
|
|
21
|
+
/** Exif orientation (1-8) */
|
|
22
|
+
orientation: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Encodes raw pixel data into a PNG
|
|
26
|
+
* @param data Raw pixel data, rows packed MSB first for depths below 8
|
|
27
|
+
*/
|
|
28
|
+
export declare function encode(data: Uint8Array, { text, width, height, channels, depth, level, palette, transparency, filter: filterRows }: EncodeOptions): Promise<Uint8Array>;
|
|
29
|
+
/**
|
|
30
|
+
* Encodes 8-bit RGBA pixels into a PNG with the smallest lossless color type:
|
|
31
|
+
* a palette for up to 256 colors, otherwise greyscale or RGB, with alpha only if a pixel isn't opaque.
|
|
32
|
+
* Rows of non-palette images are paeth filtered (unless the data isn't compressed), palette indices aren't
|
|
33
|
+
* filtered (like libpng recommends)
|
|
34
|
+
*/
|
|
35
|
+
export declare function encodeRGBA(rgba: Uint8Array, width: number, height: number, level: number, text?: EncodeOptions['text']): Promise<Uint8Array>;
|
|
36
|
+
/**
|
|
37
|
+
* Decodes a PNG into 8-bit RGBA pixel data
|
|
38
|
+
* Supports all color types, bit depths, tRNS transparency and adam7 interlacing
|
|
39
|
+
* The orientation comes from the eXIf chunk, or, if there is none, from the Exif text ImageMagick wrote before eXIf
|
|
40
|
+
* existed ("Raw profile type exif") or from the XMP packet
|
|
41
|
+
* @param {Uint8Array} array The PNG data
|
|
42
|
+
* @return {{width: number, height: number, pixels: Uint8Array, orientation: number}}
|
|
43
|
+
*/
|
|
44
|
+
export declare function decode(array: Uint8Array): Promise<Decoded>;
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { crc32 } from "./crc.js";
|
|
2
|
+
import { concat } from "../util/mem.js";
|
|
3
|
+
import { encode as utf8, decode as utf8Decode } from "../util/utf8.js";
|
|
4
|
+
import { orientation as exifOrientation, xmpOrientation } from "./exif.js";
|
|
5
|
+
import { compressAsync, decompressAsync } from "./zlib.js";
|
|
6
|
+
const HEAD = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
|
|
7
|
+
const IHDR = 0x49484452;
|
|
8
|
+
const PLTE = 0x504c5445;
|
|
9
|
+
const tRNS = 0x74524e53;
|
|
10
|
+
const IDAT = 0x49444154;
|
|
11
|
+
const IEND = 0x49454e44;
|
|
12
|
+
const eXIf = 0x65584966;
|
|
13
|
+
const tEXt = 0x74455874;
|
|
14
|
+
const zTXt = 0x7a545874;
|
|
15
|
+
const iTXt = 0x69545874;
|
|
16
|
+
const COLOR_TYPE_CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
|
|
17
|
+
const CHANNELS_TO_COLOR_TYPE = { 1: 0, 2: 4, 3: 2, 4: 6 };
|
|
18
|
+
const VALID_DEPTHS = { 0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16] };
|
|
19
|
+
const ADAM7 = [[0, 0, 8, 8], [4, 0, 8, 8], [0, 4, 4, 8], [2, 0, 4, 4], [0, 2, 2, 4], [1, 0, 2, 2], [0, 1, 1, 2]];
|
|
20
|
+
export async function encode(data, { text, width, height, channels, depth = 8, level = 0, palette, transparency, filter: filterRows = false }) {
|
|
21
|
+
const rowBytes = Math.ceil(width * channels * depth / 8);
|
|
22
|
+
let filtered;
|
|
23
|
+
if (filterRows)
|
|
24
|
+
filtered = filter(data, rowBytes, height, Math.max(1, channels * depth / 8));
|
|
25
|
+
else {
|
|
26
|
+
filtered = new Uint8Array(height * (rowBytes + 1));
|
|
27
|
+
for (let y = 0; y < height; y++)
|
|
28
|
+
filtered.set(data.subarray(y * rowBytes, (y + 1) * rowBytes), y * (rowBytes + 1) + 1);
|
|
29
|
+
}
|
|
30
|
+
const header = new Uint8Array(13);
|
|
31
|
+
const view = new DataView(header.buffer);
|
|
32
|
+
view.setUint32(0, width);
|
|
33
|
+
view.setUint32(4, height);
|
|
34
|
+
header[8] = depth;
|
|
35
|
+
header[9] = palette ? 3 : CHANNELS_TO_COLOR_TYPE[channels];
|
|
36
|
+
const chunks = [chunk(IHDR, header)];
|
|
37
|
+
if (palette)
|
|
38
|
+
chunks.push(chunk(PLTE, palette));
|
|
39
|
+
if (transparency)
|
|
40
|
+
chunks.push(chunk(tRNS, transparency));
|
|
41
|
+
chunks.push(chunk(IDAT, await compressAsync(filtered, level)));
|
|
42
|
+
for (const key in text) {
|
|
43
|
+
const value = text[key];
|
|
44
|
+
if (!value)
|
|
45
|
+
continue;
|
|
46
|
+
chunks.push(chunk(0x74455874, concat([utf8(key), [0], utf8(value)])));
|
|
47
|
+
}
|
|
48
|
+
chunks.push(chunk(IEND, new Uint8Array(0)));
|
|
49
|
+
return concat([HEAD, ...chunks]);
|
|
50
|
+
}
|
|
51
|
+
function filter(data, rowBytes, height, bpp) {
|
|
52
|
+
const out = new Uint8Array(height * (rowBytes + 1));
|
|
53
|
+
for (let y = 0; y < height; y++) {
|
|
54
|
+
const row = y * rowBytes, o = y * (rowBytes + 1) + 1;
|
|
55
|
+
out[o - 1] = 4;
|
|
56
|
+
if (y === 0) {
|
|
57
|
+
for (let i = 0; i < rowBytes; i++)
|
|
58
|
+
out[o + i] = data[row + i] - (i >= bpp ? data[row + i - bpp] : 0);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (let i = 0; i < bpp; i++)
|
|
62
|
+
out[o + i] = data[row + i] - data[row + i - rowBytes];
|
|
63
|
+
for (let i = bpp; i < rowBytes; i++) {
|
|
64
|
+
const a = data[row + i - bpp], b = data[row + i - rowBytes], c = data[row + i - rowBytes - bpp];
|
|
65
|
+
const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
|
|
66
|
+
out[o + i] = data[row + i] - (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function chunk(type, body) {
|
|
72
|
+
const out = new Uint8Array(body.length + 12);
|
|
73
|
+
const view = new DataView(out.buffer);
|
|
74
|
+
view.setUint32(0, body.length);
|
|
75
|
+
view.setUint32(4, type);
|
|
76
|
+
out.set(body, 8);
|
|
77
|
+
view.setUint32(body.length + 8, crc32(out.subarray(4, body.length + 8)));
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
export async function encodeRGBA(rgba, width, height, level, text) {
|
|
81
|
+
const count = width * height;
|
|
82
|
+
if (rgba.byteOffset % 4)
|
|
83
|
+
rgba = rgba.slice(0, count * 4);
|
|
84
|
+
const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, count);
|
|
85
|
+
let opaque = true, grey = true;
|
|
86
|
+
let colors = new Map();
|
|
87
|
+
let previous = -1;
|
|
88
|
+
for (let i = 0; i < count; i++) {
|
|
89
|
+
const value = pixels[i];
|
|
90
|
+
if (value === previous)
|
|
91
|
+
continue;
|
|
92
|
+
previous = value;
|
|
93
|
+
const o = i * 4;
|
|
94
|
+
if (rgba[o + 3] !== 255)
|
|
95
|
+
opaque = false;
|
|
96
|
+
if (rgba[o] !== rgba[o + 1] || rgba[o] !== rgba[o + 2])
|
|
97
|
+
grey = false;
|
|
98
|
+
if (colors && !colors.has(value)) {
|
|
99
|
+
if (colors.size === 256)
|
|
100
|
+
colors = null;
|
|
101
|
+
else
|
|
102
|
+
colors.set(value, colors.size);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const options = { width, height, level, text, filter: level > 0 };
|
|
106
|
+
if (colors) {
|
|
107
|
+
const entries = [...colors.keys()].map(value => new Uint8Array(new Uint32Array([value]).buffer));
|
|
108
|
+
entries.sort((a, b) => a[3] - b[3]);
|
|
109
|
+
const index = new Map();
|
|
110
|
+
entries.forEach((bytes, i) => index.set(new Uint32Array(bytes.buffer)[0], i));
|
|
111
|
+
const palette = new Uint8Array(entries.length * 3);
|
|
112
|
+
entries.forEach((bytes, i) => palette.set(bytes.subarray(0, 3), i * 3));
|
|
113
|
+
const translucent = entries.filter(bytes => bytes[3] !== 255).length;
|
|
114
|
+
const transparency = translucent ? Uint8Array.from(entries.slice(0, translucent), bytes => bytes[3]) : undefined;
|
|
115
|
+
const depth = entries.length <= 2 ? 1 : entries.length <= 4 ? 2 : entries.length <= 16 ? 4 : 8;
|
|
116
|
+
const rowBytes = Math.ceil(width * depth / 8);
|
|
117
|
+
const perByte = 8 / depth;
|
|
118
|
+
const data = new Uint8Array(rowBytes * height);
|
|
119
|
+
let last = -1, lastIndex = 0;
|
|
120
|
+
for (let y = 0; y < height; y++) {
|
|
121
|
+
for (let x = 0; x < width; x++) {
|
|
122
|
+
const value = pixels[y * width + x];
|
|
123
|
+
if (value !== last) {
|
|
124
|
+
last = value;
|
|
125
|
+
lastIndex = index.get(value);
|
|
126
|
+
}
|
|
127
|
+
data[y * rowBytes + (x / perByte | 0)] |= lastIndex << (8 - depth - (x % perByte) * depth);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return encode(data, { ...options, channels: 1, depth, palette, transparency, filter: false });
|
|
131
|
+
}
|
|
132
|
+
const channels = (grey ? 1 : 3) + (opaque ? 0 : 1);
|
|
133
|
+
if (channels === 4)
|
|
134
|
+
return encode(rgba, { ...options, channels });
|
|
135
|
+
const data = new Uint8Array(count * channels);
|
|
136
|
+
for (let i = 0, o = 0; i < count; i++) {
|
|
137
|
+
const s = i * 4;
|
|
138
|
+
if (grey)
|
|
139
|
+
data[o++] = rgba[s];
|
|
140
|
+
else {
|
|
141
|
+
data[o++] = rgba[s];
|
|
142
|
+
data[o++] = rgba[s + 1];
|
|
143
|
+
data[o++] = rgba[s + 2];
|
|
144
|
+
}
|
|
145
|
+
if (!opaque)
|
|
146
|
+
data[o++] = rgba[s + 3];
|
|
147
|
+
}
|
|
148
|
+
return encode(data, { ...options, channels });
|
|
149
|
+
}
|
|
150
|
+
export async function decode(array) {
|
|
151
|
+
const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
|
|
152
|
+
for (let i = 0; i < HEAD.length; i++)
|
|
153
|
+
if (array[i] !== HEAD[i])
|
|
154
|
+
throw new Error('PNG: invalid signature');
|
|
155
|
+
let header = null;
|
|
156
|
+
let palette = null;
|
|
157
|
+
let transparency = null;
|
|
158
|
+
const idat = [];
|
|
159
|
+
let exif = null;
|
|
160
|
+
const text = [];
|
|
161
|
+
let offset = 8;
|
|
162
|
+
while (offset + 8 <= array.length) {
|
|
163
|
+
const length = view.getUint32(offset);
|
|
164
|
+
const type = view.getUint32(offset + 4);
|
|
165
|
+
const start = offset + 8;
|
|
166
|
+
const end = start + length;
|
|
167
|
+
if (end > array.length) {
|
|
168
|
+
if (type === IDAT)
|
|
169
|
+
idat.push(array.subarray(start));
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
const chunk = array.subarray(start, end);
|
|
173
|
+
if (end + 4 <= array.length && crc32(array.subarray(offset + 4, end)) !== view.getUint32(end))
|
|
174
|
+
throw new Error('PNG: chunk checksum mismatch');
|
|
175
|
+
if (type === IHDR) {
|
|
176
|
+
if (length < 13)
|
|
177
|
+
throw new Error('PNG: invalid IHDR');
|
|
178
|
+
header = {
|
|
179
|
+
width: view.getUint32(start),
|
|
180
|
+
height: view.getUint32(start + 4),
|
|
181
|
+
depth: chunk[8],
|
|
182
|
+
colorType: chunk[9],
|
|
183
|
+
interlace: chunk[12]
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
else if (type === PLTE) {
|
|
187
|
+
if (palette)
|
|
188
|
+
throw new Error('PNG: PLTE can only occur once in an image');
|
|
189
|
+
palette = chunk;
|
|
190
|
+
}
|
|
191
|
+
else if (type === tRNS) {
|
|
192
|
+
if (transparency)
|
|
193
|
+
throw new Error('PNG: tRNS can only occur once in an image');
|
|
194
|
+
transparency = chunk;
|
|
195
|
+
}
|
|
196
|
+
else if (type === IDAT)
|
|
197
|
+
idat.push(chunk);
|
|
198
|
+
else if (type === eXIf)
|
|
199
|
+
exif ??= chunk;
|
|
200
|
+
else if (type === tEXt || type === zTXt || type === iTXt)
|
|
201
|
+
text.push({ type, chunk });
|
|
202
|
+
else if (type === IEND)
|
|
203
|
+
break;
|
|
204
|
+
offset = end + 4;
|
|
205
|
+
}
|
|
206
|
+
if (!header)
|
|
207
|
+
throw new Error('PNG: missing IHDR');
|
|
208
|
+
if (!idat.length)
|
|
209
|
+
throw new Error('PNG: missing IDAT');
|
|
210
|
+
const { width, height, depth, colorType, interlace } = header;
|
|
211
|
+
const channels = COLOR_TYPE_CHANNELS[colorType];
|
|
212
|
+
if (!channels)
|
|
213
|
+
throw new Error(`PNG: invalid color type ${colorType}`);
|
|
214
|
+
if (!VALID_DEPTHS[colorType].includes(depth))
|
|
215
|
+
throw new Error(`PNG: invalid bit depth ${depth} for color type ${colorType}`);
|
|
216
|
+
if (width === 0 || height === 0)
|
|
217
|
+
throw new Error('PNG: invalid dimensions');
|
|
218
|
+
if (colorType === 3 && !palette)
|
|
219
|
+
throw new Error('PNG: indexed color image has no PLTE');
|
|
220
|
+
const bitsPerPixel = channels * depth;
|
|
221
|
+
const bpp = Math.max(1, bitsPerPixel >> 3);
|
|
222
|
+
const passes = interlace ? ADAM7.map(([xs, ys, dx, dy]) => [xs, ys, dx, dy, Math.ceil((width - xs) / dx), Math.ceil((height - ys) / dy)]) : [[0, 0, 1, 1, width, height]];
|
|
223
|
+
let expected = 0;
|
|
224
|
+
for (const pass of passes) {
|
|
225
|
+
const [, , , , pw, ph] = pass;
|
|
226
|
+
if (pw > 0 && ph > 0)
|
|
227
|
+
expected += ph * (1 + Math.ceil(pw * bitsPerPixel / 8));
|
|
228
|
+
}
|
|
229
|
+
const data = await decompressAsync(idat.length === 1 ? idat[0] : concat(idat), expected);
|
|
230
|
+
if (data.length < expected)
|
|
231
|
+
throw new Error('PNG: image data is truncated');
|
|
232
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
233
|
+
const toRGBA = createConverter(colorType, depth, palette, transparency);
|
|
234
|
+
let dataOffset = 0;
|
|
235
|
+
for (const [xs, ys, dx, dy, pw, ph] of passes) {
|
|
236
|
+
if (pw <= 0 || ph <= 0)
|
|
237
|
+
continue;
|
|
238
|
+
const rowBytes = Math.ceil(pw * bitsPerPixel / 8);
|
|
239
|
+
const size = ph * rowBytes;
|
|
240
|
+
const pixels = unfilter(data, dataOffset, ph, rowBytes, bpp);
|
|
241
|
+
dataOffset += size + ph;
|
|
242
|
+
toRGBA(pixels, rowBytes, pw, ph, rgba, { row: y => ((ys + y * dy) * width + xs) * 4, step: dx * 4 });
|
|
243
|
+
}
|
|
244
|
+
return { width, height, pixels: rgba, orientation: exif ? exifOrientation(exif) : await textOrientation(text) };
|
|
245
|
+
}
|
|
246
|
+
const RAW_EXIF = /^Raw profile type (exif|APP1)$/;
|
|
247
|
+
const XMP = 'XML:com.adobe.xmp';
|
|
248
|
+
async function textOrientation(chunks) {
|
|
249
|
+
for (const { type, chunk } of chunks) {
|
|
250
|
+
const separator = chunk.indexOf(0);
|
|
251
|
+
if (separator < 1)
|
|
252
|
+
continue;
|
|
253
|
+
const keyword = utf8Decode(chunk.subarray(0, separator));
|
|
254
|
+
if (!RAW_EXIF.test(keyword) && keyword !== XMP)
|
|
255
|
+
continue;
|
|
256
|
+
let body;
|
|
257
|
+
try {
|
|
258
|
+
if (type === tEXt)
|
|
259
|
+
body = chunk.subarray(separator + 1);
|
|
260
|
+
else if (type === zTXt)
|
|
261
|
+
body = await decompressAsync(chunk.subarray(separator + 2), 1 << 20);
|
|
262
|
+
else {
|
|
263
|
+
const language = chunk.indexOf(0, separator + 3);
|
|
264
|
+
const translated = language < 0 ? -1 : chunk.indexOf(0, language + 1);
|
|
265
|
+
if (translated < 0)
|
|
266
|
+
continue;
|
|
267
|
+
body = chunk[separator + 1] ? await decompressAsync(chunk.subarray(translated + 1), 1 << 20) : chunk.subarray(translated + 1);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const value = utf8Decode(body);
|
|
274
|
+
const orientation = keyword === XMP ? xmpOrientation(value) : exifOrientation(hexProfile(value));
|
|
275
|
+
if (orientation !== 1)
|
|
276
|
+
return orientation;
|
|
277
|
+
}
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
function hexProfile(text) {
|
|
281
|
+
const match = /^\s*\S+\s+(\d+)\s+([\da-fA-F\s]+)/.exec(text);
|
|
282
|
+
if (!match)
|
|
283
|
+
return new Uint8Array(0);
|
|
284
|
+
const digits = match[2].replace(/\s+/g, '');
|
|
285
|
+
const out = new Uint8Array(Math.min(+match[1], digits.length >> 1));
|
|
286
|
+
for (let i = 0; i < out.length; i++)
|
|
287
|
+
out[i] = parseInt(digits.substr(i * 2, 2), 16);
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
function unfilter(data, offset, height, rowBytes, bpp) {
|
|
291
|
+
const out = new Uint8Array(height * rowBytes);
|
|
292
|
+
const first = Math.min(bpp, rowBytes);
|
|
293
|
+
for (let y = 0; y < height; y++) {
|
|
294
|
+
const filter = data[offset++];
|
|
295
|
+
const row = y * rowBytes;
|
|
296
|
+
const src = offset - row;
|
|
297
|
+
if (filter === 0 || (filter === 2 && y === 0))
|
|
298
|
+
out.set(data.subarray(offset, offset + rowBytes), row);
|
|
299
|
+
else if (filter === 1) {
|
|
300
|
+
out.set(data.subarray(offset, offset + first), row);
|
|
301
|
+
for (let i = row + bpp; i < row + rowBytes; i++)
|
|
302
|
+
out[i] = data[src + i] + out[i - bpp];
|
|
303
|
+
}
|
|
304
|
+
else if (filter === 2) {
|
|
305
|
+
for (let i = row; i < row + rowBytes; i++)
|
|
306
|
+
out[i] = data[src + i] + out[i - rowBytes];
|
|
307
|
+
}
|
|
308
|
+
else if (filter === 3) {
|
|
309
|
+
if (y === 0) {
|
|
310
|
+
out.set(data.subarray(offset, offset + first), row);
|
|
311
|
+
for (let i = row + bpp; i < row + rowBytes; i++)
|
|
312
|
+
out[i] = data[src + i] + (out[i - bpp] >> 1);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
for (let i = row; i < row + first; i++)
|
|
316
|
+
out[i] = data[src + i] + (out[i - rowBytes] >> 1);
|
|
317
|
+
for (let i = row + bpp; i < row + rowBytes; i++)
|
|
318
|
+
out[i] = data[src + i] + (out[i - bpp] + out[i - rowBytes] >> 1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
else if (filter === 4) {
|
|
322
|
+
if (y === 0) {
|
|
323
|
+
out.set(data.subarray(offset, offset + first), row);
|
|
324
|
+
for (let i = row + bpp; i < row + rowBytes; i++)
|
|
325
|
+
out[i] = data[src + i] + out[i - bpp];
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
for (let i = row; i < row + first; i++)
|
|
329
|
+
out[i] = data[src + i] + out[i - rowBytes];
|
|
330
|
+
for (let i = row + bpp; i < row + rowBytes; i++) {
|
|
331
|
+
const a = out[i - bpp], b = out[i - rowBytes], c = out[i - rowBytes - bpp];
|
|
332
|
+
const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
|
|
333
|
+
out[i] = data[src + i] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else
|
|
338
|
+
throw new Error(`PNG: invalid filter type ${filter}`);
|
|
339
|
+
offset += rowBytes;
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
function createConverter(colorType, depth, palette, transparency) {
|
|
344
|
+
const maxValue = (1 << depth) - 1;
|
|
345
|
+
const sample = depth === 8 ? (row, n) => row[n]
|
|
346
|
+
: depth === 16 ? (row, n) => row[n * 2] << 8 | row[n * 2 + 1]
|
|
347
|
+
: (row, n) => {
|
|
348
|
+
const bit = n * depth;
|
|
349
|
+
return (row[bit >> 3] >> (8 - depth - (bit & 7))) & maxValue;
|
|
350
|
+
};
|
|
351
|
+
const scale = depth === 8 ? v => v : depth === 16 ? v => v >> 8 : v => Math.round(v * 255 / maxValue);
|
|
352
|
+
if (colorType === 3) {
|
|
353
|
+
const pal = palette;
|
|
354
|
+
const entries = pal.length / 3 | 0;
|
|
355
|
+
const lut = new Uint8Array(256 * 4);
|
|
356
|
+
for (let i = 0; i < 256; i++) {
|
|
357
|
+
if (i < entries) {
|
|
358
|
+
lut[i * 4] = pal[i * 3];
|
|
359
|
+
lut[i * 4 + 1] = pal[i * 3 + 1];
|
|
360
|
+
lut[i * 4 + 2] = pal[i * 3 + 2];
|
|
361
|
+
}
|
|
362
|
+
lut[i * 4 + 3] = transparency && i < transparency.length ? transparency[i] : 0xff;
|
|
363
|
+
}
|
|
364
|
+
return (pixels, rowBytes, w, h, out, place) => {
|
|
365
|
+
for (let y = 0; y < h; y++) {
|
|
366
|
+
const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes);
|
|
367
|
+
for (let x = 0, o = place.row(y); x < w; x++, o += place.step) {
|
|
368
|
+
const i = sample(row, x) * 4;
|
|
369
|
+
out[o] = lut[i];
|
|
370
|
+
out[o + 1] = lut[i + 1];
|
|
371
|
+
out[o + 2] = lut[i + 2];
|
|
372
|
+
out[o + 3] = lut[i + 3];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (colorType === 0) {
|
|
378
|
+
const key = transparency && transparency.length >= 2 ? (transparency[0] << 8 | transparency[1]) & maxValue : -1;
|
|
379
|
+
return (pixels, rowBytes, w, h, out, place) => {
|
|
380
|
+
for (let y = 0; y < h; y++) {
|
|
381
|
+
const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes);
|
|
382
|
+
for (let x = 0, o = place.row(y); x < w; x++, o += place.step) {
|
|
383
|
+
const raw = sample(row, x);
|
|
384
|
+
const v = scale(raw);
|
|
385
|
+
out[o] = out[o + 1] = out[o + 2] = v;
|
|
386
|
+
out[o + 3] = raw === key ? 0 : 0xff;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (colorType === 2) {
|
|
392
|
+
const key = transparency && transparency.length >= 6
|
|
393
|
+
? [transparency[0] << 8 | transparency[1], transparency[2] << 8 | transparency[3], transparency[4] << 8 | transparency[5]].map(v => v & maxValue)
|
|
394
|
+
: null;
|
|
395
|
+
return (pixels, rowBytes, w, h, out, place) => {
|
|
396
|
+
for (let y = 0; y < h; y++) {
|
|
397
|
+
const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes);
|
|
398
|
+
for (let x = 0, o = place.row(y); x < w; x++, o += place.step) {
|
|
399
|
+
const r = sample(row, x * 3);
|
|
400
|
+
const g = sample(row, x * 3 + 1);
|
|
401
|
+
const b = sample(row, x * 3 + 2);
|
|
402
|
+
out[o] = scale(r);
|
|
403
|
+
out[o + 1] = scale(g);
|
|
404
|
+
out[o + 2] = scale(b);
|
|
405
|
+
out[o + 3] = key && r === key[0] && g === key[1] && b === key[2] ? 0 : 0xff;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (colorType === 4) {
|
|
411
|
+
return (pixels, rowBytes, w, h, out, place) => {
|
|
412
|
+
for (let y = 0; y < h; y++) {
|
|
413
|
+
const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes);
|
|
414
|
+
for (let x = 0, o = place.row(y); x < w; x++, o += place.step) {
|
|
415
|
+
const v = scale(sample(row, x * 2));
|
|
416
|
+
out[o] = out[o + 1] = out[o + 2] = v;
|
|
417
|
+
out[o + 3] = scale(sample(row, x * 2 + 1));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (depth === 8) {
|
|
423
|
+
return (pixels, rowBytes, w, h, out, place) => {
|
|
424
|
+
if (place.step === 4 && w * 4 === rowBytes && place.row(0) === 0 && place.row(1) === rowBytes)
|
|
425
|
+
return out.set(pixels);
|
|
426
|
+
for (let y = 0; y < h; y++)
|
|
427
|
+
for (let x = 0, o = place.row(y); x < w; x++, o += place.step)
|
|
428
|
+
out.set(pixels.subarray(y * rowBytes + x * 4, y * rowBytes + x * 4 + 4), o);
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return (pixels, rowBytes, w, h, out, place) => {
|
|
432
|
+
for (let y = 0; y < h; y++) {
|
|
433
|
+
const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes);
|
|
434
|
+
for (let x = 0, o = place.row(y); x < w; x++, o += place.step) {
|
|
435
|
+
out[o] = scale(sample(row, x * 4));
|
|
436
|
+
out[o + 1] = scale(sample(row, x * 4 + 1));
|
|
437
|
+
out[o + 2] = scale(sample(row, x * 4 + 2));
|
|
438
|
+
out[o + 3] = scale(sample(row, x * 4 + 3));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reduces RGBA pixel data to an indexed palette
|
|
3
|
+
* Pixels with alpha < 128 are mapped to a dedicated transparent index
|
|
4
|
+
* @param {Uint8Array} pixels RGBA pixel data
|
|
5
|
+
* @param {number} width Image width in pixels (needed for dithering)
|
|
6
|
+
* @param {{maxColors?: number, quality?: number}} [options] quality (1-100) scales the palette size exponentially (1 = 8 colors, 50 = 55, 90 and above = 256)
|
|
7
|
+
* @return {{palette: Uint8Array, indices: Uint8Array, transparentIndex: number}} palette is packed RGB, transparentIndex is -1 if unused
|
|
8
|
+
*/
|
|
9
|
+
export interface QuantizeOptions {
|
|
10
|
+
maxColors?: number;
|
|
11
|
+
quality?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface Quantized {
|
|
14
|
+
/** packed RGB */
|
|
15
|
+
palette: Uint8Array;
|
|
16
|
+
indices: Uint8Array;
|
|
17
|
+
/** -1 if no pixel is transparent */
|
|
18
|
+
transparentIndex: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Finds the color of the largest cluster among the opaque pixels that pass the filter
|
|
22
|
+
* @return packed RGB, or null if no pixel qualifies
|
|
23
|
+
*/
|
|
24
|
+
export declare function dominantColor(pixels: Uint8Array, include: (r: number, g: number, b: number) => boolean, clusters?: number): [number, number, number] | null;
|
|
25
|
+
export declare function quantize(pixels: Uint8Array, width: number, { maxColors, quality }?: QuantizeOptions): Quantized;
|