image-to-pixel 3.0.0 → 3.1.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 +21 -21
- package/README.md +2 -2
- package/dist/editor.css +1 -1
- package/dist/editor.css.map +2 -2
- package/dist/editor.esm.js +60 -60
- package/dist/editor.esm.js.map +4 -4
- package/dist/fonts/README.md +6 -6
- package/dist/fonts/geist-LICENSE.txt +93 -93
- package/dist/image-to-pixel.bundle.esm.js +16 -16
- package/dist/image-to-pixel.bundle.esm.js.map +3 -3
- package/dist/image-to-pixel.cjs +2 -2
- package/dist/image-to-pixel.cjs.map +2 -2
- package/dist/image-to-pixel.esm.js +2 -2
- package/dist/image-to-pixel.esm.js.map +2 -2
- package/dist/image-to-pixel.js +2 -2
- package/dist/image-to-pixel.js.map +2 -2
- package/dist/image-to-pixel.min.js +11 -11
- package/dist/image-to-pixel.min.js.map +3 -3
- package/dist/video.esm.js +2 -2
- package/dist/video.esm.js.map +4 -4
- package/lib/core/color.js +69 -69
- package/lib/core/cpu/pipeline.js +21 -21
- package/lib/core/cpu/stages.js +227 -227
- package/lib/core/dither/bluenoise-data.js +3 -3
- package/lib/core/dither/kernels.js +27 -27
- package/lib/core/dither/matrices.js +95 -95
- package/lib/core/engine.js +158 -158
- package/lib/core/export/canvas.js +22 -22
- package/lib/core/export/crc32.js +15 -15
- package/lib/core/export/gif.js +153 -153
- package/lib/core/export/png.js +94 -94
- package/lib/core/export/scale.js +103 -103
- package/lib/core/export/sheet.js +95 -95
- package/lib/core/export/zip.js +55 -55
- package/lib/core/gpu/backend.js +159 -159
- package/lib/core/gpu/shaders.js +258 -258
- package/lib/core/palette/builtin.js +36 -36
- package/lib/core/palette/extract.js +69 -69
- package/lib/core/palette/lospec.js +24 -24
- package/lib/core/palette/parse.js +112 -112
- package/lib/core/plan.js +58 -58
- package/lib/core/presets.js +42 -42
- package/lib/core/settings.js +274 -274
- package/lib/core/source.js +89 -89
- package/lib/core/video/audio.js +159 -0
- package/lib/core/video/frames.js +38 -0
- package/lib/core/video/mux-mp4.js +213 -24
- package/lib/core/video/mux-webm.js +63 -22
- package/lib/editor/controls.js +139 -139
- package/lib/editor/dom.js +65 -65
- package/lib/editor/exporter.js +43 -3
- package/lib/editor/icons.js +4 -0
- package/lib/editor/index.js +218 -9
- package/lib/editor/inspector.js +19 -4
- package/lib/editor/library.js +8 -2
- package/lib/editor/persist.js +18 -18
- package/lib/editor/samples.js +90 -90
- package/lib/editor/stage.js +239 -239
- package/lib/editor/store.js +80 -80
- package/lib/editor/styles/editor.css +34 -1
- package/lib/editor/styles/fonts/README.md +6 -6
- package/lib/editor/styles/fonts/geist-LICENSE.txt +93 -93
- package/lib/editor/styles/tokens.css +89 -89
- package/lib/editor/ui.js +97 -97
- package/lib/editor/workspace.js +62 -0
- package/lib/index.js +31 -29
- package/lib/video.js +37 -7
- package/package.json +1 -1
- package/types/editor.d.ts +34 -34
- package/types/index.d.ts +196 -196
- package/types/video.d.ts +61 -5
package/lib/core/export/crc32.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
// CRC-32 (IEEE), shared by the PNG and zip writers.
|
|
2
|
-
let table = null;
|
|
3
|
-
export function crc32(bytes, crc = 0) {
|
|
4
|
-
if (!table) {
|
|
5
|
-
table = new Uint32Array(256);
|
|
6
|
-
for (let n = 0; n < 256; n++) {
|
|
7
|
-
let c = n;
|
|
8
|
-
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
9
|
-
table[n] = c >>> 0;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
crc = ~crc >>> 0;
|
|
13
|
-
for (let i = 0; i < bytes.length; i++) crc = table[(crc ^ bytes[i]) & 255] ^ (crc >>> 8);
|
|
14
|
-
return ~crc >>> 0;
|
|
15
|
-
}
|
|
1
|
+
// CRC-32 (IEEE), shared by the PNG and zip writers.
|
|
2
|
+
let table = null;
|
|
3
|
+
export function crc32(bytes, crc = 0) {
|
|
4
|
+
if (!table) {
|
|
5
|
+
table = new Uint32Array(256);
|
|
6
|
+
for (let n = 0; n < 256; n++) {
|
|
7
|
+
let c = n;
|
|
8
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
9
|
+
table[n] = c >>> 0;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
crc = ~crc >>> 0;
|
|
13
|
+
for (let i = 0; i < bytes.length; i++) crc = table[(crc ^ bytes[i]) & 255] ^ (crc >>> 8);
|
|
14
|
+
return ~crc >>> 0;
|
|
15
|
+
}
|
package/lib/core/export/gif.js
CHANGED
|
@@ -1,153 +1,153 @@
|
|
|
1
|
-
// Animated GIF encoder. Pixel art already uses a small palette, so frames are written
|
|
2
|
-
// losslessly with one global colour table whenever the whole animation fits in 256 colours
|
|
3
|
-
// (255 + transparency). Otherwise each frame gets its own table, reduced by median cut if needed.
|
|
4
|
-
|
|
5
|
-
import { upscale } from './scale.js';
|
|
6
|
-
import { extractPalette } from '../palette/extract.js';
|
|
7
|
-
import { parseColor } from '../color.js';
|
|
8
|
-
|
|
9
|
-
class ByteWriter {
|
|
10
|
-
constructor() { this.buf = new Uint8Array(1 << 16); this.n = 0; }
|
|
11
|
-
byte(b) { if (this.n === this.buf.length) { const nb = new Uint8Array(this.buf.length * 2); nb.set(this.buf); this.buf = nb; } this.buf[this.n++] = b; }
|
|
12
|
-
bytes(arr) { for (let i = 0; i < arr.length; i++) this.byte(arr[i]); }
|
|
13
|
-
u16(v) { this.byte(v & 255); this.byte((v >> 8) & 255); }
|
|
14
|
-
str(s) { for (let i = 0; i < s.length; i++) this.byte(s.charCodeAt(i)); }
|
|
15
|
-
result() { return this.buf.slice(0, this.n); }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** GIF-flavoured LZW for one frame of indices. */
|
|
19
|
-
export function lzwEncode(indices, minCodeSize, w) {
|
|
20
|
-
const clear = 1 << minCodeSize, eoi = clear + 1;
|
|
21
|
-
let codeSize = minCodeSize + 1, next = eoi + 1;
|
|
22
|
-
let dict = new Map();
|
|
23
|
-
let cur = 0, curBits = 0;
|
|
24
|
-
const sub = [];
|
|
25
|
-
let block = [];
|
|
26
|
-
const emit = code => {
|
|
27
|
-
cur |= code << curBits; curBits += codeSize;
|
|
28
|
-
while (curBits >= 8) { block.push(cur & 255); cur >>>= 8; curBits -= 8; if (block.length === 255) { sub.push(block); block = []; } }
|
|
29
|
-
};
|
|
30
|
-
emit(clear);
|
|
31
|
-
let prefix = indices[0];
|
|
32
|
-
for (let i = 1; i < indices.length; i++) {
|
|
33
|
-
const k = indices[i];
|
|
34
|
-
const key = prefix * 256 + k;
|
|
35
|
-
const hit = dict.get(key);
|
|
36
|
-
if (hit !== undefined) { prefix = hit; continue; }
|
|
37
|
-
emit(prefix);
|
|
38
|
-
if (next < 4096) {
|
|
39
|
-
dict.set(key, next++);
|
|
40
|
-
if (next > (1 << codeSize) && codeSize < 12) codeSize++;
|
|
41
|
-
} else {
|
|
42
|
-
emit(clear); dict = new Map(); codeSize = minCodeSize + 1; next = eoi + 1;
|
|
43
|
-
}
|
|
44
|
-
prefix = k;
|
|
45
|
-
}
|
|
46
|
-
emit(prefix);
|
|
47
|
-
emit(eoi);
|
|
48
|
-
if (curBits > 0) block.push(cur & 255);
|
|
49
|
-
if (block.length) sub.push(block);
|
|
50
|
-
w.byte(minCodeSize);
|
|
51
|
-
for (const b of sub) { w.byte(b.length); w.bytes(b); }
|
|
52
|
-
w.byte(0);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const keyOf = (d, o) => (d[o + 3] < 128 ? -1 : (d[o] << 16) | (d[o + 1] << 8) | d[o + 2]);
|
|
56
|
-
|
|
57
|
-
function tableFor(colorKeys, transparent) {
|
|
58
|
-
const list = [...colorKeys].filter(k => k !== -1);
|
|
59
|
-
const colors = list.map(k => [k >> 16, (k >> 8) & 255, k & 255]);
|
|
60
|
-
const tIndex = transparent ? colors.length : -1;
|
|
61
|
-
if (transparent) colors.push([0, 0, 0]);
|
|
62
|
-
let bits = 1; while ((1 << bits) < colors.length) bits++;
|
|
63
|
-
while (colors.length < (1 << bits)) colors.push([0, 0, 0]);
|
|
64
|
-
const index = new Map(list.map((k, i) => [k, i]));
|
|
65
|
-
return { colors, bits, tIndex, index };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function reduce(frame, max) {
|
|
69
|
-
// > max colours: median-cut palette, then nearest colour per pixel
|
|
70
|
-
const pal = extractPalette(frame, max).map(parseColor);
|
|
71
|
-
const d = new Uint8ClampedArray(frame.data);
|
|
72
|
-
const cache = new Map();
|
|
73
|
-
for (let o = 0; o < d.length; o += 4) {
|
|
74
|
-
if (d[o + 3] < 128) continue;
|
|
75
|
-
const k = (d[o] << 16) | (d[o + 1] << 8) | d[o + 2];
|
|
76
|
-
let best = cache.get(k);
|
|
77
|
-
if (best === undefined) {
|
|
78
|
-
let bd = Infinity; best = 0;
|
|
79
|
-
for (let i = 0; i < pal.length; i++) {
|
|
80
|
-
const dr = d[o] - pal[i][0], dg = d[o + 1] - pal[i][1], db = d[o + 2] - pal[i][2];
|
|
81
|
-
const dist = dr * dr * 3 + dg * dg * 4 + db * db * 2;
|
|
82
|
-
if (dist < bd) { bd = dist; best = i; }
|
|
83
|
-
}
|
|
84
|
-
cache.set(k, best);
|
|
85
|
-
}
|
|
86
|
-
d.set(pal[best], o);
|
|
87
|
-
}
|
|
88
|
-
return { ...frame, data: d };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* @param {{ width: number, height: number, data: ArrayLike<number>, delay?: number }[]} frames delay in ms
|
|
93
|
-
* @param {{ loop?: number, scale?: number, delay?: number }} [opts] loop: 0 = forever, -1 = play once
|
|
94
|
-
* @returns {Uint8Array}
|
|
95
|
-
*/
|
|
96
|
-
export function encodeGIF(frames, opts = {}) {
|
|
97
|
-
if (!frames.length) throw new Error('No frames');
|
|
98
|
-
const scale = Math.max(1, Math.round(opts.scale || 1));
|
|
99
|
-
const fr = frames.map(f => ({ ...(scale > 1 ? upscale(f, scale) : f), delay: f.delay ?? opts.delay ?? 100 }));
|
|
100
|
-
const W = fr[0].width, H = fr[0].height;
|
|
101
|
-
|
|
102
|
-
// collect colours
|
|
103
|
-
let transparent = false;
|
|
104
|
-
const all = new Set();
|
|
105
|
-
const perFrame = fr.map(f => {
|
|
106
|
-
const s = new Set();
|
|
107
|
-
for (let o = 0; o < f.data.length; o += 4) { const k = keyOf(f.data, o); if (k === -1) transparent = true; s.add(k); all.add(k); }
|
|
108
|
-
return s;
|
|
109
|
-
});
|
|
110
|
-
const opaqueCount = s => s.size - (s.has(-1) ? 1 : 0);
|
|
111
|
-
const globalOK = opaqueCount(all) + (transparent ? 1 : 0) <= 256;
|
|
112
|
-
const global = globalOK ? tableFor(all, transparent) : null;
|
|
113
|
-
|
|
114
|
-
const w = new ByteWriter();
|
|
115
|
-
w.str('GIF89a');
|
|
116
|
-
w.u16(W); w.u16(H);
|
|
117
|
-
w.byte(global ? 0xf0 | (global.bits - 1) : 0x00);
|
|
118
|
-
w.byte(0); w.byte(0);
|
|
119
|
-
if (global) for (const c of global.colors) w.bytes(c);
|
|
120
|
-
if (fr.length > 1 && opts.loop !== -1) {
|
|
121
|
-
w.byte(0x21); w.byte(0xff); w.byte(11); w.str('NETSCAPE2.0'); w.byte(3); w.byte(1); w.u16(opts.loop || 0); w.byte(0);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
fr.forEach((f0, i) => {
|
|
125
|
-
let f = f0, table = global, set = perFrame[i];
|
|
126
|
-
if (!table) {
|
|
127
|
-
const max = transparent ? 255 : 256;
|
|
128
|
-
if (opaqueCount(set) > max) {
|
|
129
|
-
f = reduce(f0, max);
|
|
130
|
-
set = new Set(); for (let o = 0; o < f.data.length; o += 4) set.add(keyOf(f.data, o));
|
|
131
|
-
}
|
|
132
|
-
table = tableFor(set, set.has(-1));
|
|
133
|
-
}
|
|
134
|
-
// graphic control extension
|
|
135
|
-
w.byte(0x21); w.byte(0xf9); w.byte(4);
|
|
136
|
-
w.byte(((transparent ? 2 : 1) << 2) | (table.tIndex >= 0 ? 1 : 0));
|
|
137
|
-
w.u16(Math.max(2, Math.round(f.delay / 10)));
|
|
138
|
-
w.byte(table.tIndex >= 0 ? table.tIndex : 0);
|
|
139
|
-
w.byte(0);
|
|
140
|
-
// image descriptor
|
|
141
|
-
w.byte(0x2c); w.u16(0); w.u16(0); w.u16(W); w.u16(H);
|
|
142
|
-
w.byte(table === global ? 0 : 0x80 | (table.bits - 1));
|
|
143
|
-
if (table !== global) for (const c of table.colors) w.bytes(c);
|
|
144
|
-
const idx = new Uint8Array(W * H);
|
|
145
|
-
for (let p = 0; p < idx.length; p++) {
|
|
146
|
-
const k = keyOf(f.data, p * 4);
|
|
147
|
-
idx[p] = k === -1 ? table.tIndex : table.index.get(k);
|
|
148
|
-
}
|
|
149
|
-
lzwEncode(idx, Math.max(2, table.bits), w);
|
|
150
|
-
});
|
|
151
|
-
w.byte(0x3b);
|
|
152
|
-
return w.result();
|
|
153
|
-
}
|
|
1
|
+
// Animated GIF encoder. Pixel art already uses a small palette, so frames are written
|
|
2
|
+
// losslessly with one global colour table whenever the whole animation fits in 256 colours
|
|
3
|
+
// (255 + transparency). Otherwise each frame gets its own table, reduced by median cut if needed.
|
|
4
|
+
|
|
5
|
+
import { upscale } from './scale.js';
|
|
6
|
+
import { extractPalette } from '../palette/extract.js';
|
|
7
|
+
import { parseColor } from '../color.js';
|
|
8
|
+
|
|
9
|
+
class ByteWriter {
|
|
10
|
+
constructor() { this.buf = new Uint8Array(1 << 16); this.n = 0; }
|
|
11
|
+
byte(b) { if (this.n === this.buf.length) { const nb = new Uint8Array(this.buf.length * 2); nb.set(this.buf); this.buf = nb; } this.buf[this.n++] = b; }
|
|
12
|
+
bytes(arr) { for (let i = 0; i < arr.length; i++) this.byte(arr[i]); }
|
|
13
|
+
u16(v) { this.byte(v & 255); this.byte((v >> 8) & 255); }
|
|
14
|
+
str(s) { for (let i = 0; i < s.length; i++) this.byte(s.charCodeAt(i)); }
|
|
15
|
+
result() { return this.buf.slice(0, this.n); }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** GIF-flavoured LZW for one frame of indices. */
|
|
19
|
+
export function lzwEncode(indices, minCodeSize, w) {
|
|
20
|
+
const clear = 1 << minCodeSize, eoi = clear + 1;
|
|
21
|
+
let codeSize = minCodeSize + 1, next = eoi + 1;
|
|
22
|
+
let dict = new Map();
|
|
23
|
+
let cur = 0, curBits = 0;
|
|
24
|
+
const sub = [];
|
|
25
|
+
let block = [];
|
|
26
|
+
const emit = code => {
|
|
27
|
+
cur |= code << curBits; curBits += codeSize;
|
|
28
|
+
while (curBits >= 8) { block.push(cur & 255); cur >>>= 8; curBits -= 8; if (block.length === 255) { sub.push(block); block = []; } }
|
|
29
|
+
};
|
|
30
|
+
emit(clear);
|
|
31
|
+
let prefix = indices[0];
|
|
32
|
+
for (let i = 1; i < indices.length; i++) {
|
|
33
|
+
const k = indices[i];
|
|
34
|
+
const key = prefix * 256 + k;
|
|
35
|
+
const hit = dict.get(key);
|
|
36
|
+
if (hit !== undefined) { prefix = hit; continue; }
|
|
37
|
+
emit(prefix);
|
|
38
|
+
if (next < 4096) {
|
|
39
|
+
dict.set(key, next++);
|
|
40
|
+
if (next > (1 << codeSize) && codeSize < 12) codeSize++;
|
|
41
|
+
} else {
|
|
42
|
+
emit(clear); dict = new Map(); codeSize = minCodeSize + 1; next = eoi + 1;
|
|
43
|
+
}
|
|
44
|
+
prefix = k;
|
|
45
|
+
}
|
|
46
|
+
emit(prefix);
|
|
47
|
+
emit(eoi);
|
|
48
|
+
if (curBits > 0) block.push(cur & 255);
|
|
49
|
+
if (block.length) sub.push(block);
|
|
50
|
+
w.byte(minCodeSize);
|
|
51
|
+
for (const b of sub) { w.byte(b.length); w.bytes(b); }
|
|
52
|
+
w.byte(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const keyOf = (d, o) => (d[o + 3] < 128 ? -1 : (d[o] << 16) | (d[o + 1] << 8) | d[o + 2]);
|
|
56
|
+
|
|
57
|
+
function tableFor(colorKeys, transparent) {
|
|
58
|
+
const list = [...colorKeys].filter(k => k !== -1);
|
|
59
|
+
const colors = list.map(k => [k >> 16, (k >> 8) & 255, k & 255]);
|
|
60
|
+
const tIndex = transparent ? colors.length : -1;
|
|
61
|
+
if (transparent) colors.push([0, 0, 0]);
|
|
62
|
+
let bits = 1; while ((1 << bits) < colors.length) bits++;
|
|
63
|
+
while (colors.length < (1 << bits)) colors.push([0, 0, 0]);
|
|
64
|
+
const index = new Map(list.map((k, i) => [k, i]));
|
|
65
|
+
return { colors, bits, tIndex, index };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function reduce(frame, max) {
|
|
69
|
+
// > max colours: median-cut palette, then nearest colour per pixel
|
|
70
|
+
const pal = extractPalette(frame, max).map(parseColor);
|
|
71
|
+
const d = new Uint8ClampedArray(frame.data);
|
|
72
|
+
const cache = new Map();
|
|
73
|
+
for (let o = 0; o < d.length; o += 4) {
|
|
74
|
+
if (d[o + 3] < 128) continue;
|
|
75
|
+
const k = (d[o] << 16) | (d[o + 1] << 8) | d[o + 2];
|
|
76
|
+
let best = cache.get(k);
|
|
77
|
+
if (best === undefined) {
|
|
78
|
+
let bd = Infinity; best = 0;
|
|
79
|
+
for (let i = 0; i < pal.length; i++) {
|
|
80
|
+
const dr = d[o] - pal[i][0], dg = d[o + 1] - pal[i][1], db = d[o + 2] - pal[i][2];
|
|
81
|
+
const dist = dr * dr * 3 + dg * dg * 4 + db * db * 2;
|
|
82
|
+
if (dist < bd) { bd = dist; best = i; }
|
|
83
|
+
}
|
|
84
|
+
cache.set(k, best);
|
|
85
|
+
}
|
|
86
|
+
d.set(pal[best], o);
|
|
87
|
+
}
|
|
88
|
+
return { ...frame, data: d };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {{ width: number, height: number, data: ArrayLike<number>, delay?: number }[]} frames delay in ms
|
|
93
|
+
* @param {{ loop?: number, scale?: number, delay?: number }} [opts] loop: 0 = forever, -1 = play once
|
|
94
|
+
* @returns {Uint8Array}
|
|
95
|
+
*/
|
|
96
|
+
export function encodeGIF(frames, opts = {}) {
|
|
97
|
+
if (!frames.length) throw new Error('No frames');
|
|
98
|
+
const scale = Math.max(1, Math.round(opts.scale || 1));
|
|
99
|
+
const fr = frames.map(f => ({ ...(scale > 1 ? upscale(f, scale) : f), delay: f.delay ?? opts.delay ?? 100 }));
|
|
100
|
+
const W = fr[0].width, H = fr[0].height;
|
|
101
|
+
|
|
102
|
+
// collect colours
|
|
103
|
+
let transparent = false;
|
|
104
|
+
const all = new Set();
|
|
105
|
+
const perFrame = fr.map(f => {
|
|
106
|
+
const s = new Set();
|
|
107
|
+
for (let o = 0; o < f.data.length; o += 4) { const k = keyOf(f.data, o); if (k === -1) transparent = true; s.add(k); all.add(k); }
|
|
108
|
+
return s;
|
|
109
|
+
});
|
|
110
|
+
const opaqueCount = s => s.size - (s.has(-1) ? 1 : 0);
|
|
111
|
+
const globalOK = opaqueCount(all) + (transparent ? 1 : 0) <= 256;
|
|
112
|
+
const global = globalOK ? tableFor(all, transparent) : null;
|
|
113
|
+
|
|
114
|
+
const w = new ByteWriter();
|
|
115
|
+
w.str('GIF89a');
|
|
116
|
+
w.u16(W); w.u16(H);
|
|
117
|
+
w.byte(global ? 0xf0 | (global.bits - 1) : 0x00);
|
|
118
|
+
w.byte(0); w.byte(0);
|
|
119
|
+
if (global) for (const c of global.colors) w.bytes(c);
|
|
120
|
+
if (fr.length > 1 && opts.loop !== -1) {
|
|
121
|
+
w.byte(0x21); w.byte(0xff); w.byte(11); w.str('NETSCAPE2.0'); w.byte(3); w.byte(1); w.u16(opts.loop || 0); w.byte(0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fr.forEach((f0, i) => {
|
|
125
|
+
let f = f0, table = global, set = perFrame[i];
|
|
126
|
+
if (!table) {
|
|
127
|
+
const max = transparent ? 255 : 256;
|
|
128
|
+
if (opaqueCount(set) > max) {
|
|
129
|
+
f = reduce(f0, max);
|
|
130
|
+
set = new Set(); for (let o = 0; o < f.data.length; o += 4) set.add(keyOf(f.data, o));
|
|
131
|
+
}
|
|
132
|
+
table = tableFor(set, set.has(-1));
|
|
133
|
+
}
|
|
134
|
+
// graphic control extension
|
|
135
|
+
w.byte(0x21); w.byte(0xf9); w.byte(4);
|
|
136
|
+
w.byte(((transparent ? 2 : 1) << 2) | (table.tIndex >= 0 ? 1 : 0));
|
|
137
|
+
w.u16(Math.max(2, Math.round(f.delay / 10)));
|
|
138
|
+
w.byte(table.tIndex >= 0 ? table.tIndex : 0);
|
|
139
|
+
w.byte(0);
|
|
140
|
+
// image descriptor
|
|
141
|
+
w.byte(0x2c); w.u16(0); w.u16(0); w.u16(W); w.u16(H);
|
|
142
|
+
w.byte(table === global ? 0 : 0x80 | (table.bits - 1));
|
|
143
|
+
if (table !== global) for (const c of table.colors) w.bytes(c);
|
|
144
|
+
const idx = new Uint8Array(W * H);
|
|
145
|
+
for (let p = 0; p < idx.length; p++) {
|
|
146
|
+
const k = keyOf(f.data, p * 4);
|
|
147
|
+
idx[p] = k === -1 ? table.tIndex : table.index.get(k);
|
|
148
|
+
}
|
|
149
|
+
lzwEncode(idx, Math.max(2, table.bits), w);
|
|
150
|
+
});
|
|
151
|
+
w.byte(0x3b);
|
|
152
|
+
return w.result();
|
|
153
|
+
}
|
package/lib/core/export/png.js
CHANGED
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
// Minimal PNG encoder: indexed (colour type 3, with tRNS) when the image has ≤ 256 colours,
|
|
2
|
-
// otherwise RGBA. Uses CompressionStream('deflate'), so it works in browsers and in Node 18+.
|
|
3
|
-
|
|
4
|
-
import { crc32 } from './crc32.js';
|
|
5
|
-
import { upscale, resizeNearest } from './scale.js';
|
|
6
|
-
|
|
7
|
-
const SIG = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
8
|
-
|
|
9
|
-
async function deflate(bytes) {
|
|
10
|
-
if (typeof CompressionStream === 'function') {
|
|
11
|
-
const cs = new CompressionStream('deflate');
|
|
12
|
-
const stream = new Blob([bytes]).stream().pipeThrough(cs);
|
|
13
|
-
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
14
|
-
}
|
|
15
|
-
const { deflateSync } = await import('node:zlib');
|
|
16
|
-
return new Uint8Array(deflateSync(bytes));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function chunk(type, data) {
|
|
20
|
-
const out = new Uint8Array(12 + data.length);
|
|
21
|
-
const dv = new DataView(out.buffer);
|
|
22
|
-
dv.setUint32(0, data.length);
|
|
23
|
-
for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
|
|
24
|
-
out.set(data, 8);
|
|
25
|
-
dv.setUint32(8 + data.length, crc32(out.subarray(4, 8 + data.length)));
|
|
26
|
-
return out;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Map pixels to palette indices. Returns null when there are more than 256 colours. */
|
|
30
|
-
export function indexImage(img) {
|
|
31
|
-
const d = img.data, n = img.width * img.height;
|
|
32
|
-
const map = new Map(), colors = [], idx = new Uint8Array(n);
|
|
33
|
-
for (let i = 0; i < n; i++) {
|
|
34
|
-
const o = i * 4;
|
|
35
|
-
const k = d[o + 3] === 0 ? -1 : ((d[o] << 24) | (d[o + 1] << 16) | (d[o + 2] << 8) | d[o + 3]) >>> 0;
|
|
36
|
-
let v = map.get(k);
|
|
37
|
-
if (v === undefined) {
|
|
38
|
-
if (colors.length === 256) return null;
|
|
39
|
-
v = colors.length; map.set(k, v);
|
|
40
|
-
colors.push(k === -1 ? [0, 0, 0, 0] : [d[o], d[o + 1], d[o + 2], d[o + 3]]);
|
|
41
|
-
}
|
|
42
|
-
idx[i] = v;
|
|
43
|
-
}
|
|
44
|
-
return { colors, indices: idx };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @param {{ width: number, height: number, data: ArrayLike<number> }} img
|
|
49
|
-
* @param {{ scale?: number, grid?: boolean, gridColor?: string, indexed?: boolean, width?: number, height?: number }} [opts]
|
|
50
|
-
* width/height: resize to exactly this size (nearest neighbour) instead of scaling
|
|
51
|
-
* @returns {Promise<Uint8Array>}
|
|
52
|
-
*/
|
|
53
|
-
export async function encodePNG(img, opts = {}) {
|
|
54
|
-
if (opts.width && opts.height) img = resizeNearest(img, opts.width, opts.height);
|
|
55
|
-
else if ((opts.scale && opts.scale > 1) || opts.grid) img = upscale(img, opts.scale || 1, opts);
|
|
56
|
-
const { width: w, height: h } = img;
|
|
57
|
-
const ix = opts.indexed === false ? null : indexImage(img);
|
|
58
|
-
const ihdr = new Uint8Array(13);
|
|
59
|
-
const dv = new DataView(ihdr.buffer);
|
|
60
|
-
dv.setUint32(0, w); dv.setUint32(4, h);
|
|
61
|
-
const parts = [new Uint8Array(SIG)];
|
|
62
|
-
let raw;
|
|
63
|
-
if (ix) {
|
|
64
|
-
const bits = ix.colors.length <= 2 ? 1 : ix.colors.length <= 4 ? 2 : ix.colors.length <= 16 ? 4 : 8;
|
|
65
|
-
ihdr.set([bits, 3, 0, 0, 0], 8);
|
|
66
|
-
const stride = Math.ceil(w * bits / 8);
|
|
67
|
-
raw = new Uint8Array((stride + 1) * h);
|
|
68
|
-
for (let y = 0; y < h; y++) {
|
|
69
|
-
const row = y * (stride + 1) + 1;
|
|
70
|
-
for (let x = 0; x < w; x++) {
|
|
71
|
-
const v = ix.indices[y * w + x];
|
|
72
|
-
const bit = x * bits;
|
|
73
|
-
raw[row + (bit >> 3)] |= v << (8 - bits - (bit & 7));
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
parts.push(chunk('IHDR', ihdr));
|
|
77
|
-
parts.push(chunk('PLTE', Uint8Array.from(ix.colors.flatMap(c => c.slice(0, 3)))));
|
|
78
|
-
if (ix.colors.some(c => c[3] < 255)) {
|
|
79
|
-
let last = ix.colors.length; while (last > 0 && ix.colors[last - 1][3] === 255) last--;
|
|
80
|
-
parts.push(chunk('tRNS', Uint8Array.from(ix.colors.slice(0, last).map(c => c[3]))));
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
ihdr.set([8, 6, 0, 0, 0], 8);
|
|
84
|
-
raw = new Uint8Array((w * 4 + 1) * h);
|
|
85
|
-
for (let y = 0; y < h; y++) raw.set(img.data.subarray ? img.data.subarray(y * w * 4, (y + 1) * w * 4) : Array.prototype.slice.call(img.data, y * w * 4, (y + 1) * w * 4), y * (w * 4 + 1) + 1);
|
|
86
|
-
parts.push(chunk('IHDR', ihdr));
|
|
87
|
-
}
|
|
88
|
-
parts.push(chunk('IDAT', await deflate(raw)));
|
|
89
|
-
parts.push(chunk('IEND', new Uint8Array(0)));
|
|
90
|
-
const total = parts.reduce((s, p) => s + p.length, 0);
|
|
91
|
-
const out = new Uint8Array(total);
|
|
92
|
-
let o = 0; for (const p of parts) { out.set(p, o); o += p.length; }
|
|
93
|
-
return out;
|
|
94
|
-
}
|
|
1
|
+
// Minimal PNG encoder: indexed (colour type 3, with tRNS) when the image has ≤ 256 colours,
|
|
2
|
+
// otherwise RGBA. Uses CompressionStream('deflate'), so it works in browsers and in Node 18+.
|
|
3
|
+
|
|
4
|
+
import { crc32 } from './crc32.js';
|
|
5
|
+
import { upscale, resizeNearest } from './scale.js';
|
|
6
|
+
|
|
7
|
+
const SIG = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
8
|
+
|
|
9
|
+
async function deflate(bytes) {
|
|
10
|
+
if (typeof CompressionStream === 'function') {
|
|
11
|
+
const cs = new CompressionStream('deflate');
|
|
12
|
+
const stream = new Blob([bytes]).stream().pipeThrough(cs);
|
|
13
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
14
|
+
}
|
|
15
|
+
const { deflateSync } = await import('node:zlib');
|
|
16
|
+
return new Uint8Array(deflateSync(bytes));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function chunk(type, data) {
|
|
20
|
+
const out = new Uint8Array(12 + data.length);
|
|
21
|
+
const dv = new DataView(out.buffer);
|
|
22
|
+
dv.setUint32(0, data.length);
|
|
23
|
+
for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
|
|
24
|
+
out.set(data, 8);
|
|
25
|
+
dv.setUint32(8 + data.length, crc32(out.subarray(4, 8 + data.length)));
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Map pixels to palette indices. Returns null when there are more than 256 colours. */
|
|
30
|
+
export function indexImage(img) {
|
|
31
|
+
const d = img.data, n = img.width * img.height;
|
|
32
|
+
const map = new Map(), colors = [], idx = new Uint8Array(n);
|
|
33
|
+
for (let i = 0; i < n; i++) {
|
|
34
|
+
const o = i * 4;
|
|
35
|
+
const k = d[o + 3] === 0 ? -1 : ((d[o] << 24) | (d[o + 1] << 16) | (d[o + 2] << 8) | d[o + 3]) >>> 0;
|
|
36
|
+
let v = map.get(k);
|
|
37
|
+
if (v === undefined) {
|
|
38
|
+
if (colors.length === 256) return null;
|
|
39
|
+
v = colors.length; map.set(k, v);
|
|
40
|
+
colors.push(k === -1 ? [0, 0, 0, 0] : [d[o], d[o + 1], d[o + 2], d[o + 3]]);
|
|
41
|
+
}
|
|
42
|
+
idx[i] = v;
|
|
43
|
+
}
|
|
44
|
+
return { colors, indices: idx };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {{ width: number, height: number, data: ArrayLike<number> }} img
|
|
49
|
+
* @param {{ scale?: number, grid?: boolean, gridColor?: string, indexed?: boolean, width?: number, height?: number }} [opts]
|
|
50
|
+
* width/height: resize to exactly this size (nearest neighbour) instead of scaling
|
|
51
|
+
* @returns {Promise<Uint8Array>}
|
|
52
|
+
*/
|
|
53
|
+
export async function encodePNG(img, opts = {}) {
|
|
54
|
+
if (opts.width && opts.height) img = resizeNearest(img, opts.width, opts.height);
|
|
55
|
+
else if ((opts.scale && opts.scale > 1) || opts.grid) img = upscale(img, opts.scale || 1, opts);
|
|
56
|
+
const { width: w, height: h } = img;
|
|
57
|
+
const ix = opts.indexed === false ? null : indexImage(img);
|
|
58
|
+
const ihdr = new Uint8Array(13);
|
|
59
|
+
const dv = new DataView(ihdr.buffer);
|
|
60
|
+
dv.setUint32(0, w); dv.setUint32(4, h);
|
|
61
|
+
const parts = [new Uint8Array(SIG)];
|
|
62
|
+
let raw;
|
|
63
|
+
if (ix) {
|
|
64
|
+
const bits = ix.colors.length <= 2 ? 1 : ix.colors.length <= 4 ? 2 : ix.colors.length <= 16 ? 4 : 8;
|
|
65
|
+
ihdr.set([bits, 3, 0, 0, 0], 8);
|
|
66
|
+
const stride = Math.ceil(w * bits / 8);
|
|
67
|
+
raw = new Uint8Array((stride + 1) * h);
|
|
68
|
+
for (let y = 0; y < h; y++) {
|
|
69
|
+
const row = y * (stride + 1) + 1;
|
|
70
|
+
for (let x = 0; x < w; x++) {
|
|
71
|
+
const v = ix.indices[y * w + x];
|
|
72
|
+
const bit = x * bits;
|
|
73
|
+
raw[row + (bit >> 3)] |= v << (8 - bits - (bit & 7));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
parts.push(chunk('IHDR', ihdr));
|
|
77
|
+
parts.push(chunk('PLTE', Uint8Array.from(ix.colors.flatMap(c => c.slice(0, 3)))));
|
|
78
|
+
if (ix.colors.some(c => c[3] < 255)) {
|
|
79
|
+
let last = ix.colors.length; while (last > 0 && ix.colors[last - 1][3] === 255) last--;
|
|
80
|
+
parts.push(chunk('tRNS', Uint8Array.from(ix.colors.slice(0, last).map(c => c[3]))));
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
ihdr.set([8, 6, 0, 0, 0], 8);
|
|
84
|
+
raw = new Uint8Array((w * 4 + 1) * h);
|
|
85
|
+
for (let y = 0; y < h; y++) raw.set(img.data.subarray ? img.data.subarray(y * w * 4, (y + 1) * w * 4) : Array.prototype.slice.call(img.data, y * w * 4, (y + 1) * w * 4), y * (w * 4 + 1) + 1);
|
|
86
|
+
parts.push(chunk('IHDR', ihdr));
|
|
87
|
+
}
|
|
88
|
+
parts.push(chunk('IDAT', await deflate(raw)));
|
|
89
|
+
parts.push(chunk('IEND', new Uint8Array(0)));
|
|
90
|
+
const total = parts.reduce((s, p) => s + p.length, 0);
|
|
91
|
+
const out = new Uint8Array(total);
|
|
92
|
+
let o = 0; for (const p of parts) { out.set(p, o); o += p.length; }
|
|
93
|
+
return out;
|
|
94
|
+
}
|