avatarsniff 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,834 @@
1
+ 'use strict';
2
+
3
+ var chunkB5TUEKW2_cjs = require('./chunk-B5TUEKW2.cjs');
4
+ require('./chunk-ESH45ZBG.cjs');
5
+
6
+ // src/detectors.ts
7
+ function clamp01(value) {
8
+ return value < 0 ? 0 : value > 1 ? 1 : value;
9
+ }
10
+ var initials = (f, o) => {
11
+ if (f.isLightBackground || f.isDarkBackground) {
12
+ return null;
13
+ }
14
+ if (f.dominantChroma < 24) {
15
+ return null;
16
+ }
17
+ if (f.coloredOtherFraction > o.maxColoredContent) {
18
+ return null;
19
+ }
20
+ if (f.glyphFraction < o.minGlyphFraction || f.dominantFraction < o.minDominantFraction || f.significantColors > o.maxSignificantColors) {
21
+ return null;
22
+ }
23
+ const dominanceScore = clamp01(
24
+ (f.dominantFraction - o.minDominantFraction) / (1 - o.minDominantFraction)
25
+ );
26
+ const simplicityScore = clamp01(
27
+ (o.maxSignificantColors + 1 - f.significantColors) / o.maxSignificantColors
28
+ );
29
+ const score = clamp01(dominanceScore * 0.6 + simplicityScore * 0.4);
30
+ if (score < o.scoreThreshold) {
31
+ return null;
32
+ }
33
+ return {
34
+ name: "initials",
35
+ score,
36
+ reason: `coloured background (${(f.dominantFraction * 100).toFixed(0)}% dominant) with a ${(f.glyphFraction * 100).toFixed(1)}% white glyph`
37
+ };
38
+ };
39
+ var solidColor = (f, o) => {
40
+ if (f.isLightBackground || f.isDarkBackground) {
41
+ return null;
42
+ }
43
+ if (f.dominantChroma < 24) {
44
+ return null;
45
+ }
46
+ if (f.dominantFraction < o.solidMinDominantFraction || f.significantColors > 1 || f.glyphFraction >= o.minGlyphFraction) {
47
+ return null;
48
+ }
49
+ return {
50
+ name: "solidColor",
51
+ score: clamp01(f.dominantFraction),
52
+ reason: `flat ${(f.dominantFraction * 100).toFixed(0)}% solid colour with no glyph`
53
+ };
54
+ };
55
+ var personIcon = (f, o) => {
56
+ if (f.meanChroma > o.maxGrayChroma) {
57
+ return null;
58
+ }
59
+ if (f.symmetry < o.minSymmetry) {
60
+ return null;
61
+ }
62
+ const [r, g, b] = f.dominantRgb;
63
+ const lightGreyBackground = Math.min(r, g, b) >= 150 && !f.isLightBackground && !f.isDarkBackground;
64
+ if (!lightGreyBackground) {
65
+ return null;
66
+ }
67
+ if (f.glyphFraction < o.minPersonFigureFraction) {
68
+ return null;
69
+ }
70
+ return {
71
+ name: "personIcon",
72
+ score: clamp01((f.symmetry - o.minSymmetry) / (1 - o.minSymmetry)),
73
+ reason: `grey person silhouette (${(f.glyphFraction * 100).toFixed(0)}% figure) on a light background`
74
+ };
75
+ };
76
+ var identicon = (f, o) => {
77
+ if (f.symmetry < o.identiconMinSymmetry) {
78
+ return null;
79
+ }
80
+ if (f.significantColors > o.identiconMaxColors) {
81
+ return null;
82
+ }
83
+ const foreground = 1 - f.dominantFraction;
84
+ if (foreground < o.identiconMinForeground) {
85
+ return null;
86
+ }
87
+ return {
88
+ name: "identicon",
89
+ score: clamp01((f.symmetry - o.identiconMinSymmetry) / (1 - o.identiconMinSymmetry)),
90
+ reason: `symmetric pattern (${(f.symmetry * 100).toFixed(0)}% mirror, ${(foreground * 100).toFixed(0)}% foreground)`
91
+ };
92
+ };
93
+ var DETECTORS = [
94
+ { name: "initials", run: initials },
95
+ { name: "solidColor", run: solidColor },
96
+ { name: "personIcon", run: personIcon },
97
+ { name: "identicon", run: identicon }
98
+ ];
99
+
100
+ // src/features.ts
101
+ function sampleRgb(image, size) {
102
+ const { data, width, height } = image;
103
+ const channels = image.channels ?? Math.round(data.length / (width * height));
104
+ const out = new Uint8Array(size * size * 3);
105
+ for (let sy = 0; sy < size; sy++) {
106
+ const srcY = Math.min(height - 1, Math.floor((sy + 0.5) * height / size));
107
+ for (let sx = 0; sx < size; sx++) {
108
+ const srcX = Math.min(width - 1, Math.floor((sx + 0.5) * width / size));
109
+ const si = (srcY * width + srcX) * channels;
110
+ let r = data[si];
111
+ let g = data[si + 1];
112
+ let b = data[si + 2];
113
+ if (channels === 4) {
114
+ const alpha = data[si + 3] / 255;
115
+ r = Math.round(r * alpha + 255 * (1 - alpha));
116
+ g = Math.round(g * alpha + 255 * (1 - alpha));
117
+ b = Math.round(b * alpha + 255 * (1 - alpha));
118
+ }
119
+ const di = (sy * size + sx) * 3;
120
+ out[di] = r;
121
+ out[di + 1] = g;
122
+ out[di + 2] = b;
123
+ }
124
+ }
125
+ return out;
126
+ }
127
+ function extractFeatures(image, opts) {
128
+ const { width, height } = image;
129
+ const channels = image.channels ?? Math.round(image.data.length / (width * height));
130
+ if (!(width && height) || channels < 3) {
131
+ return null;
132
+ }
133
+ const size = opts.sampleSize;
134
+ const totalPixels = size * size;
135
+ const pixels = sampleRgb(image, size);
136
+ const levels = 2 ** opts.quantizeBits;
137
+ const divisor = 256 / levels;
138
+ const counts = /* @__PURE__ */ new Map();
139
+ let nearWhitePixels = 0;
140
+ let chromaSum = 0;
141
+ for (let i = 0; i < pixels.length; i += 3) {
142
+ const r = pixels[i];
143
+ const g = pixels[i + 1];
144
+ const b = pixels[i + 2];
145
+ const max = Math.max(r, g, b);
146
+ const min = Math.min(r, g, b);
147
+ chromaSum += max - min;
148
+ if (min >= opts.glyphWhiteCutoff) {
149
+ nearWhitePixels += 1;
150
+ }
151
+ const key = Math.floor(r / divisor) * levels * levels + Math.floor(g / divisor) * levels + Math.floor(b / divisor);
152
+ counts.set(key, (counts.get(key) ?? 0) + 1);
153
+ }
154
+ let dominantCount = 0;
155
+ let dominantKey = 0;
156
+ let significantColors = 0;
157
+ const noiseThreshold = opts.noiseFloor * totalPixels;
158
+ for (const [key, count] of counts) {
159
+ if (count > dominantCount) {
160
+ dominantCount = count;
161
+ dominantKey = key;
162
+ }
163
+ if (count >= noiseThreshold) {
164
+ significantColors += 1;
165
+ }
166
+ }
167
+ const dominantB = dominantKey % levels;
168
+ const dominantG = Math.floor(dominantKey / levels) % levels;
169
+ const dominantR = Math.floor(dominantKey / (levels * levels)) % levels;
170
+ const bucketCenter = (bucket) => (bucket + 0.5) * divisor;
171
+ const dominantRgb = [dominantR, dominantG, dominantB].map(bucketCenter);
172
+ let coloredOtherPixels = 0;
173
+ for (let i = 0; i < pixels.length; i += 3) {
174
+ const r = pixels[i];
175
+ const g = pixels[i + 1];
176
+ const b = pixels[i + 2];
177
+ if (Math.min(r, g, b) >= opts.glyphWhiteCutoff) {
178
+ continue;
179
+ }
180
+ const nearDominant = Math.abs(Math.floor(r / divisor) - dominantR) <= 1 && Math.abs(Math.floor(g / divisor) - dominantG) <= 1 && Math.abs(Math.floor(b / divisor) - dominantB) <= 1;
181
+ if (!nearDominant) {
182
+ coloredOtherPixels += 1;
183
+ }
184
+ }
185
+ let symMatches = 0;
186
+ const tol = 36;
187
+ for (let y = 0; y < size; y++) {
188
+ for (let x = 0; x < size; x++) {
189
+ const i = (y * size + x) * 3;
190
+ const mi = (y * size + (size - 1 - x)) * 3;
191
+ const diff = Math.abs(pixels[i] - pixels[mi]) + Math.abs(pixels[i + 1] - pixels[mi + 1]) + Math.abs(pixels[i + 2] - pixels[mi + 2]);
192
+ if (diff <= tol) {
193
+ symMatches += 1;
194
+ }
195
+ }
196
+ }
197
+ return {
198
+ size,
199
+ totalPixels,
200
+ pixels,
201
+ dominantFraction: dominantCount / totalPixels,
202
+ dominantRgb,
203
+ dominantChroma: Math.max(...dominantRgb) - Math.min(...dominantRgb),
204
+ dominantBucket: [dominantR, dominantG, dominantB],
205
+ significantColors,
206
+ glyphFraction: nearWhitePixels / totalPixels,
207
+ coloredOtherFraction: coloredOtherPixels / totalPixels,
208
+ meanChroma: chromaSum / totalPixels,
209
+ symmetry: symMatches / totalPixels,
210
+ isLightBackground: dominantRgb.every((c) => c >= opts.whiteCutoff),
211
+ isDarkBackground: dominantRgb.every((c) => c <= opts.blackCutoff),
212
+ levels,
213
+ divisor
214
+ };
215
+ }
216
+
217
+ // src/analyze.ts
218
+ var DEFAULTS = {
219
+ sampleSize: 48,
220
+ quantizeBits: 4,
221
+ noiseFloor: 0.02,
222
+ minDominantFraction: 0.55,
223
+ maxSignificantColors: 4,
224
+ scoreThreshold: 0.6,
225
+ whiteCutoff: 224,
226
+ blackCutoff: 40,
227
+ glyphWhiteCutoff: 200,
228
+ minGlyphFraction: 8e-3,
229
+ maxColoredContent: 0.06,
230
+ solidMinDominantFraction: 0.92,
231
+ minSymmetry: 0.9,
232
+ maxGrayChroma: 18,
233
+ minPersonFigureFraction: 0.1,
234
+ identiconMinSymmetry: 0.92,
235
+ identiconMinForeground: 0.12,
236
+ identiconMaxColors: 6
237
+ // maxBytes is a decode-layer concern, not used by the pixel analysis.
238
+ };
239
+ function notDefault(reason) {
240
+ return {
241
+ isDefault: false,
242
+ matched: null,
243
+ score: 0,
244
+ dominantFraction: 0,
245
+ significantColors: 0,
246
+ glyphFraction: 0,
247
+ coloredOtherFraction: 0,
248
+ reason
249
+ };
250
+ }
251
+ function rejectionReason(f, o) {
252
+ if (f.isLightBackground) {
253
+ return "near-white background - a logo/drawing/photo, not a default";
254
+ }
255
+ if (f.isDarkBackground) {
256
+ return "near-black background - a dark photo/icon, not a default";
257
+ }
258
+ if (f.coloredOtherFraction > o.maxColoredContent) {
259
+ return `${(f.coloredOtherFraction * 100).toFixed(1)}% coloured content - a photo/illustration, not a default`;
260
+ }
261
+ if (f.glyphFraction < o.minGlyphFraction && f.dominantFraction < o.solidMinDominantFraction) {
262
+ return `no glyph (${(f.glyphFraction * 100).toFixed(1)}%) and not a flat block - not a default`;
263
+ }
264
+ return `not a recognised default (${(f.dominantFraction * 100).toFixed(0)}% dominant, ${f.significantColors} significant colours, ${(f.symmetry * 100).toFixed(0)}% symmetry)`;
265
+ }
266
+ function analyzeImage(image, options = {}) {
267
+ const opts = { ...DEFAULTS, ...options };
268
+ const features = extractFeatures(image, opts);
269
+ if (!features) {
270
+ return notDefault("empty or non-RGB image");
271
+ }
272
+ const thresholds = opts;
273
+ const toggles = options.detect ?? {};
274
+ const base = {
275
+ dominantFraction: features.dominantFraction,
276
+ significantColors: features.significantColors,
277
+ glyphFraction: features.glyphFraction,
278
+ coloredOtherFraction: features.coloredOtherFraction
279
+ };
280
+ for (const { name, run } of DETECTORS) {
281
+ if (toggles[name] === false) {
282
+ continue;
283
+ }
284
+ const match = run(features, thresholds);
285
+ if (match) {
286
+ return {
287
+ isDefault: true,
288
+ matched: match.name,
289
+ score: match.score,
290
+ reason: match.reason,
291
+ ...base
292
+ };
293
+ }
294
+ }
295
+ return {
296
+ isDefault: false,
297
+ matched: null,
298
+ score: 0,
299
+ reason: rejectionReason(features, opts),
300
+ ...base
301
+ };
302
+ }
303
+
304
+ // src/decode.ts
305
+ var DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
306
+ function sniffFormat(bytes) {
307
+ if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80) {
308
+ return "png";
309
+ }
310
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
311
+ return "jpeg";
312
+ }
313
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) {
314
+ return "gif";
315
+ }
316
+ if (bytes.length >= 12 && bytes[0] === 82 && // R
317
+ bytes[1] === 73 && // I
318
+ bytes[2] === 70 && // F
319
+ bytes[8] === 87 && // W
320
+ bytes[9] === 69 && // E
321
+ bytes[10] === 66 && // B
322
+ bytes[11] === 80) {
323
+ return "webp";
324
+ }
325
+ let i = 0;
326
+ while (i < bytes.length && bytes[i] <= 32) {
327
+ i++;
328
+ }
329
+ const head = String.fromCharCode(...bytes.subarray(i, i + 5)).toLowerCase();
330
+ if (head.startsWith("<?xml") || head.startsWith("<svg")) {
331
+ return "svg";
332
+ }
333
+ return "unknown";
334
+ }
335
+ function readUint32(bytes, offset) {
336
+ return bytes[offset] * 16777216 + (bytes[offset + 1] << 16) + (bytes[offset + 2] << 8) + bytes[offset + 3];
337
+ }
338
+ async function inflate(data) {
339
+ const stream = new Response(data).body?.pipeThrough(
340
+ new DecompressionStream("deflate")
341
+ );
342
+ if (!stream) {
343
+ throw new Error("no readable stream");
344
+ }
345
+ return new Uint8Array(await new Response(stream).arrayBuffer());
346
+ }
347
+ function paeth(a, b, c) {
348
+ const p = a + b - c;
349
+ const pa = Math.abs(p - a);
350
+ const pb = Math.abs(p - b);
351
+ const pc = Math.abs(p - c);
352
+ if (pa <= pb && pa <= pc) {
353
+ return a;
354
+ }
355
+ return pb <= pc ? b : c;
356
+ }
357
+ var PNG_CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
358
+ async function decodePng(bytes) {
359
+ let pos = 8;
360
+ let width = 0;
361
+ let height = 0;
362
+ let bitDepth = 0;
363
+ let colorType = 0;
364
+ let interlace = 0;
365
+ let palette = null;
366
+ let transparency = null;
367
+ const idat = [];
368
+ while (pos + 8 <= bytes.length) {
369
+ const length = readUint32(bytes, pos);
370
+ pos += 4;
371
+ const type = String.fromCharCode(
372
+ bytes[pos],
373
+ bytes[pos + 1],
374
+ bytes[pos + 2],
375
+ bytes[pos + 3]
376
+ );
377
+ pos += 4;
378
+ const data = bytes.subarray(pos, pos + length);
379
+ pos += length + 4;
380
+ if (type === "IHDR") {
381
+ width = readUint32(data, 0);
382
+ height = readUint32(data, 4);
383
+ bitDepth = data[8];
384
+ colorType = data[9];
385
+ interlace = data[12];
386
+ } else if (type === "PLTE") {
387
+ palette = data;
388
+ } else if (type === "tRNS") {
389
+ transparency = data;
390
+ } else if (type === "IDAT") {
391
+ idat.push(data);
392
+ } else if (type === "IEND") {
393
+ break;
394
+ }
395
+ }
396
+ const srcChannels = PNG_CHANNELS[colorType];
397
+ if (!(width && height && srcChannels) || bitDepth !== 8 || interlace !== 0) {
398
+ return null;
399
+ }
400
+ let total = 0;
401
+ for (const c of idat) {
402
+ total += c.length;
403
+ }
404
+ const compressed = new Uint8Array(total);
405
+ let w = 0;
406
+ for (const c of idat) {
407
+ compressed.set(c, w);
408
+ w += c.length;
409
+ }
410
+ const raw = await inflate(compressed);
411
+ const stride = width * srcChannels;
412
+ const out = new Uint8Array(height * stride);
413
+ let rp = 0;
414
+ for (let y = 0; y < height; y++) {
415
+ const filter = raw[rp++];
416
+ for (let x = 0; x < stride; x++) {
417
+ const cur = raw[rp++];
418
+ const a = x >= srcChannels ? out[y * stride + x - srcChannels] : 0;
419
+ const b = y > 0 ? out[(y - 1) * stride + x] : 0;
420
+ const c = x >= srcChannels && y > 0 ? out[(y - 1) * stride + x - srcChannels] : 0;
421
+ let v = cur;
422
+ if (filter === 1) {
423
+ v = cur + a;
424
+ } else if (filter === 2) {
425
+ v = cur + b;
426
+ } else if (filter === 3) {
427
+ v = cur + (a + b >> 1);
428
+ } else if (filter === 4) {
429
+ v = cur + paeth(a, b, c);
430
+ }
431
+ out[y * stride + x] = v & 255;
432
+ }
433
+ }
434
+ const rgba = new Uint8Array(width * height * 4);
435
+ for (let i = 0; i < width * height; i++) {
436
+ const s = i * srcChannels;
437
+ let r = 0;
438
+ let g = 0;
439
+ let b = 0;
440
+ let alpha = 255;
441
+ if (colorType === 0) {
442
+ r = g = b = out[s];
443
+ } else if (colorType === 2) {
444
+ r = out[s];
445
+ g = out[s + 1];
446
+ b = out[s + 2];
447
+ } else if (colorType === 4) {
448
+ r = g = b = out[s];
449
+ alpha = out[s + 1];
450
+ } else if (colorType === 6) {
451
+ r = out[s];
452
+ g = out[s + 1];
453
+ b = out[s + 2];
454
+ alpha = out[s + 3];
455
+ } else if (colorType === 3) {
456
+ if (!palette) {
457
+ return null;
458
+ }
459
+ const idx = out[s];
460
+ r = palette[idx * 3];
461
+ g = palette[idx * 3 + 1];
462
+ b = palette[idx * 3 + 2];
463
+ if (transparency && idx < transparency.length) {
464
+ alpha = transparency[idx];
465
+ }
466
+ }
467
+ rgba[i * 4] = r;
468
+ rgba[i * 4 + 1] = g;
469
+ rgba[i * 4 + 2] = b;
470
+ rgba[i * 4 + 3] = alpha;
471
+ }
472
+ return { data: rgba, width, height, channels: 4 };
473
+ }
474
+ function lzwDecode(data, minCodeSize, pixelCount) {
475
+ const out = new Uint8Array(pixelCount);
476
+ let outPos = 0;
477
+ const clearCode = 1 << minCodeSize;
478
+ const eoiCode = clearCode + 1;
479
+ let codeSize = minCodeSize + 1;
480
+ let dict = [];
481
+ const reset = () => {
482
+ dict = [];
483
+ for (let i = 0; i < clearCode; i++) {
484
+ dict[i] = [i];
485
+ }
486
+ dict[clearCode] = [];
487
+ dict[eoiCode] = [];
488
+ codeSize = minCodeSize + 1;
489
+ };
490
+ reset();
491
+ let bitBuffer = 0;
492
+ let bitCount = 0;
493
+ let dataPos = 0;
494
+ let prev = null;
495
+ const readCode = () => {
496
+ while (bitCount < codeSize) {
497
+ if (dataPos >= data.length) {
498
+ return eoiCode;
499
+ }
500
+ bitBuffer |= data[dataPos++] << bitCount;
501
+ bitCount += 8;
502
+ }
503
+ const code = bitBuffer & (1 << codeSize) - 1;
504
+ bitBuffer >>= codeSize;
505
+ bitCount -= codeSize;
506
+ return code;
507
+ };
508
+ for (; ; ) {
509
+ const code = readCode();
510
+ if (code === eoiCode) {
511
+ break;
512
+ }
513
+ if (code === clearCode) {
514
+ reset();
515
+ prev = null;
516
+ continue;
517
+ }
518
+ let entry;
519
+ if (dict[code]) {
520
+ entry = dict[code];
521
+ } else if (prev) {
522
+ entry = [...prev, prev[0]];
523
+ } else {
524
+ break;
525
+ }
526
+ for (const value of entry) {
527
+ if (outPos < pixelCount) {
528
+ out[outPos++] = value;
529
+ }
530
+ }
531
+ if (prev) {
532
+ dict.push([...prev, entry[0]]);
533
+ if (dict.length === (1 << codeSize) - 1 && codeSize < 12) {
534
+ codeSize++;
535
+ }
536
+ }
537
+ prev = entry;
538
+ }
539
+ return out;
540
+ }
541
+ function deinterlace(pixels, width, height) {
542
+ const result = new Uint8Array(pixels.length);
543
+ const passes = [
544
+ [0, 8],
545
+ [4, 8],
546
+ [2, 4],
547
+ [1, 2]
548
+ ];
549
+ let srcRow = 0;
550
+ for (const [start, step] of passes) {
551
+ for (let row = start; row < height; row += step) {
552
+ result.set(
553
+ pixels.subarray(srcRow * width * 4, (srcRow + 1) * width * 4),
554
+ row * width * 4
555
+ );
556
+ srcRow++;
557
+ }
558
+ }
559
+ pixels.set(result);
560
+ }
561
+ function readUint16LE(bytes, offset) {
562
+ return bytes[offset] | bytes[offset + 1] << 8;
563
+ }
564
+ function decodeGif(bytes) {
565
+ let p = 6;
566
+ const packed = bytes[p + 4];
567
+ const gctFlag = (packed & 128) !== 0;
568
+ const gctSize = 2 << (packed & 7);
569
+ p += 7;
570
+ let gct = null;
571
+ if (gctFlag) {
572
+ gct = bytes.subarray(p, p + gctSize * 3);
573
+ p += gctSize * 3;
574
+ }
575
+ let transparentIndex = -1;
576
+ while (p < bytes.length) {
577
+ const block = bytes[p++];
578
+ if (block === 59) {
579
+ break;
580
+ }
581
+ if (block === 33) {
582
+ const label = bytes[p++];
583
+ if (label === 249) {
584
+ const size = bytes[p++];
585
+ const flags = bytes[p];
586
+ transparentIndex = flags & 1 ? bytes[p + 3] : -1;
587
+ p += size;
588
+ p++;
589
+ } else {
590
+ let size = bytes[p++];
591
+ while (size !== 0) {
592
+ p += size;
593
+ size = bytes[p++];
594
+ }
595
+ }
596
+ } else if (block === 44) {
597
+ const iw = readUint16LE(bytes, p + 4);
598
+ const ih = readUint16LE(bytes, p + 6);
599
+ const ipacked = bytes[p + 8];
600
+ p += 9;
601
+ const lctFlag = (ipacked & 128) !== 0;
602
+ const interlaced = (ipacked & 64) !== 0;
603
+ const lctSize = 2 << (ipacked & 7);
604
+ let ct = gct;
605
+ if (lctFlag) {
606
+ ct = bytes.subarray(p, p + lctSize * 3);
607
+ p += lctSize * 3;
608
+ }
609
+ if (!ct) {
610
+ return null;
611
+ }
612
+ const minCodeSize = bytes[p++];
613
+ const data = [];
614
+ let size = bytes[p++];
615
+ while (size !== 0) {
616
+ for (let i = 0; i < size; i++) {
617
+ data.push(bytes[p + i]);
618
+ }
619
+ p += size;
620
+ size = bytes[p++];
621
+ }
622
+ const indices = lzwDecode(new Uint8Array(data), minCodeSize, iw * ih);
623
+ const rgba = new Uint8Array(iw * ih * 4);
624
+ for (let i = 0; i < iw * ih; i++) {
625
+ const idx = indices[i];
626
+ if (idx === transparentIndex) {
627
+ rgba[i * 4] = 255;
628
+ rgba[i * 4 + 1] = 255;
629
+ rgba[i * 4 + 2] = 255;
630
+ rgba[i * 4 + 3] = 0;
631
+ continue;
632
+ }
633
+ rgba[i * 4] = ct[idx * 3];
634
+ rgba[i * 4 + 1] = ct[idx * 3 + 1];
635
+ rgba[i * 4 + 2] = ct[idx * 3 + 2];
636
+ rgba[i * 4 + 3] = 255;
637
+ }
638
+ if (interlaced) {
639
+ deinterlace(rgba, iw, ih);
640
+ }
641
+ return { data: rgba, width: iw, height: ih, channels: 4 };
642
+ } else {
643
+ break;
644
+ }
645
+ }
646
+ return null;
647
+ }
648
+ async function decodeJpeg(bytes) {
649
+ try {
650
+ const jpeg = (await import('./jpeg-js-H7NYMOHI.cjs')).default;
651
+ const decoded = jpeg.decode(bytes, {
652
+ useTArray: true,
653
+ maxMemoryUsageInMB: 512
654
+ });
655
+ return {
656
+ data: decoded.data,
657
+ width: decoded.width,
658
+ height: decoded.height,
659
+ channels: 4
660
+ };
661
+ } catch {
662
+ return null;
663
+ }
664
+ }
665
+ function getCanvas(width, height) {
666
+ if (typeof OffscreenCanvas !== "undefined") {
667
+ const ctx = new OffscreenCanvas(width, height).getContext("2d");
668
+ return ctx ? { ctx } : null;
669
+ }
670
+ if (typeof document !== "undefined") {
671
+ const canvas = document.createElement("canvas");
672
+ canvas.width = width;
673
+ canvas.height = height;
674
+ const ctx = canvas.getContext("2d");
675
+ return ctx ? { ctx } : null;
676
+ }
677
+ return null;
678
+ }
679
+ function loadSvgImage(bytes) {
680
+ if (typeof document === "undefined" || typeof Image === "undefined") {
681
+ return null;
682
+ }
683
+ const url = URL.createObjectURL(
684
+ new Blob([bytes], { type: "image/svg+xml" })
685
+ );
686
+ return new Promise((resolve, reject) => {
687
+ const img = new Image();
688
+ img.onload = () => {
689
+ URL.revokeObjectURL(url);
690
+ resolve(img);
691
+ };
692
+ img.onerror = () => {
693
+ URL.revokeObjectURL(url);
694
+ reject(new Error("svg load failed"));
695
+ };
696
+ img.src = url;
697
+ });
698
+ }
699
+ async function decodeNative(bytes, format) {
700
+ try {
701
+ let width;
702
+ let height;
703
+ let source;
704
+ if (format === "svg") {
705
+ const pending = loadSvgImage(bytes);
706
+ if (!pending) {
707
+ return null;
708
+ }
709
+ const img = await pending;
710
+ width = img.naturalWidth || img.width || 64;
711
+ height = img.naturalHeight || img.height || 64;
712
+ source = img;
713
+ } else if (typeof createImageBitmap === "function") {
714
+ const bitmap = await createImageBitmap(new Blob([bytes]));
715
+ width = bitmap.width;
716
+ height = bitmap.height;
717
+ source = bitmap;
718
+ } else {
719
+ return null;
720
+ }
721
+ const canvas = getCanvas(width, height);
722
+ if (!canvas) {
723
+ return null;
724
+ }
725
+ canvas.ctx.drawImage(source, 0, 0, width, height);
726
+ const imageData = canvas.ctx.getImageData(0, 0, width, height);
727
+ return {
728
+ data: imageData.data,
729
+ width: imageData.width,
730
+ height: imageData.height,
731
+ channels: 4
732
+ };
733
+ } catch {
734
+ return null;
735
+ }
736
+ }
737
+ async function decodeImage(bytes, options = {}) {
738
+ const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
739
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
740
+ if (data.length === 0 || data.length > maxBytes) {
741
+ return null;
742
+ }
743
+ const format = sniffFormat(data);
744
+ try {
745
+ const native = await decodeNative(data, format);
746
+ if (native) {
747
+ return native;
748
+ }
749
+ if (format === "png") {
750
+ return await decodePng(data);
751
+ }
752
+ if (format === "gif") {
753
+ return decodeGif(data);
754
+ }
755
+ if (format === "jpeg") {
756
+ return await decodeJpeg(data);
757
+ }
758
+ const registered = chunkB5TUEKW2_cjs.getDecoder(format);
759
+ if (registered) {
760
+ return await registered(data);
761
+ }
762
+ return null;
763
+ } catch {
764
+ return null;
765
+ }
766
+ }
767
+
768
+ // src/sniff.ts
769
+ function notDefault2(reason) {
770
+ return {
771
+ isDefault: false,
772
+ matched: null,
773
+ score: 0,
774
+ dominantFraction: 0,
775
+ significantColors: 0,
776
+ glyphFraction: 0,
777
+ coloredOtherFraction: 0,
778
+ reason
779
+ };
780
+ }
781
+ async function sniff(input, options = {}) {
782
+ if (input == null) {
783
+ return null;
784
+ }
785
+ if (typeof input === "string") {
786
+ try {
787
+ const response = await fetch(input);
788
+ if (!response.ok) {
789
+ return null;
790
+ }
791
+ return await sniff(new Uint8Array(await response.arrayBuffer()), options);
792
+ } catch {
793
+ return null;
794
+ }
795
+ }
796
+ if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
797
+ const image = await decodeImage(input, options);
798
+ if (!image) {
799
+ return notDefault2(
800
+ "could not decode image (unsupported format in this runtime)"
801
+ );
802
+ }
803
+ return analyzeImage(image, options);
804
+ }
805
+ const pixels = input;
806
+ return analyzeImage(
807
+ {
808
+ data: pixels.data,
809
+ width: pixels.width,
810
+ height: pixels.height,
811
+ channels: pixels.channels
812
+ },
813
+ options
814
+ );
815
+ }
816
+
817
+ // src/types.ts
818
+ var DETECTOR_NAMES = [
819
+ "initials",
820
+ "solidColor",
821
+ "personIcon",
822
+ "identicon"
823
+ ];
824
+
825
+ Object.defineProperty(exports, "registerDecoder", {
826
+ enumerable: true,
827
+ get: function () { return chunkB5TUEKW2_cjs.registerDecoder; }
828
+ });
829
+ exports.DEFAULT_MAX_BYTES = DEFAULT_MAX_BYTES;
830
+ exports.DETECTOR_NAMES = DETECTOR_NAMES;
831
+ exports.analyzeImage = analyzeImage;
832
+ exports.decodeImage = decodeImage;
833
+ exports.sniff = sniff;
834
+ exports.sniffFormat = sniffFormat;