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 +21 -0
- package/README.md +177 -0
- package/dist/chunk-B5TUEKW2.cjs +20 -0
- package/dist/chunk-ESH45ZBG.cjs +10 -0
- package/dist/chunk-PFMIBRDS.js +7 -0
- package/dist/chunk-PNNWYQXN.js +17 -0
- package/dist/index.cjs +834 -0
- package/dist/index.d.cts +86 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +824 -0
- package/dist/jpeg-js-6X443IYI.js +2141 -0
- package/dist/jpeg-js-H7NYMOHI.cjs +2143 -0
- package/dist/svg.cjs +611 -0
- package/dist/svg.d.cts +5 -0
- package/dist/svg.d.ts +5 -0
- package/dist/svg.js +609 -0
- package/dist/types-BlZG8aqK.d.cts +98 -0
- package/dist/types-BlZG8aqK.d.ts +98 -0
- package/dist/webp.cjs +1385 -0
- package/dist/webp.d.cts +5 -0
- package/dist/webp.d.ts +5 -0
- package/dist/webp.js +1382 -0
- package/package.json +98 -0
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
|
+
[](https://www.npmjs.com/package/avatarsniff)
|
|
4
|
+
[](https://bundlephobia.com/package/avatarsniff)
|
|
5
|
+
[](https://www.npmjs.com/package/avatarsniff?activeTab=dependencies)
|
|
6
|
+
[](https://www.npmjs.com/package/avatarsniff)
|
|
7
|
+
[](./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 };
|