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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tunç Türkmen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # avatarsniff
2
+
3
+ [![npm version](https://img.shields.io/npm/v/avatarsniff.svg)](https://www.npmjs.com/package/avatarsniff)
4
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/avatarsniff.svg)](https://bundlephobia.com/package/avatarsniff)
5
+ [![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](https://www.npmjs.com/package/avatarsniff?activeTab=dependencies)
6
+ [![types included](https://img.shields.io/npm/types/avatarsniff.svg)](https://www.npmjs.com/package/avatarsniff)
7
+ [![license](https://img.shields.io/npm/l/avatarsniff.svg)](./LICENSE)
8
+
9
+ Sniff out generic/default provider avatars - Google's initial-on-colour, flat
10
+ solid-colour placeholders, the Gravatar mystery-person silhouette, GitHub/Gravatar
11
+ identicons - straight from image pixels.
12
+
13
+ A lot of users never set a profile picture, so providers serve a boring,
14
+ auto-generated default (a letter on a coloured square). `avatarsniff` detects
15
+ those so you can replace them with something better (your own generated avatar,
16
+ an upload prompt, etc.) instead of showing the generic one.
17
+
18
+ - **Batteries included, zero dependencies.** Decodes **PNG, JPEG, GIF, WEBP and
19
+ SVG** (up to 10MB) with no native binaries and no install-time deps - every
20
+ decoder is bundled into the build. PNG/GIF/JPEG are in the core; WEBP and SVG
21
+ are opt-in subpaths (so their wasm only loads if you need them).
22
+ - **Server and client.** Same API in the browser, Node, Deno, Bun, edge
23
+ runtimes and workers.
24
+ - **Framework-agnostic.** No React/Vue/Angular - just install and import.
25
+ - **Four structural detectors** - `initials`, `solidColor`, `personIcon`,
26
+ `identicon`. Each keys on shape (flat colour, a white glyph, mirror symmetry),
27
+ never on a hard-coded palette, so they keep working as providers add colours.
28
+ Every family is on by default; opt out per call.
29
+
30
+ ### Decoding matrix
31
+
32
+ | Format | Browser / Deno / Bun / worker | Node (no canvas) |
33
+ | ------ | ----------------------------- | ---------------- |
34
+ | PNG | ✅ native or built-in | ✅ built-in (pure JS) |
35
+ | GIF | ✅ native or built-in | ✅ built-in (pure JS) |
36
+ | JPEG | ✅ native or built-in | ✅ built-in (bundled `jpeg-js`) |
37
+ | WEBP | ✅ native | ✅ `import "avatarsniff/webp"` |
38
+ | SVG | ✅ rasterised | ✅ `import "avatarsniff/svg"` |
39
+
40
+ Every decoder is inlined into the build, so installing `avatarsniff` pulls
41
+ **zero** dependencies. PNG/GIF/JPEG are in the core. WEBP and SVG ship as opt-in
42
+ subpaths so their wasm (~228KB and ~3MB) only loads if you import them - the
43
+ core stays tiny.
44
+
45
+ ## Install
46
+
47
+ ```sh
48
+ npm install avatarsniff
49
+ pnpm add avatarsniff
50
+ yarn add avatarsniff
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ One entry point - `sniff` - figures out what you passed it. Always async.
56
+
57
+ ### From image bytes or a URL (batteries included) - server or client
58
+
59
+ ```ts
60
+ import { sniff } from "avatarsniff";
61
+
62
+ const result = await sniff(pngOrGifBytes); // Uint8Array | ArrayBuffer
63
+ if (result.isDefault) {
64
+ // generic provider default (result.matched tells you which family)
65
+ // swap in your own avatar
66
+ }
67
+
68
+ const fromUrl = await sniff(user.photoUrl); // string URL, fetched for you
69
+ // null if the URL is missing or the fetch fails
70
+ ```
71
+
72
+ ### Browser (canvas `ImageData`)
73
+
74
+ ```ts
75
+ import { sniff } from "avatarsniff";
76
+
77
+ const ctx = canvas.getContext("2d")!;
78
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
79
+ const { isDefault, matched } = await sniff(
80
+ ctx.getImageData(0, 0, canvas.width, canvas.height)
81
+ );
82
+ ```
83
+
84
+ > **Draw at the image's natural size, not a shrunken one.** `sniff` downsamples
85
+ > internally. If you scale the image down onto a small canvas first (with image
86
+ > smoothing on), the blur averages fine detail into grey and a busy identicon can
87
+ > read as a near-white photo. Either draw 1:1 at `img.naturalWidth/Height`, set
88
+ > `ctx.imageSmoothingEnabled = false`, or just pass the original bytes to `sniff`.
89
+
90
+ ### Opt out of detector families
91
+
92
+ Every family runs by default. Disable any of `initials`, `solidColor`,
93
+ `personIcon`, `identicon` per call:
94
+
95
+ ```ts
96
+ await sniff(bytes, { detect: { identicon: false, personIcon: false } });
97
+ ```
98
+
99
+ ### Raw pixels, synchronously (lowest level, anywhere)
100
+
101
+ `sniff` is async; for a synchronous call on pixels you already have, use
102
+ `analyzeImage`:
103
+
104
+ ```ts
105
+ import { analyzeImage } from "avatarsniff";
106
+
107
+ // `data` is RGB or RGBA bytes; alpha is flattened over white.
108
+ const result = analyzeImage({ data, width, height, channels: 4 });
109
+ ```
110
+
111
+ ### WEBP / SVG in plain Node (opt-in)
112
+
113
+ PNG, GIF and JPEG decode in core everywhere. To also decode WEBP and SVG in a
114
+ plain Node process (no canvas), import the opt-in subpath - it registers the
115
+ decoder so the core API picks it up:
116
+
117
+ ```ts
118
+ import "avatarsniff/webp"; // registers the WEBP decoder (~228KB wasm)
119
+ import "avatarsniff/svg"; // registers the SVG rasteriser (~3MB wasm)
120
+ import { sniff } from "avatarsniff";
121
+
122
+ await sniff(webpBytes); // now decodes in plain Node too
123
+ ```
124
+
125
+ Or call the decoders directly:
126
+
127
+ ```ts
128
+ import { decodeWebp } from "avatarsniff/webp";
129
+ import { decodeSvg } from "avatarsniff/svg";
130
+
131
+ const image = await decodeSvg(svgBytes); // RgbaImage | null
132
+ ```
133
+
134
+ `sniff` accepts a `maxBytes` option (default **10MB**); larger inputs are
135
+ rejected before decoding.
136
+
137
+ ## Result
138
+
139
+ ```ts
140
+ type DetectorName = "initials" | "solidColor" | "personIcon" | "identicon";
141
+
142
+ interface DefaultAvatarDetection {
143
+ isDefault: boolean; // the verdict you usually want
144
+ matched: DetectorName | null;// which family fired (null if none)
145
+ score: number; // 0..1, higher = more default-like
146
+ dominantFraction: number; // share of the most common colour (the background)
147
+ significantColors: number;
148
+ glyphFraction: number; // share of near-white pixels (the initial)
149
+ coloredOtherFraction: number;// share of "real" content (high => a photo)
150
+ reason: string; // human-readable explanation
151
+ }
152
+ ```
153
+
154
+ `sniff` resolves to `null` only when there was nothing to sniff - a nullish input
155
+ or a URL that was missing/failed to fetch.
156
+
157
+ ## How it decides
158
+
159
+ Each family is an independent structural detector; the first to match wins and is
160
+ reported in `matched`. The detectors never match specific palette colours, so
161
+ they keep working as providers add new ones.
162
+
163
+ - **`initials`** - a near-white glyph (the letter) on a flat **saturated** colour
164
+ that is neither near-white nor near-black, with almost no other content.
165
+ - **`solidColor`** - a flat saturated colour block with no glyph.
166
+ - **`personIcon`** - a desaturated (grey) person silhouette on a light
167
+ background, strongly mirror-symmetric.
168
+ - **`identicon`** - a mirror-symmetric blocky pattern (GitHub / Gravatar /
169
+ DiceBear), small palette, substantial foreground.
170
+
171
+ Real photos are mainly ruled out by the symmetry test and by having lots of
172
+ coloured content. Every threshold is configurable via the optional
173
+ `DetectOptions` argument, and any family can be turned off with `detect`.
174
+
175
+ ## License
176
+
177
+ MIT © Tunç Türkmen
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ // src/registry.ts
4
+ var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("avatarsniff.decoders");
5
+ function registry() {
6
+ const g = globalThis;
7
+ if (!g[REGISTRY_KEY]) {
8
+ g[REGISTRY_KEY] = /* @__PURE__ */ new Map();
9
+ }
10
+ return g[REGISTRY_KEY];
11
+ }
12
+ function registerDecoder(format, decoder) {
13
+ registry().set(format, decoder);
14
+ }
15
+ function getDecoder(format) {
16
+ return registry().get(format);
17
+ }
18
+
19
+ exports.getDecoder = getDecoder;
20
+ exports.registerDecoder = registerDecoder;
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __commonJS = (cb, mod) => function __require() {
5
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
6
+ };
7
+ var __toBinaryNode = Uint8Array.fromBase64 || ((base64) => new Uint8Array(Buffer.from(base64, "base64")));
8
+
9
+ exports.__commonJS = __commonJS;
10
+ exports.__toBinaryNode = __toBinaryNode;
@@ -0,0 +1,7 @@
1
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
+ var __commonJS = (cb, mod) => function __require() {
3
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
4
+ };
5
+ var __toBinaryNode = Uint8Array.fromBase64 || ((base64) => new Uint8Array(Buffer.from(base64, "base64")));
6
+
7
+ export { __commonJS, __toBinaryNode };
@@ -0,0 +1,17 @@
1
+ // src/registry.ts
2
+ var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("avatarsniff.decoders");
3
+ function registry() {
4
+ const g = globalThis;
5
+ if (!g[REGISTRY_KEY]) {
6
+ g[REGISTRY_KEY] = /* @__PURE__ */ new Map();
7
+ }
8
+ return g[REGISTRY_KEY];
9
+ }
10
+ function registerDecoder(format, decoder) {
11
+ registry().set(format, decoder);
12
+ }
13
+ function getDecoder(format) {
14
+ return registry().get(format);
15
+ }
16
+
17
+ export { getDecoder, registerDecoder };