imagescript 1.3.1 → 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 +4 -1
- package/README.md +76 -26
- package/THIRD_PARTY_NOTICES.md +156 -0
- package/dist/animation.d.ts +55 -0
- package/dist/animation.js +167 -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 +97 -0
- package/dist/codecs/png.js +664 -0
- package/dist/codecs/quantize.d.ts +25 -0
- package/dist/codecs/quantize.js +302 -0
- package/dist/codecs/tga.d.ts +16 -0
- package/dist/codecs/tga.js +203 -0
- package/dist/codecs/tiff.d.ts +6 -0
- package/dist/codecs/tiff.js +546 -0
- package/dist/codecs/vp8-tables.d.ts +10 -0
- package/dist/codecs/vp8-tables.js +17 -0
- package/dist/codecs/vp8.d.ts +8 -0
- package/dist/codecs/vp8.js +711 -0
- package/dist/codecs/vp8l.d.ts +62 -0
- package/dist/codecs/vp8l.js +909 -0
- package/dist/codecs/webp.d.ts +67 -0
- package/dist/codecs/webp.js +280 -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 +31 -0
- package/dist/gif.js +20 -0
- package/dist/image-type.d.ts +24 -0
- package/dist/image-type.js +64 -0
- package/dist/image.d.ts +553 -0
- package/dist/image.js +1156 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +20 -0
- package/dist/package.json +1 -0
- package/dist/render/font.d.ts +92 -0
- package/dist/render/font.js +954 -0
- package/dist/render/hinting.d.ts +110 -0
- package/dist/render/hinting.js +1236 -0
- package/dist/render/svg.d.ts +11 -0
- package/dist/render/svg.js +1717 -0
- package/dist/render/text.d.ts +63 -0
- package/dist/render/text.js +342 -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/FTL.txt +169 -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,302 @@
|
|
|
1
|
+
const WR = 3, WG = 4, WB = 2;
|
|
2
|
+
export function dominantColor(pixels, include, clusters = 8) {
|
|
3
|
+
const { palette, weights } = medianCut(pixels, clusters, 4, include);
|
|
4
|
+
if (!weights.length)
|
|
5
|
+
return null;
|
|
6
|
+
let best = 0;
|
|
7
|
+
for (let i = 1; i < weights.length; i++)
|
|
8
|
+
if (weights[i] > weights[best])
|
|
9
|
+
best = i;
|
|
10
|
+
return [Math.round(palette[best * 3]), Math.round(palette[best * 3 + 1]), Math.round(palette[best * 3 + 2])];
|
|
11
|
+
}
|
|
12
|
+
export function quantize(pixels, width, { maxColors = 256, quality = 95 } = {}) {
|
|
13
|
+
const length = pixels.length >> 2;
|
|
14
|
+
let hasTransparency = false;
|
|
15
|
+
for (let i = 3; i < pixels.length; i += 4)
|
|
16
|
+
if (pixels[i] < 128) {
|
|
17
|
+
hasTransparency = true;
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
quality = Math.min(100, Math.max(1, quality));
|
|
21
|
+
const limit = Math.min(maxColors, Math.round(8 * 32 ** (Math.min(quality, 90) / 90)));
|
|
22
|
+
const colors = hasTransparency ? limit - 1 : limit;
|
|
23
|
+
const exact = exactPalette(pixels, colors);
|
|
24
|
+
let palette, indices;
|
|
25
|
+
if (exact)
|
|
26
|
+
({ palette, indices } = exact);
|
|
27
|
+
else {
|
|
28
|
+
palette = Uint8Array.from(medianCut(pixels, colors, Math.ceil(quality / 25)).palette, Math.round);
|
|
29
|
+
indices = ditherMap(pixels, width, palette, palette.length / 3);
|
|
30
|
+
}
|
|
31
|
+
let transparentIndex = -1;
|
|
32
|
+
if (hasTransparency) {
|
|
33
|
+
transparentIndex = palette.length / 3;
|
|
34
|
+
const padded = new Uint8Array(palette.length + 3);
|
|
35
|
+
padded.set(palette);
|
|
36
|
+
palette = padded;
|
|
37
|
+
for (let i = 0; i < length; i++)
|
|
38
|
+
if (pixels[i * 4 + 3] < 128)
|
|
39
|
+
indices[i] = transparentIndex;
|
|
40
|
+
}
|
|
41
|
+
return { palette, indices, transparentIndex };
|
|
42
|
+
}
|
|
43
|
+
function exactPalette(pixels, limit) {
|
|
44
|
+
const lookup = new Map();
|
|
45
|
+
const indices = new Uint8Array(pixels.length >> 2);
|
|
46
|
+
const palette = [];
|
|
47
|
+
for (let i = 0, p = 0; i < pixels.length; i += 4, p++) {
|
|
48
|
+
if (pixels[i + 3] < 128)
|
|
49
|
+
continue;
|
|
50
|
+
const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
|
|
51
|
+
let index = lookup.get(key);
|
|
52
|
+
if (index === undefined) {
|
|
53
|
+
if (lookup.size >= limit)
|
|
54
|
+
return null;
|
|
55
|
+
index = lookup.size;
|
|
56
|
+
lookup.set(key, index);
|
|
57
|
+
palette.push(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
58
|
+
}
|
|
59
|
+
indices[p] = index;
|
|
60
|
+
}
|
|
61
|
+
if (!palette.length)
|
|
62
|
+
palette.push(0, 0, 0);
|
|
63
|
+
return { palette: Uint8Array.from(palette), indices };
|
|
64
|
+
}
|
|
65
|
+
function medianCut(pixels, colors, iterations, include) {
|
|
66
|
+
const count = new Float64Array(32768);
|
|
67
|
+
const sumR = new Float64Array(32768);
|
|
68
|
+
const sumG = new Float64Array(32768);
|
|
69
|
+
const sumB = new Float64Array(32768);
|
|
70
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
71
|
+
if (pixels[i + 3] < 128)
|
|
72
|
+
continue;
|
|
73
|
+
const r = pixels[i], g = pixels[i + 1], b = pixels[i + 2];
|
|
74
|
+
if (include && !include(r, g, b))
|
|
75
|
+
continue;
|
|
76
|
+
const bin = (r >> 3) << 10 | (g >> 3) << 5 | b >> 3;
|
|
77
|
+
count[bin]++;
|
|
78
|
+
sumR[bin] += r;
|
|
79
|
+
sumG[bin] += g;
|
|
80
|
+
sumB[bin] += b;
|
|
81
|
+
}
|
|
82
|
+
let n = 0;
|
|
83
|
+
for (let i = 0; i < 32768; i++)
|
|
84
|
+
if (count[i])
|
|
85
|
+
n++;
|
|
86
|
+
const points = { c: [new Float64Array(n), new Float64Array(n), new Float64Array(n)], w: new Float64Array(n) };
|
|
87
|
+
for (let i = 0, p = 0; i < 32768; i++) {
|
|
88
|
+
if (!count[i])
|
|
89
|
+
continue;
|
|
90
|
+
points.c[0][p] = sumR[i] / count[i];
|
|
91
|
+
points.c[1][p] = sumG[i] / count[i];
|
|
92
|
+
points.c[2][p] = sumB[i] / count[i];
|
|
93
|
+
points.w[p++] = count[i];
|
|
94
|
+
}
|
|
95
|
+
if (!n)
|
|
96
|
+
return { palette: new Float64Array(0), weights: new Float64Array(0) };
|
|
97
|
+
const boxes = [makeBox(points, Int32Array.from({ length: n }, (_, i) => i))];
|
|
98
|
+
const scratch = new Int32Array(n);
|
|
99
|
+
while (boxes.length < colors) {
|
|
100
|
+
let target = -1;
|
|
101
|
+
for (let i = 0; i < boxes.length; i++)
|
|
102
|
+
if (boxes[i].members.length > 1 && (target === -1 || boxes[i].error > boxes[target].error))
|
|
103
|
+
target = i;
|
|
104
|
+
if (target === -1)
|
|
105
|
+
break;
|
|
106
|
+
const { members, axis, weight } = boxes[target];
|
|
107
|
+
sortBy(members, points.c[axis], scratch);
|
|
108
|
+
const half = weight / 2;
|
|
109
|
+
let split = 1;
|
|
110
|
+
for (let acc = 0; split < members.length - 1; split++) {
|
|
111
|
+
acc += points.w[members[split - 1]];
|
|
112
|
+
if (acc >= half)
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
boxes.splice(target, 1, makeBox(points, members.subarray(0, split)), makeBox(points, members.subarray(split)));
|
|
116
|
+
}
|
|
117
|
+
const palette = new Float64Array(boxes.length * 3);
|
|
118
|
+
boxes.forEach((box, i) => palette.set(box.mean, i * 3));
|
|
119
|
+
const entries = boxes.length;
|
|
120
|
+
const [pr, pg, pb] = points.c, pw = points.w;
|
|
121
|
+
const weights = Float64Array.from(boxes, box => box.weight);
|
|
122
|
+
for (let iteration = 0; iteration < iterations; iteration++) {
|
|
123
|
+
const acc = new Float64Array(entries * 4);
|
|
124
|
+
const nearest = nearestSearch(palette, entries);
|
|
125
|
+
for (let i = 0; i < n; i++) {
|
|
126
|
+
const index = nearest(pr[i], pg[i], pb[i]);
|
|
127
|
+
acc[index * 4] += pr[i] * pw[i];
|
|
128
|
+
acc[index * 4 + 1] += pg[i] * pw[i];
|
|
129
|
+
acc[index * 4 + 2] += pb[i] * pw[i];
|
|
130
|
+
acc[index * 4 + 3] += pw[i];
|
|
131
|
+
}
|
|
132
|
+
for (let i = 0; i < entries; i++) {
|
|
133
|
+
const w = weights[i] = acc[i * 4 + 3];
|
|
134
|
+
if (!w)
|
|
135
|
+
continue;
|
|
136
|
+
palette[i * 3] = acc[i * 4] / w;
|
|
137
|
+
palette[i * 3 + 1] = acc[i * 4 + 1] / w;
|
|
138
|
+
palette[i * 3 + 2] = acc[i * 4 + 2] / w;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { palette, weights };
|
|
142
|
+
}
|
|
143
|
+
function makeBox({ c: [r, g, b], w }, members) {
|
|
144
|
+
let weight = 0;
|
|
145
|
+
const sum = [0, 0, 0];
|
|
146
|
+
const sq = [0, 0, 0];
|
|
147
|
+
for (let m = 0; m < members.length; m++) {
|
|
148
|
+
const i = members[m];
|
|
149
|
+
weight += w[i];
|
|
150
|
+
sum[0] += r[i] * w[i];
|
|
151
|
+
sq[0] += r[i] * r[i] * w[i];
|
|
152
|
+
sum[1] += g[i] * w[i];
|
|
153
|
+
sq[1] += g[i] * g[i] * w[i];
|
|
154
|
+
sum[2] += b[i] * w[i];
|
|
155
|
+
sq[2] += b[i] * b[i] * w[i];
|
|
156
|
+
}
|
|
157
|
+
const mean = sum.map(v => v / weight);
|
|
158
|
+
const error = sq.map((v, k) => (v - sum[k] * mean[k]) * [WR, WG, WB][k]);
|
|
159
|
+
const axis = error.indexOf(Math.max(...error));
|
|
160
|
+
return { members, weight, mean, axis, error: error[0] + error[1] + error[2] };
|
|
161
|
+
}
|
|
162
|
+
function sortBy(indices, keys, scratch) {
|
|
163
|
+
const n = indices.length;
|
|
164
|
+
const RUN = 16;
|
|
165
|
+
for (let start = 0; start < n; start += RUN) {
|
|
166
|
+
const end = Math.min(n, start + RUN);
|
|
167
|
+
for (let i = start + 1; i < end; i++) {
|
|
168
|
+
const v = indices[i], key = keys[v];
|
|
169
|
+
let j = i - 1;
|
|
170
|
+
for (; j >= start && keys[indices[j]] > key; j--)
|
|
171
|
+
indices[j + 1] = indices[j];
|
|
172
|
+
indices[j + 1] = v;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
let src = indices, dst = scratch.subarray(0, n);
|
|
176
|
+
for (let width = RUN; width < n; width *= 2) {
|
|
177
|
+
for (let start = 0; start < n; start += 2 * width) {
|
|
178
|
+
const mid = Math.min(n, start + width), end = Math.min(n, start + 2 * width);
|
|
179
|
+
let i = start, j = mid, o = start;
|
|
180
|
+
if (mid >= end || keys[src[mid - 1]] <= keys[src[mid]]) {
|
|
181
|
+
dst.set(src.subarray(start, end), start);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
while (i < mid && j < end)
|
|
185
|
+
dst[o++] = keys[src[j]] < keys[src[i]] ? src[j++] : src[i++];
|
|
186
|
+
while (i < mid)
|
|
187
|
+
dst[o++] = src[i++];
|
|
188
|
+
while (j < end)
|
|
189
|
+
dst[o++] = src[j++];
|
|
190
|
+
}
|
|
191
|
+
[src, dst] = [dst, src];
|
|
192
|
+
}
|
|
193
|
+
if (src !== indices)
|
|
194
|
+
indices.set(src);
|
|
195
|
+
}
|
|
196
|
+
function nearestSearch(palette, entries) {
|
|
197
|
+
const weights = [WR, WG, WB];
|
|
198
|
+
let axis = 0, spread = -1;
|
|
199
|
+
for (let k = 0; k < 3; k++) {
|
|
200
|
+
let min = Infinity, max = -Infinity;
|
|
201
|
+
for (let i = 0; i < entries; i++) {
|
|
202
|
+
min = Math.min(min, palette[i * 3 + k]);
|
|
203
|
+
max = Math.max(max, palette[i * 3 + k]);
|
|
204
|
+
}
|
|
205
|
+
if ((max - min) * weights[k] > spread) {
|
|
206
|
+
spread = (max - min) * weights[k];
|
|
207
|
+
axis = k;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const order = Int32Array.from({ length: entries }, (_, i) => i).sort((a, b) => palette[a * 3 + axis] - palette[b * 3 + axis]);
|
|
211
|
+
const keys = Float64Array.from(order, i => palette[i * 3 + axis]);
|
|
212
|
+
const weight = weights[axis];
|
|
213
|
+
return (r, g, b) => {
|
|
214
|
+
const key = axis === 0 ? r : axis === 1 ? g : b;
|
|
215
|
+
let best = 0, bestDistance = Infinity;
|
|
216
|
+
const test = (i) => {
|
|
217
|
+
const dr = r - palette[i * 3], dg = g - palette[i * 3 + 1], db = b - palette[i * 3 + 2];
|
|
218
|
+
const distance = WR * dr * dr + WG * dg * dg + WB * db * db;
|
|
219
|
+
if (distance < bestDistance || (distance === bestDistance && i < best)) {
|
|
220
|
+
bestDistance = distance;
|
|
221
|
+
best = i;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
let lo = 0, hi = entries;
|
|
225
|
+
while (lo < hi) {
|
|
226
|
+
const mid = lo + hi >> 1;
|
|
227
|
+
if (keys[mid] < key)
|
|
228
|
+
lo = mid + 1;
|
|
229
|
+
else
|
|
230
|
+
hi = mid;
|
|
231
|
+
}
|
|
232
|
+
for (let up = lo, down = lo - 1; up < entries || down >= 0;) {
|
|
233
|
+
if (up < entries) {
|
|
234
|
+
const d = key - keys[up];
|
|
235
|
+
if (weight * d * d > bestDistance)
|
|
236
|
+
up = entries;
|
|
237
|
+
else
|
|
238
|
+
test(order[up++]);
|
|
239
|
+
}
|
|
240
|
+
if (down >= 0) {
|
|
241
|
+
const d = key - keys[down];
|
|
242
|
+
if (weight * d * d > bestDistance)
|
|
243
|
+
down = -1;
|
|
244
|
+
else
|
|
245
|
+
test(order[down--]);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return best;
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function createLookup(palette, entries) {
|
|
252
|
+
const cache = new Int16Array(1 << 18).fill(-1);
|
|
253
|
+
const nearest = nearestSearch(palette, entries);
|
|
254
|
+
return (r, g, b) => {
|
|
255
|
+
const key = (r >> 2) << 12 | (g >> 2) << 6 | b >> 2;
|
|
256
|
+
let index = cache[key];
|
|
257
|
+
if (index === -1)
|
|
258
|
+
index = cache[key] = nearest(r, g, b);
|
|
259
|
+
return index;
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function ditherMap(pixels, width, palette, entries) {
|
|
263
|
+
const height = (pixels.length >> 2) / width;
|
|
264
|
+
const lookup = createLookup(palette, entries);
|
|
265
|
+
const indices = new Uint8Array(pixels.length >> 2);
|
|
266
|
+
let current = new Float32Array((width + 2) * 3);
|
|
267
|
+
let next = new Float32Array((width + 2) * 3);
|
|
268
|
+
const strength = 0.85;
|
|
269
|
+
for (let y = 0; y < height; y++) {
|
|
270
|
+
const reverse = y & 1;
|
|
271
|
+
for (let step = 0; step < width; step++) {
|
|
272
|
+
const x = reverse ? width - 1 - step : step;
|
|
273
|
+
const p = y * width + x;
|
|
274
|
+
const e = (x + 1) * 3;
|
|
275
|
+
if (pixels[p * 4 + 3] < 128)
|
|
276
|
+
continue;
|
|
277
|
+
const r = clamp(pixels[p * 4] + current[e] * strength);
|
|
278
|
+
const g = clamp(pixels[p * 4 + 1] + current[e + 1] * strength);
|
|
279
|
+
const b = clamp(pixels[p * 4 + 2] + current[e + 2] * strength);
|
|
280
|
+
const index = indices[p] = lookup(r, g, b);
|
|
281
|
+
const er = r - palette[index * 3];
|
|
282
|
+
const eg = g - palette[index * 3 + 1];
|
|
283
|
+
const eb = b - palette[index * 3 + 2];
|
|
284
|
+
const forward = reverse ? -3 : 3;
|
|
285
|
+
diffuse(current, next, e, forward, er);
|
|
286
|
+
diffuse(current, next, e + 1, forward, eg);
|
|
287
|
+
diffuse(current, next, e + 2, forward, eb);
|
|
288
|
+
}
|
|
289
|
+
[current, next] = [next, current];
|
|
290
|
+
next.fill(0);
|
|
291
|
+
}
|
|
292
|
+
return indices;
|
|
293
|
+
}
|
|
294
|
+
function diffuse(current, next, i, forward, error) {
|
|
295
|
+
current[i + forward] += error * 7 / 16;
|
|
296
|
+
next[i - forward] += error * 3 / 16;
|
|
297
|
+
next[i] += error * 5 / 16;
|
|
298
|
+
next[i + forward] += error / 16;
|
|
299
|
+
}
|
|
300
|
+
function clamp(v) {
|
|
301
|
+
return v < 0 ? 0 : v > 255 ? 255 : v | 0;
|
|
302
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the data looks like a TGA. Older TGAs have no signature, so this checks that the header is consistent
|
|
3
|
+
*/
|
|
4
|
+
export declare function isTGA(d: Uint8Array): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Decodes a TGA into RGBA
|
|
7
|
+
*/
|
|
8
|
+
export declare function decode(d: Uint8Array): {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
pixels: Uint8Array;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Encodes RGBA pixels into a run length encoded TGA (32 bit with alpha, 24 bit if opaque), top left origin
|
|
15
|
+
*/
|
|
16
|
+
export declare function encode(rgba: Uint8Array, width: number, height: number): Uint8Array;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const SIGNATURE = 'TRUEVISION-XFILE.\0';
|
|
2
|
+
function readHeader(d) {
|
|
3
|
+
if (d.length < 18)
|
|
4
|
+
return null;
|
|
5
|
+
return {
|
|
6
|
+
idLength: d[0],
|
|
7
|
+
colorMapType: d[1],
|
|
8
|
+
imageType: d[2],
|
|
9
|
+
mapFirst: d[3] | d[4] << 8,
|
|
10
|
+
mapLength: d[5] | d[6] << 8,
|
|
11
|
+
mapEntrySize: d[7],
|
|
12
|
+
width: d[12] | d[13] << 8,
|
|
13
|
+
height: d[14] | d[15] << 8,
|
|
14
|
+
depth: d[16],
|
|
15
|
+
alphaBits: d[17] & 15,
|
|
16
|
+
rightToLeft: (d[17] & 16) !== 0,
|
|
17
|
+
topToBottom: (d[17] & 32) !== 0
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function hasFooter(d) {
|
|
21
|
+
if (d.length < 44)
|
|
22
|
+
return false;
|
|
23
|
+
for (let i = 0; i < SIGNATURE.length; i++)
|
|
24
|
+
if (d[d.length - 18 + i] !== SIGNATURE.charCodeAt(i))
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
export function isTGA(d) {
|
|
29
|
+
const h = readHeader(d);
|
|
30
|
+
if (!h)
|
|
31
|
+
return false;
|
|
32
|
+
if (hasFooter(d))
|
|
33
|
+
return true;
|
|
34
|
+
const base = h.imageType & ~8;
|
|
35
|
+
if (h.colorMapType > 1 || ![1, 2, 3].includes(base) || h.imageType > 11 || (h.imageType & 4))
|
|
36
|
+
return false;
|
|
37
|
+
if (!h.width || !h.height || d[17] & 0xc0)
|
|
38
|
+
return false;
|
|
39
|
+
if (base === 1 && (h.colorMapType !== 1 || (h.depth !== 8 && h.depth !== 16)))
|
|
40
|
+
return false;
|
|
41
|
+
if (base === 2 && ![15, 16, 24, 32].includes(h.depth))
|
|
42
|
+
return false;
|
|
43
|
+
if (base === 3 && h.depth !== 8 && h.depth !== 16)
|
|
44
|
+
return false;
|
|
45
|
+
if (h.colorMapType === 1 && ![8, 15, 16, 24, 32].includes(h.mapEntrySize))
|
|
46
|
+
return false;
|
|
47
|
+
const mapBytes = h.colorMapType ? h.mapLength * Math.ceil(h.mapEntrySize / 8) : 0;
|
|
48
|
+
const dataStart = 18 + h.idLength + mapBytes;
|
|
49
|
+
const pixelBytes = h.width * h.height * Math.ceil(h.depth / 8);
|
|
50
|
+
return h.imageType & 8 ? d.length > dataStart : d.length >= dataStart + pixelBytes;
|
|
51
|
+
}
|
|
52
|
+
function readColor(d, o, bits, alphaBits, out, p) {
|
|
53
|
+
if (bits === 8) {
|
|
54
|
+
out[p] = out[p + 1] = out[p + 2] = d[o];
|
|
55
|
+
out[p + 3] = 255;
|
|
56
|
+
}
|
|
57
|
+
else if (bits === 15 || bits === 16) {
|
|
58
|
+
const v = d[o] | d[o + 1] << 8;
|
|
59
|
+
out[p] = (v >> 10 & 31) * 255 / 31 | 0;
|
|
60
|
+
out[p + 1] = (v >> 5 & 31) * 255 / 31 | 0;
|
|
61
|
+
out[p + 2] = (v & 31) * 255 / 31 | 0;
|
|
62
|
+
out[p + 3] = 255;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
out[p] = d[o + 2];
|
|
66
|
+
out[p + 1] = d[o + 1];
|
|
67
|
+
out[p + 2] = d[o];
|
|
68
|
+
out[p + 3] = bits === 32 && alphaBits ? d[o + 3] : 255;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function decode(d) {
|
|
72
|
+
const h = readHeader(d);
|
|
73
|
+
if (!h || !isTGA(d))
|
|
74
|
+
throw new Error('TGA: invalid header');
|
|
75
|
+
const { width, height, depth } = h;
|
|
76
|
+
const base = h.imageType & ~8, rle = (h.imageType & 8) !== 0;
|
|
77
|
+
const bytesPerPixel = Math.ceil(depth / 8);
|
|
78
|
+
let offset = 18 + h.idLength;
|
|
79
|
+
let palette = null;
|
|
80
|
+
if (h.colorMapType === 1) {
|
|
81
|
+
const entryBytes = Math.ceil(h.mapEntrySize / 8);
|
|
82
|
+
palette = new Uint8Array((h.mapFirst + h.mapLength) * 4);
|
|
83
|
+
for (let i = 0; i < h.mapLength; i++) {
|
|
84
|
+
if (offset + entryBytes > d.length)
|
|
85
|
+
throw new Error('TGA: truncated color map');
|
|
86
|
+
readColor(d, offset, h.mapEntrySize, h.alphaBits, palette, (h.mapFirst + i) * 4);
|
|
87
|
+
offset += entryBytes;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (base === 1 && !palette)
|
|
91
|
+
throw new Error('TGA: color mapped image without color map');
|
|
92
|
+
const count = width * height;
|
|
93
|
+
const stored = new Uint8Array(count * 4);
|
|
94
|
+
const pixel = (o, p) => {
|
|
95
|
+
if (base === 1) {
|
|
96
|
+
const index = depth === 16 ? d[o] | d[o + 1] << 8 : d[o];
|
|
97
|
+
const pal = palette;
|
|
98
|
+
if (index * 4 + 3 < pal.length)
|
|
99
|
+
stored.set(pal.subarray(index * 4, index * 4 + 4), p);
|
|
100
|
+
else
|
|
101
|
+
stored[p + 3] = 255;
|
|
102
|
+
}
|
|
103
|
+
else if (base === 3) {
|
|
104
|
+
stored[p] = stored[p + 1] = stored[p + 2] = d[o];
|
|
105
|
+
stored[p + 3] = depth === 16 ? d[o + 1] : 255;
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
readColor(d, o, depth, h.alphaBits, stored, p);
|
|
109
|
+
};
|
|
110
|
+
if (!rle) {
|
|
111
|
+
if (offset + count * bytesPerPixel > d.length)
|
|
112
|
+
throw new Error('TGA: truncated image data');
|
|
113
|
+
for (let i = 0; i < count; i++)
|
|
114
|
+
pixel(offset + i * bytesPerPixel, i * 4);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
for (let i = 0; i < count;) {
|
|
118
|
+
if (offset >= d.length)
|
|
119
|
+
throw new Error('TGA: truncated image data');
|
|
120
|
+
const header = d[offset++];
|
|
121
|
+
const n = (header & 127) + 1;
|
|
122
|
+
if (offset + (header & 128 ? 1 : n) * bytesPerPixel > d.length)
|
|
123
|
+
throw new Error('TGA: truncated image data');
|
|
124
|
+
if (header & 128) {
|
|
125
|
+
pixel(offset, i * 4);
|
|
126
|
+
for (let k = 1; k < n && i + k < count; k++)
|
|
127
|
+
stored.copyWithin((i + k) * 4, i * 4, i * 4 + 4);
|
|
128
|
+
offset += bytesPerPixel;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
for (let k = 0; k < n && i + k < count; k++)
|
|
132
|
+
pixel(offset + k * bytesPerPixel, (i + k) * 4);
|
|
133
|
+
offset += n * bytesPerPixel;
|
|
134
|
+
}
|
|
135
|
+
i += n;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!h.topToBottom || h.rightToLeft) {
|
|
139
|
+
const pixels = new Uint8Array(count * 4);
|
|
140
|
+
const src = new Uint32Array(stored.buffer), dst = new Uint32Array(pixels.buffer);
|
|
141
|
+
for (let y = 0; y < height; y++) {
|
|
142
|
+
const sy = h.topToBottom ? y : height - 1 - y;
|
|
143
|
+
for (let x = 0; x < width; x++)
|
|
144
|
+
dst[y * width + x] = src[sy * width + (h.rightToLeft ? width - 1 - x : x)];
|
|
145
|
+
}
|
|
146
|
+
return { width, height, pixels };
|
|
147
|
+
}
|
|
148
|
+
return { width, height, pixels: stored };
|
|
149
|
+
}
|
|
150
|
+
export function encode(rgba, width, height) {
|
|
151
|
+
if (width < 1 || height < 1 || width > 65535 || height > 65535)
|
|
152
|
+
throw new RangeError('TGA: dimensions must be 1-65535');
|
|
153
|
+
let opaque = true;
|
|
154
|
+
for (let i = 3; i < rgba.length; i += 4)
|
|
155
|
+
if (rgba[i] !== 255) {
|
|
156
|
+
opaque = false;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
const bpp = opaque ? 3 : 4;
|
|
160
|
+
const count = width * height;
|
|
161
|
+
if (rgba.byteOffset % 4)
|
|
162
|
+
rgba = rgba.slice(0, count * 4);
|
|
163
|
+
const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, count);
|
|
164
|
+
const out = new Uint8Array(18 + count * bpp + Math.ceil(count / 128) + 26);
|
|
165
|
+
out[2] = 10;
|
|
166
|
+
out[12] = width & 255;
|
|
167
|
+
out[13] = width >> 8;
|
|
168
|
+
out[14] = height & 255;
|
|
169
|
+
out[15] = height >> 8;
|
|
170
|
+
out[16] = bpp * 8;
|
|
171
|
+
out[17] = 32 | (opaque ? 0 : 8);
|
|
172
|
+
let o = 18;
|
|
173
|
+
const put = (i) => {
|
|
174
|
+
const p = i * 4;
|
|
175
|
+
out[o++] = rgba[p + 2];
|
|
176
|
+
out[o++] = rgba[p + 1];
|
|
177
|
+
out[o++] = rgba[p];
|
|
178
|
+
if (bpp === 4)
|
|
179
|
+
out[o++] = rgba[p + 3];
|
|
180
|
+
};
|
|
181
|
+
for (let i = 0; i < count;) {
|
|
182
|
+
let run = 1;
|
|
183
|
+
while (run < 128 && i + run < count && pixels[i + run] === pixels[i])
|
|
184
|
+
run++;
|
|
185
|
+
if (run >= 2) {
|
|
186
|
+
out[o++] = 128 | run - 1;
|
|
187
|
+
put(i);
|
|
188
|
+
i += run;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
let raw = 1;
|
|
192
|
+
while (raw < 128 && i + raw < count && !(i + raw + 1 < count && pixels[i + raw] === pixels[i + raw + 1]))
|
|
193
|
+
raw++;
|
|
194
|
+
out[o++] = raw - 1;
|
|
195
|
+
for (let k = 0; k < raw; k++)
|
|
196
|
+
put(i + k);
|
|
197
|
+
i += raw;
|
|
198
|
+
}
|
|
199
|
+
o += 8;
|
|
200
|
+
for (let i = 0; i < SIGNATURE.length; i++)
|
|
201
|
+
out[o++] = SIGNATURE.charCodeAt(i);
|
|
202
|
+
return out.slice(0, o);
|
|
203
|
+
}
|