splathash 1.0.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/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # SplatHash — TypeScript / JavaScript
2
+
3
+ TypeScript implementation of [SplatHash](../../README.md): compress any image to 16 bytes and reconstruct it.
4
+
5
+ Works in **Node.js** and **browsers** — the library itself has zero runtime dependencies.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install splathash
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Node.js
16
+
17
+ The library works on raw RGBA bytes. Use any image-loading library (e.g. `sharp`) to get them:
18
+
19
+ ```typescript
20
+ import sharp from "sharp";
21
+ import { encode, decode } from "splathash";
22
+
23
+ const { data, info } = await sharp("photo.jpg")
24
+ .ensureAlpha()
25
+ .raw()
26
+ .toBuffer({ resolveWithObject: true });
27
+
28
+ // Encode: raw RGBA → 16-byte hash
29
+ const hash = encode(new Uint8ClampedArray(data), info.width, info.height);
30
+ console.log(Buffer.from(hash).toString("hex")); // e.g. a3f1bc...
31
+
32
+ // Decode: 16-byte hash → 32×32 RGBA
33
+ const result = decode(hash);
34
+ // result.width = 32
35
+ // result.height = 32
36
+ // result.rgba = Uint8ClampedArray (32 * 32 * 4 bytes)
37
+ ```
38
+
39
+ ### Browser
40
+
41
+ Same package, same API. Get raw RGBA from a `<canvas>`:
42
+
43
+ ```typescript
44
+ import { encode, decode } from "splathash";
45
+
46
+ const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
47
+ const ctx = canvas.getContext("2d")!;
48
+ const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
49
+
50
+ const hash = encode(data, canvas.width, canvas.height);
51
+ ```
52
+
53
+ Or from an `<img>` element:
54
+
55
+ ```typescript
56
+ function hashImage(img: HTMLImageElement): Uint8Array {
57
+ const canvas = document.createElement("canvas");
58
+ canvas.width = img.naturalWidth;
59
+ canvas.height = img.naturalHeight;
60
+ const ctx = canvas.getContext("2d")!;
61
+ ctx.drawImage(img, 0, 0);
62
+ const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
63
+ return encode(data, canvas.width, canvas.height);
64
+ }
65
+ ```
66
+
67
+ See [`examples/simple.ts`](examples/simple.ts) for a full Node.js example.
68
+
69
+ ## API
70
+
71
+ ### `encode(rgba, width, height): Uint8Array`
72
+
73
+ | Parameter | Type | Description |
74
+ | :-------- | :--- | :---------- |
75
+ | `rgba` | `Uint8ClampedArray \| Uint8Array` | Raw RGBA pixel data (4 bytes per pixel) |
76
+ | `width` | `number` | Image width in pixels |
77
+ | `height` | `number` | Image height in pixels |
78
+
79
+ Returns a `Uint8Array` of exactly 16 bytes.
80
+
81
+ ### `decode(hash): DecodedImage`
82
+
83
+ | Parameter | Type | Description |
84
+ | :-------- | :--- | :---------- |
85
+ | `hash` | `Uint8Array` | 16-byte SplatHash |
86
+
87
+ Returns a `DecodedImage`:
88
+
89
+ ```typescript
90
+ interface DecodedImage {
91
+ width: number; // always 32
92
+ height: number; // always 32
93
+ rgba: Uint8ClampedArray; // 32 * 32 * 4 bytes
94
+ }
95
+ ```
96
+
97
+ Throws if the hash is not exactly 16 bytes.
98
+
99
+ ## Building from Source
100
+
101
+ ```bash
102
+ npm install
103
+ npm run build # emits to dist/
104
+ npm test # build + run tests
105
+ ```
106
+
107
+ Or via mise from the repo root:
108
+
109
+ ```bash
110
+ mise run test:ts
111
+ ```
112
+
113
+ ## How It Works
114
+
115
+ See [ALGORITHM.md](../../ALGORITHM.md) for the full technical specification.
@@ -0,0 +1,35 @@
1
+ export declare const TARGET_SIZE = 32;
2
+ export declare const RIDGE_LAMBDA = 0.001;
3
+ export declare const SIGMA_TABLE: number[];
4
+ export interface Splat {
5
+ x: number;
6
+ y: number;
7
+ sigma: number;
8
+ l: number;
9
+ a: number;
10
+ b: number;
11
+ isLepton: boolean;
12
+ }
13
+ export interface DecodedImage {
14
+ meanL: number;
15
+ meanA: number;
16
+ meanB: number;
17
+ splats: Splat[];
18
+ width: number;
19
+ height: number;
20
+ rgba: Uint8ClampedArray;
21
+ }
22
+ /**
23
+ * Encodes an RGBA image buffer into a 16-byte SplatHash.
24
+ * @param rgba Raw RGBA data (Uint8ClampedArray or Uint8Array)
25
+ * @param width Image width
26
+ * @param height Image height
27
+ * @returns 16-byte Uint8Array hash
28
+ */
29
+ export declare function encode(rgba: Uint8ClampedArray | Uint8Array, width: number, height: number): Uint8Array;
30
+ /**
31
+ * Decodes a 16-byte SplatHash back into RGBA pixel data.
32
+ * @param hash 16-byte Uint8Array
33
+ * @returns DecodedImage object containing splats and reconstructed buffer (32x32)
34
+ */
35
+ export declare function decode(hash: Uint8Array): DecodedImage;
@@ -0,0 +1,536 @@
1
+ "use strict";
2
+ // SplatHash V4: The Universal Quantum Brain (TypeScript Edition)
3
+ //
4
+ // SplatHash describes an image using a "Quantum Entanglement" model.
5
+ // It decomposes the image into a background field (Mean) and 6 particles (Splats).
6
+ // The optimal configuration of these particles is found via a global Ridge Regression solver,
7
+ // allowing for extremely high data density (16 bytes).
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.SIGMA_TABLE = exports.RIDGE_LAMBDA = exports.TARGET_SIZE = void 0;
10
+ exports.encode = encode;
11
+ exports.decode = decode;
12
+ exports.TARGET_SIZE = 32;
13
+ exports.RIDGE_LAMBDA = 0.001;
14
+ exports.SIGMA_TABLE = [0.025, 0.1, 0.2, 0.35];
15
+ /**
16
+ * Encodes an RGBA image buffer into a 16-byte SplatHash.
17
+ * @param rgba Raw RGBA data (Uint8ClampedArray or Uint8Array)
18
+ * @param width Image width
19
+ * @param height Image height
20
+ * @returns 16-byte Uint8Array hash
21
+ */
22
+ function encode(rgba, width, height) {
23
+ // 1. Preprocess to Oklab Grid
24
+ const grid = imageToOklabGrid(rgba, width, height, exports.TARGET_SIZE, exports.TARGET_SIZE);
25
+ // 2. Compute Mean
26
+ let meanL = 0, meanA = 0, meanB = 0;
27
+ const n = Math.floor(grid.length / 3);
28
+ for (let i = 0; i < n; i++) {
29
+ meanL += grid[i * 3];
30
+ meanA += grid[i * 3 + 1];
31
+ meanB += grid[i * 3 + 2];
32
+ }
33
+ meanL /= n;
34
+ meanA /= n;
35
+ meanB /= n;
36
+ // QUANTUM STEP: Optimize against quantized mean
37
+ const pMean = packMean(meanL, meanA, meanB);
38
+ const uMean = unpackMean(pMean);
39
+ meanL = uMean.l;
40
+ meanA = uMean.a;
41
+ meanB = uMean.b;
42
+ // Residuals
43
+ const targetL = new Float64Array(n);
44
+ const targetA = new Float64Array(n);
45
+ const targetB = new Float64Array(n);
46
+ for (let i = 0; i < n; i++) {
47
+ targetL[i] = grid[i * 3] - meanL;
48
+ targetA[i] = grid[i * 3 + 1] - meanA;
49
+ targetB[i] = grid[i * 3 + 2] - meanB;
50
+ }
51
+ // 3. Basis Search (Greedy)
52
+ let basis = [];
53
+ const currentRecon = new Float64Array(exports.TARGET_SIZE * exports.TARGET_SIZE * 3);
54
+ for (let i = 0; i < 6; i++) {
55
+ const candidate = findBestSplat(grid, currentRecon, meanL, meanA, meanB, exports.TARGET_SIZE, exports.TARGET_SIZE);
56
+ if (candidate.score < 0.00001)
57
+ break;
58
+ const s = candidate.splat;
59
+ s.isLepton = (i >= 3); // First 3 are Baryons (Full Color), Next 3 are Leptons (Luma Only)
60
+ basis.push(s);
61
+ addSplatToGrid(currentRecon, s, exports.TARGET_SIZE, exports.TARGET_SIZE);
62
+ }
63
+ // 4. Global Linear Projection
64
+ if (basis.length > 0) {
65
+ basis = solveV4Weights(basis, targetL, targetA, targetB, exports.TARGET_SIZE, exports.TARGET_SIZE);
66
+ }
67
+ // 5. Pack
68
+ return packV4(pMean, basis);
69
+ }
70
+ /**
71
+ * Decodes a 16-byte SplatHash back into RGBA pixel data.
72
+ * @param hash 16-byte Uint8Array
73
+ * @returns DecodedImage object containing splats and reconstructed buffer (32x32)
74
+ */
75
+ function decode(hash) {
76
+ if (hash.length !== 16)
77
+ throw new Error("Invalid SplatHash: Must be 16 bytes.");
78
+ const { meanL, meanA, meanB, splats } = unpackV4(hash);
79
+ const w = 32;
80
+ const h = 32;
81
+ const grid = new Float64Array(w * h * 3);
82
+ // Fill background
83
+ for (let i = 0; i < grid.length; i += 3) {
84
+ grid[i] = meanL;
85
+ grid[i + 1] = meanA;
86
+ grid[i + 2] = meanB;
87
+ }
88
+ // Add splats
89
+ for (const s of splats) {
90
+ addSplatToGrid(grid, s, w, h);
91
+ }
92
+ // Convert to RGBA
93
+ const rgba = new Uint8ClampedArray(w * h * 4);
94
+ for (let y = 0; y < h; y++) {
95
+ for (let x = 0; x < w; x++) {
96
+ const idx = (y * w + x) * 3;
97
+ const l = grid[idx], a = grid[idx + 1], b = grid[idx + 2];
98
+ const { r, g, b: bl } = oklabToSrgb(l, a, b);
99
+ const pIdx = (y * w + x) * 4;
100
+ rgba[pIdx] = clampi(Math.round(r * 255), 0, 255);
101
+ rgba[pIdx + 1] = clampi(Math.round(g * 255), 0, 255);
102
+ rgba[pIdx + 2] = clampi(Math.round(bl * 255), 0, 255);
103
+ rgba[pIdx + 3] = 255;
104
+ }
105
+ }
106
+ return { meanL, meanA, meanB, splats, width: w, height: h, rgba };
107
+ }
108
+ // --- Implementation Details ---
109
+ function solveV4Weights(basis, tL, tA, tB, w, h) {
110
+ const nTotal = basis.length;
111
+ let nBaryons = 0;
112
+ for (const s of basis)
113
+ if (!s.isLepton)
114
+ nBaryons++;
115
+ // Precompute Activations
116
+ const activations = basis.map(s => computeBasisMap(s, w, h));
117
+ // Linear Solve
118
+ const xL = solveChannel(activations, tL, nTotal, exports.RIDGE_LAMBDA);
119
+ const xA = solveChannel(activations.slice(0, nBaryons), tA, nBaryons, exports.RIDGE_LAMBDA);
120
+ const xB = solveChannel(activations.slice(0, nBaryons), tB, nBaryons, exports.RIDGE_LAMBDA);
121
+ // Update Basis
122
+ for (let i = 0; i < nTotal; i++) {
123
+ basis[i].l = xL[i];
124
+ if (i < nBaryons) {
125
+ basis[i].a = xA[i];
126
+ basis[i].b = xB[i];
127
+ }
128
+ else {
129
+ basis[i].a = 0;
130
+ basis[i].b = 0;
131
+ }
132
+ }
133
+ return basis;
134
+ }
135
+ function solveChannel(activations, target, n, lambda) {
136
+ if (n === 0)
137
+ return new Float64Array(0);
138
+ const m = target.length;
139
+ const ata = new Float64Array(n * n);
140
+ const atb = new Float64Array(n);
141
+ // Build Normal Equations (ATA * x = ATb)
142
+ for (let i = 0; i < n; i++) {
143
+ for (let j = i; j < n; j++) {
144
+ let sum = 0.0;
145
+ const actI = activations[i];
146
+ const actJ = activations[j];
147
+ for (let p = 0; p < m; p++)
148
+ sum += actI[p] * actJ[p];
149
+ ata[i * n + j] = sum;
150
+ ata[j * n + i] = sum;
151
+ }
152
+ let sumB = 0.0;
153
+ const actI = activations[i];
154
+ for (let p = 0; p < m; p++)
155
+ sumB += actI[p] * target[p];
156
+ atb[i] = sumB;
157
+ }
158
+ // Ridge Regularization
159
+ for (let i = 0; i < n; i++)
160
+ ata[i * n + i] += lambda;
161
+ return solveLinearSystem(ata, atb, n);
162
+ }
163
+ function solveLinearSystem(mat, vec, n) {
164
+ // Gaussian Elimination
165
+ const a = new Float64Array(mat); // Copy
166
+ const b = new Float64Array(vec); // Copy
167
+ for (let k = 0; k < n - 1; k++) {
168
+ for (let i = k + 1; i < n; i++) {
169
+ const factor = a[i * n + k] / a[k * n + k];
170
+ for (let j = k; j < n; j++)
171
+ a[i * n + j] -= factor * a[k * n + j];
172
+ b[i] -= factor * b[k];
173
+ }
174
+ }
175
+ const x = new Float64Array(n);
176
+ for (let i = n - 1; i >= 0; i--) {
177
+ let sum = 0.0;
178
+ for (let j = i + 1; j < n; j++)
179
+ sum += a[i * n + j] * x[j];
180
+ x[i] = (b[i] - sum) / a[i * n + i];
181
+ }
182
+ return x;
183
+ }
184
+ function findBestSplat(grid, recon, mL, mA, mB, w, h) {
185
+ let maxScore = -1.0;
186
+ let bestSplat = { x: 0, y: 0, sigma: 0.1, l: 0, a: 0, b: 0, isLepton: false };
187
+ // Compute Residuals
188
+ const n = Math.floor(grid.length / 3);
189
+ const resL = new Float64Array(n);
190
+ const resA = new Float64Array(n);
191
+ const resB = new Float64Array(n);
192
+ for (let i = 0; i < n; i++) {
193
+ resL[i] = grid[i * 3] - mL - recon[i * 3];
194
+ resA[i] = grid[i * 3 + 1] - mA - recon[i * 3 + 1];
195
+ resB[i] = grid[i * 3 + 2] - mB - recon[i * 3 + 2];
196
+ }
197
+ const step = 2; // Optimization: stride
198
+ const sigmas = exports.SIGMA_TABLE;
199
+ for (let y = 0; y < h; y += step) {
200
+ for (let x = 0; x < w; x += step) {
201
+ const xf = x / w;
202
+ const yf = y / h;
203
+ for (const sigma of sigmas) {
204
+ let dotL = 0, dotA = 0, dotB = 0, dotG = 0;
205
+ const rad = Math.floor(sigma * w * 3.5);
206
+ const y0 = clampi(y - rad, 0, h - 1);
207
+ const y1 = clampi(y + rad, 0, h - 1);
208
+ const x0 = clampi(x - rad, 0, w - 1);
209
+ const x1 = clampi(x + rad, 0, w - 1);
210
+ for (let sy = y0; sy <= y1; sy++) {
211
+ const dy = (sy / h) - yf;
212
+ const rowBase = sy * w;
213
+ for (let sx = x0; sx <= x1; sx++) {
214
+ const dx = (sx / w) - xf;
215
+ const distSq = dx * dx + dy * dy;
216
+ const weight = Math.exp(-distSq / (2 * sigma * sigma));
217
+ const idx = rowBase + sx;
218
+ dotL += weight * resL[idx];
219
+ dotA += weight * resA[idx];
220
+ dotB += weight * resB[idx];
221
+ dotG += weight * weight;
222
+ }
223
+ }
224
+ if (dotG < 1e-9)
225
+ continue;
226
+ // Score based on energy
227
+ const score = (dotL * dotL + dotA * dotA + dotB * dotB) / dotG;
228
+ if (score > maxScore) {
229
+ maxScore = score;
230
+ bestSplat = {
231
+ x: xf, y: yf, sigma,
232
+ l: dotL / dotG, a: dotA / dotG, b: dotB / dotG,
233
+ isLepton: false
234
+ };
235
+ }
236
+ }
237
+ }
238
+ }
239
+ return { splat: bestSplat, score: maxScore };
240
+ }
241
+ function computeBasisMap(s, w, h) {
242
+ const out = new Float64Array(w * h);
243
+ const rad = Math.floor(s.sigma * w * 3.5);
244
+ const cx = Math.floor(s.x * w);
245
+ const cy = Math.floor(s.y * h);
246
+ const y0 = clampi(cy - rad, 0, h - 1);
247
+ const y1 = clampi(cy + rad, 0, h - 1);
248
+ const x0 = clampi(cx - rad, 0, w - 1);
249
+ const x1 = clampi(cx + rad, 0, w - 1);
250
+ for (let y = y0; y <= y1; y++) {
251
+ const dy = (y / h) - s.y;
252
+ for (let x = x0; x <= x1; x++) {
253
+ const dx = (x / w) - s.x;
254
+ out[y * w + x] = Math.exp(-(dx * dx + dy * dy) / (2 * s.sigma * s.sigma));
255
+ }
256
+ }
257
+ return out;
258
+ }
259
+ function addSplatToGrid(grid, s, w, h) {
260
+ const rad = Math.floor(s.sigma * w * 3.5);
261
+ const cx = Math.floor(s.x * w);
262
+ const cy = Math.floor(s.y * h);
263
+ const y0 = clampi(cy - rad, 0, h - 1);
264
+ const y1 = clampi(cy + rad, 0, h - 1);
265
+ const x0 = clampi(cx - rad, 0, w - 1);
266
+ const x1 = clampi(cx + rad, 0, w - 1);
267
+ for (let y = y0; y <= y1; y++) {
268
+ const dy = (y / h) - s.y;
269
+ for (let x = x0; x <= x1; x++) {
270
+ const dx = (x / w) - s.x;
271
+ const weight = Math.exp(-(dx * dx + dy * dy) / (2 * s.sigma * s.sigma));
272
+ const idx = (y * w + x) * 3;
273
+ grid[idx] += s.l * weight;
274
+ if (!s.isLepton) {
275
+ grid[idx + 1] += s.a * weight;
276
+ grid[idx + 2] += s.b * weight;
277
+ }
278
+ }
279
+ }
280
+ }
281
+ // --- Image Helpers ---
282
+ function imageToOklabGrid(rgba, srcW, srcH, w, h) {
283
+ const out = new Float64Array(w * h * 3);
284
+ for (let y = 0; y < h; y++) {
285
+ const y0 = Math.floor((y * srcH) / h);
286
+ const y1 = Math.floor(((y + 1) * srcH) / h);
287
+ for (let x = 0; x < w; x++) {
288
+ const x0 = Math.floor((x * srcW) / w);
289
+ const x1 = Math.floor(((x + 1) * srcW) / w);
290
+ let rSum = 0, gSum = 0, bSum = 0, count = 0;
291
+ for (let iy = y0; iy < y1; iy++) {
292
+ if (iy >= srcH)
293
+ break;
294
+ for (let ix = x0; ix < x1; ix++) {
295
+ if (ix >= srcW)
296
+ break;
297
+ const idx = (iy * srcW + ix) * 4;
298
+ rSum += rgba[idx];
299
+ gSum += rgba[idx + 1];
300
+ bSum += rgba[idx + 2];
301
+ count++;
302
+ }
303
+ }
304
+ if (count === 0)
305
+ continue;
306
+ const r = (rSum / count) / 255.0;
307
+ const g = (gSum / count) / 255.0;
308
+ const b = (bSum / count) / 255.0;
309
+ const lab = srgbToOklab(r, g, b);
310
+ const idx = (y * w + x) * 3;
311
+ out[idx] = lab.l;
312
+ out[idx + 1] = lab.a;
313
+ out[idx + 2] = lab.b;
314
+ }
315
+ }
316
+ return out;
317
+ }
318
+ function srgbToOklab(r, g, b) {
319
+ const lin = (c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
320
+ r = lin(r);
321
+ g = lin(g);
322
+ b = lin(b);
323
+ const l1 = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
324
+ const m1 = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
325
+ const s1 = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
326
+ const l_ = Math.cbrt(l1), m_ = Math.cbrt(m1), s_ = Math.cbrt(s1);
327
+ return {
328
+ l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
329
+ a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
330
+ b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
331
+ };
332
+ }
333
+ function oklabToSrgb(l, a, b) {
334
+ const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
335
+ const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
336
+ const s_ = l - 0.0894841775 * a - 1.2914855480 * b;
337
+ const l3 = l_ * l_ * l_;
338
+ const m3 = m_ * m_ * m_;
339
+ const s3 = s_ * s_ * s_;
340
+ let r = +4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
341
+ let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
342
+ let bl = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3;
343
+ const srt = (c) => (c <= 0.0031308 ? 12.92 * c : (c < 0 ? 0 : 1.055 * Math.pow(c, 1.0 / 2.4) - 0.055));
344
+ return { r: srt(r), g: srt(g), b: srt(bl) };
345
+ }
346
+ // --- Bit Packing ---
347
+ class BitStream {
348
+ constructor() {
349
+ this.buf = [];
350
+ this.acc = 0n;
351
+ this.n = 0;
352
+ }
353
+ write(val, bits) {
354
+ this.acc = (this.acc << BigInt(bits)) | BigInt(val);
355
+ this.n += bits;
356
+ while (this.n >= 8) {
357
+ const shift = BigInt(this.n - 8);
358
+ const byteVal = Number((this.acc >> shift) & 0xffn);
359
+ this.buf.push(byteVal);
360
+ this.n -= 8;
361
+ }
362
+ }
363
+ getBytes() {
364
+ if (this.n > 0) {
365
+ this.buf.push(Number((this.acc << BigInt(8 - this.n)) & 0xffn));
366
+ }
367
+ return new Uint8Array(this.buf);
368
+ }
369
+ }
370
+ class BitReader {
371
+ constructor(data) {
372
+ this.pos = 0;
373
+ this.rem = 0;
374
+ this.curr = 0;
375
+ this.data = data;
376
+ }
377
+ read(bits) {
378
+ let val = 0;
379
+ let bitsRemaining = bits;
380
+ while (bitsRemaining > 0) {
381
+ if (this.rem === 0) {
382
+ if (this.pos >= this.data.length)
383
+ return val << bitsRemaining; // EOF
384
+ this.curr = this.data[this.pos++];
385
+ this.rem = 8;
386
+ }
387
+ // Optimization for partial read
388
+ const take = Math.min(this.rem, bitsRemaining);
389
+ const shift = this.rem - take;
390
+ const mask = (1 << take) - 1;
391
+ const chunk = (this.curr >> shift) & mask;
392
+ val = (val << take) | chunk;
393
+ this.rem -= take;
394
+ bitsRemaining -= take;
395
+ }
396
+ return val;
397
+ }
398
+ }
399
+ function packV4(mean, splats) {
400
+ const bw = new BitStream();
401
+ // Header (16 bits)
402
+ bw.write(mean, 16);
403
+ // Baryons (3x)
404
+ let count = 0;
405
+ for (const s of splats) {
406
+ if (s.isLepton)
407
+ continue;
408
+ if (count >= 3)
409
+ break;
410
+ const xi = clampi(Math.round(s.x * 15.0), 0, 15);
411
+ const yi = clampi(Math.round(s.y * 15.0), 0, 15);
412
+ const sigI = getSigmaIdx(s.sigma);
413
+ const lQ = quant(s.l, -0.8, 0.8, 4);
414
+ const aQ = quant(s.a, -0.4, 0.4, 4);
415
+ const bQ = quant(s.b, -0.4, 0.4, 4);
416
+ bw.write(xi, 4);
417
+ bw.write(yi, 4);
418
+ bw.write(sigI, 2);
419
+ bw.write(lQ, 4);
420
+ bw.write(aQ, 4);
421
+ bw.write(bQ, 4);
422
+ count++;
423
+ }
424
+ while (count < 3) {
425
+ bw.write(0, 22);
426
+ count++;
427
+ }
428
+ // Leptons (3x)
429
+ count = 0;
430
+ for (const s of splats) {
431
+ if (!s.isLepton)
432
+ continue;
433
+ if (count >= 3)
434
+ break;
435
+ const xi = clampi(Math.round(s.x * 15.0), 0, 15);
436
+ const yi = clampi(Math.round(s.y * 15.0), 0, 15);
437
+ const sigI = getSigmaIdx(s.sigma);
438
+ const lQ = quant(s.l, -0.8, 0.8, 5);
439
+ bw.write(xi, 4);
440
+ bw.write(yi, 4);
441
+ bw.write(sigI, 2);
442
+ bw.write(lQ, 5);
443
+ count++;
444
+ }
445
+ while (count < 3) {
446
+ bw.write(0, 15);
447
+ count++;
448
+ }
449
+ bw.write(0, 1); // Pad
450
+ return bw.getBytes();
451
+ }
452
+ function unpackV4(hash) {
453
+ const br = new BitReader(hash);
454
+ const meanMap = br.read(16);
455
+ const { l, a, b } = unpackMean(meanMap);
456
+ const splats = [];
457
+ // Baryons
458
+ for (let i = 0; i < 3; i++) {
459
+ const xi = br.read(4);
460
+ const yi = br.read(4);
461
+ const sigI = br.read(2);
462
+ const lQ = br.read(4);
463
+ const aQ = br.read(4);
464
+ const bQ = br.read(4);
465
+ if (xi === 0 && yi === 0 && lQ === 0 && aQ === 0 && bQ === 0)
466
+ continue;
467
+ splats.push({
468
+ x: xi / 15.0,
469
+ y: yi / 15.0,
470
+ sigma: exports.SIGMA_TABLE[sigI],
471
+ l: unquant(lQ, -0.8, 0.8, 4),
472
+ a: unquant(aQ, -0.4, 0.4, 4),
473
+ b: unquant(bQ, -0.4, 0.4, 4),
474
+ isLepton: false
475
+ });
476
+ }
477
+ // Leptons
478
+ for (let i = 0; i < 3; i++) {
479
+ const xi = br.read(4);
480
+ const yi = br.read(4);
481
+ const sigI = br.read(2);
482
+ const lQ = br.read(5);
483
+ if (xi === 0 && yi === 0 && lQ === 0)
484
+ continue;
485
+ splats.push({
486
+ x: xi / 15.0,
487
+ y: yi / 15.0,
488
+ sigma: exports.SIGMA_TABLE[sigI],
489
+ l: unquant(lQ, -0.8, 0.8, 5),
490
+ a: 0, b: 0,
491
+ isLepton: true
492
+ });
493
+ }
494
+ return { meanL: l, meanA: a, meanB: b, splats };
495
+ }
496
+ function getSigmaIdx(s) {
497
+ let minD = 100.0;
498
+ let idx = 0;
499
+ for (let i = 0; i < exports.SIGMA_TABLE.length; i++) {
500
+ const d = Math.abs(exports.SIGMA_TABLE[i] - s);
501
+ if (d < minD) {
502
+ minD = d;
503
+ idx = i;
504
+ }
505
+ }
506
+ return idx;
507
+ }
508
+ function packMean(l, a, b) {
509
+ const li = clampi(Math.floor(l * 63.5), 0, 63);
510
+ const ai = clampi(Math.floor(((a + 0.2) / 0.4) * 31.5), 0, 31);
511
+ const bi = clampi(Math.floor(((b + 0.2) / 0.4) * 31.5), 0, 31);
512
+ return (li << 10) | (ai << 5) | bi;
513
+ }
514
+ function unpackMean(p) {
515
+ const li = (p >> 10) & 0x3F;
516
+ const ai = (p >> 5) & 0x1F;
517
+ const bi = p & 0x1F;
518
+ return {
519
+ l: li / 63.0,
520
+ a: (ai / 31.0 * 0.4) - 0.2,
521
+ b: (bi / 31.0 * 0.4) - 0.2
522
+ };
523
+ }
524
+ function quant(v, min, max, bits) {
525
+ const steps = (1 << bits) - 1;
526
+ const norm = (v - min) / (max - min);
527
+ return clampi(Math.round(norm * steps), 0, steps);
528
+ }
529
+ function unquant(v, min, max, bits) {
530
+ const steps = (1 << bits) - 1;
531
+ const norm = v / steps;
532
+ return norm * (max - min) + min;
533
+ }
534
+ function clampi(v, min, max) {
535
+ return v < min ? min : (v > max ? max : v);
536
+ }
@@ -0,0 +1 @@
1
+ export * from "./splathash";
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./splathash"), exports);
@@ -0,0 +1,31 @@
1
+ export declare const TARGET_SIZE = 32;
2
+ export declare const RIDGE_LAMBDA = 0.001;
3
+ export declare const SIGMA_TABLE: number[];
4
+ export interface Splat {
5
+ x: number;
6
+ y: number;
7
+ sigma: number;
8
+ l: number;
9
+ a: number;
10
+ b: number;
11
+ isLepton: boolean;
12
+ }
13
+ export interface DecodedImage {
14
+ width: number;
15
+ height: number;
16
+ rgba: Uint8ClampedArray;
17
+ }
18
+ /**
19
+ * Encodes an RGBA image buffer into a 16-byte SplatHash.
20
+ * @param rgba Raw RGBA data (Uint8ClampedArray or Uint8Array)
21
+ * @param width Image width
22
+ * @param height Image height
23
+ * @returns 16-byte Uint8Array hash
24
+ */
25
+ export declare function encode(rgba: Uint8ClampedArray | Uint8Array, width: number, height: number): Uint8Array;
26
+ /**
27
+ * Decodes a 16-byte SplatHash back into RGBA pixel data.
28
+ * @param hash 16-byte Uint8Array
29
+ * @returns DecodedImage object containing splats and reconstructed buffer (32x32)
30
+ */
31
+ export declare function decode(hash: Uint8Array): DecodedImage;
@@ -0,0 +1,556 @@
1
+ "use strict";
2
+ // SplatHash — TypeScript implementation
3
+ //
4
+ // Encodes any image into 16 bytes and reconstructs a 32x32 preview.
5
+ // An image is decomposed into a background color (Mean) and six Gaussian blobs (Splats):
6
+ // - 3 Baryons: full-color Splats for dominant features
7
+ // - 3 Leptons: luma-only Splats for texture and detail
8
+ //
9
+ // Splat positions are found greedily; Ridge Regression then refines all weights together.
10
+ // All computation is done in Oklab. The hash fits into exactly 128 bits.
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.SIGMA_TABLE = exports.RIDGE_LAMBDA = exports.TARGET_SIZE = void 0;
13
+ exports.encode = encode;
14
+ exports.decode = decode;
15
+ exports.TARGET_SIZE = 32;
16
+ exports.RIDGE_LAMBDA = 0.001;
17
+ exports.SIGMA_TABLE = [0.025, 0.1, 0.2, 0.35];
18
+ /**
19
+ * Encodes an RGBA image buffer into a 16-byte SplatHash.
20
+ * @param rgba Raw RGBA data (Uint8ClampedArray or Uint8Array)
21
+ * @param width Image width
22
+ * @param height Image height
23
+ * @returns 16-byte Uint8Array hash
24
+ */
25
+ function encode(rgba, width, height) {
26
+ // 1. Preprocess to Oklab Grid
27
+ const grid = imageToOklabGrid(rgba, width, height, exports.TARGET_SIZE, exports.TARGET_SIZE);
28
+ // 2. Compute Mean
29
+ let meanL = 0, meanA = 0, meanB = 0;
30
+ const n = Math.floor(grid.length / 3);
31
+ for (let i = 0; i < n; i++) {
32
+ meanL += grid[i * 3];
33
+ meanA += grid[i * 3 + 1];
34
+ meanB += grid[i * 3 + 2];
35
+ }
36
+ meanL /= n;
37
+ meanA /= n;
38
+ meanB /= n;
39
+ // 3. Quantize Mean immediately so the solver optimizes against the reconstructed mean.
40
+ const pMean = packMean(meanL, meanA, meanB);
41
+ const uMean = unpackMean(pMean);
42
+ meanL = uMean.l;
43
+ meanA = uMean.a;
44
+ meanB = uMean.b;
45
+ // Residuals
46
+ const targetL = new Float64Array(n);
47
+ const targetA = new Float64Array(n);
48
+ const targetB = new Float64Array(n);
49
+ for (let i = 0; i < n; i++) {
50
+ targetL[i] = grid[i * 3] - meanL;
51
+ targetA[i] = grid[i * 3 + 1] - meanA;
52
+ targetB[i] = grid[i * 3 + 2] - meanB;
53
+ }
54
+ // 3. Basis Search (Greedy)
55
+ let basis = [];
56
+ const currentRecon = new Float64Array(exports.TARGET_SIZE * exports.TARGET_SIZE * 3);
57
+ for (let i = 0; i < 6; i++) {
58
+ const candidate = findBestSplat(grid, currentRecon, meanL, meanA, meanB, exports.TARGET_SIZE, exports.TARGET_SIZE);
59
+ if (candidate.score < 0.00001)
60
+ break;
61
+ const s = candidate.splat;
62
+ s.isLepton = i >= 3; // First 3 are Baryons (Full Color), Next 3 are Leptons (Luma Only)
63
+ basis.push(s);
64
+ addSplatToGrid(currentRecon, s, exports.TARGET_SIZE, exports.TARGET_SIZE);
65
+ }
66
+ // 4. Global Linear Projection
67
+ if (basis.length > 0) {
68
+ basis = solveV4Weights(basis, targetL, targetA, targetB, exports.TARGET_SIZE, exports.TARGET_SIZE);
69
+ }
70
+ // 5. Pack
71
+ return packV4(pMean, basis);
72
+ }
73
+ /**
74
+ * Decodes a 16-byte SplatHash back into RGBA pixel data.
75
+ * @param hash 16-byte Uint8Array
76
+ * @returns DecodedImage object containing splats and reconstructed buffer (32x32)
77
+ */
78
+ function decode(hash) {
79
+ if (hash.length !== 16)
80
+ throw new Error("Invalid SplatHash: Must be 16 bytes.");
81
+ const { meanL, meanA, meanB, splats } = unpackV4(hash);
82
+ const w = 32;
83
+ const h = 32;
84
+ const grid = new Float64Array(w * h * 3);
85
+ // Fill background
86
+ for (let i = 0; i < grid.length; i += 3) {
87
+ grid[i] = meanL;
88
+ grid[i + 1] = meanA;
89
+ grid[i + 2] = meanB;
90
+ }
91
+ // Add splats
92
+ for (const s of splats) {
93
+ addSplatToGrid(grid, s, w, h);
94
+ }
95
+ // Convert to RGBA
96
+ const rgba = new Uint8ClampedArray(w * h * 4);
97
+ for (let y = 0; y < h; y++) {
98
+ for (let x = 0; x < w; x++) {
99
+ const idx = (y * w + x) * 3;
100
+ const l = grid[idx], a = grid[idx + 1], b = grid[idx + 2];
101
+ const { r, g, b: bl } = oklabToSrgb(l, a, b);
102
+ const pIdx = (y * w + x) * 4;
103
+ rgba[pIdx] = clampi(Math.round(r * 255), 0, 255);
104
+ rgba[pIdx + 1] = clampi(Math.round(g * 255), 0, 255);
105
+ rgba[pIdx + 2] = clampi(Math.round(bl * 255), 0, 255);
106
+ rgba[pIdx + 3] = 255;
107
+ }
108
+ }
109
+ return { width: w, height: h, rgba };
110
+ }
111
+ // --- Implementation Details ---
112
+ function solveV4Weights(basis, tL, tA, tB, w, h) {
113
+ const nTotal = basis.length;
114
+ let nBaryons = 0;
115
+ for (const s of basis)
116
+ if (!s.isLepton)
117
+ nBaryons++;
118
+ // Precompute Activations
119
+ const activations = basis.map((s) => computeBasisMap(s, w, h));
120
+ // Linear Solve
121
+ const xL = solveChannel(activations, tL, nTotal, exports.RIDGE_LAMBDA);
122
+ const xA = solveChannel(activations.slice(0, nBaryons), tA, nBaryons, exports.RIDGE_LAMBDA);
123
+ const xB = solveChannel(activations.slice(0, nBaryons), tB, nBaryons, exports.RIDGE_LAMBDA);
124
+ // Update Basis
125
+ for (let i = 0; i < nTotal; i++) {
126
+ basis[i].l = xL[i];
127
+ if (i < nBaryons) {
128
+ basis[i].a = xA[i];
129
+ basis[i].b = xB[i];
130
+ }
131
+ else {
132
+ basis[i].a = 0;
133
+ basis[i].b = 0;
134
+ }
135
+ }
136
+ return basis;
137
+ }
138
+ function solveChannel(activations, target, n, lambda) {
139
+ if (n === 0)
140
+ return new Float64Array(0);
141
+ const m = target.length;
142
+ const ata = new Float64Array(n * n);
143
+ const atb = new Float64Array(n);
144
+ // Build Normal Equations (ATA * x = ATb)
145
+ for (let i = 0; i < n; i++) {
146
+ for (let j = i; j < n; j++) {
147
+ let sum = 0.0;
148
+ const actI = activations[i];
149
+ const actJ = activations[j];
150
+ for (let p = 0; p < m; p++)
151
+ sum += actI[p] * actJ[p];
152
+ ata[i * n + j] = sum;
153
+ ata[j * n + i] = sum;
154
+ }
155
+ let sumB = 0.0;
156
+ const actI = activations[i];
157
+ for (let p = 0; p < m; p++)
158
+ sumB += actI[p] * target[p];
159
+ atb[i] = sumB;
160
+ }
161
+ // Ridge Regularization
162
+ for (let i = 0; i < n; i++)
163
+ ata[i * n + i] += lambda;
164
+ return solveLinearSystem(ata, atb, n);
165
+ }
166
+ function solveLinearSystem(mat, vec, n) {
167
+ // Gaussian Elimination
168
+ const a = new Float64Array(mat); // Copy
169
+ const b = new Float64Array(vec); // Copy
170
+ for (let k = 0; k < n - 1; k++) {
171
+ for (let i = k + 1; i < n; i++) {
172
+ const factor = a[i * n + k] / a[k * n + k];
173
+ for (let j = k; j < n; j++)
174
+ a[i * n + j] -= factor * a[k * n + j];
175
+ b[i] -= factor * b[k];
176
+ }
177
+ }
178
+ const x = new Float64Array(n);
179
+ for (let i = n - 1; i >= 0; i--) {
180
+ let sum = 0.0;
181
+ for (let j = i + 1; j < n; j++)
182
+ sum += a[i * n + j] * x[j];
183
+ x[i] = (b[i] - sum) / a[i * n + i];
184
+ }
185
+ return x;
186
+ }
187
+ function findBestSplat(grid, recon, mL, mA, mB, w, h) {
188
+ let maxScore = -1.0;
189
+ let bestSplat = {
190
+ x: 0,
191
+ y: 0,
192
+ sigma: 0.1,
193
+ l: 0,
194
+ a: 0,
195
+ b: 0,
196
+ isLepton: false,
197
+ };
198
+ // Compute Residuals
199
+ const n = Math.floor(grid.length / 3);
200
+ const resL = new Float64Array(n);
201
+ const resA = new Float64Array(n);
202
+ const resB = new Float64Array(n);
203
+ for (let i = 0; i < n; i++) {
204
+ resL[i] = grid[i * 3] - mL - recon[i * 3];
205
+ resA[i] = grid[i * 3 + 1] - mA - recon[i * 3 + 1];
206
+ resB[i] = grid[i * 3 + 2] - mB - recon[i * 3 + 2];
207
+ }
208
+ const step = 2; // Optimization: stride
209
+ const sigmas = exports.SIGMA_TABLE;
210
+ for (let y = 0; y < h; y += step) {
211
+ for (let x = 0; x < w; x += step) {
212
+ const xf = x / w;
213
+ const yf = y / h;
214
+ for (const sigma of sigmas) {
215
+ let dotL = 0, dotA = 0, dotB = 0, dotG = 0;
216
+ const rad = Math.floor(sigma * w * 3.5);
217
+ const y0 = clampi(y - rad, 0, h - 1);
218
+ const y1 = clampi(y + rad, 0, h - 1);
219
+ const x0 = clampi(x - rad, 0, w - 1);
220
+ const x1 = clampi(x + rad, 0, w - 1);
221
+ for (let sy = y0; sy <= y1; sy++) {
222
+ const dy = sy / h - yf;
223
+ const rowBase = sy * w;
224
+ for (let sx = x0; sx <= x1; sx++) {
225
+ const dx = sx / w - xf;
226
+ const distSq = dx * dx + dy * dy;
227
+ const weight = Math.exp(-distSq / (2 * sigma * sigma));
228
+ const idx = rowBase + sx;
229
+ dotL += weight * resL[idx];
230
+ dotA += weight * resA[idx];
231
+ dotB += weight * resB[idx];
232
+ dotG += weight * weight;
233
+ }
234
+ }
235
+ if (dotG < 1e-9)
236
+ continue;
237
+ // Score based on energy
238
+ const score = (dotL * dotL + dotA * dotA + dotB * dotB) / dotG;
239
+ if (score > maxScore) {
240
+ maxScore = score;
241
+ bestSplat = {
242
+ x: xf,
243
+ y: yf,
244
+ sigma,
245
+ l: dotL / dotG,
246
+ a: dotA / dotG,
247
+ b: dotB / dotG,
248
+ isLepton: false,
249
+ };
250
+ }
251
+ }
252
+ }
253
+ }
254
+ return { splat: bestSplat, score: maxScore };
255
+ }
256
+ function computeBasisMap(s, w, h) {
257
+ const out = new Float64Array(w * h);
258
+ const rad = Math.floor(s.sigma * w * 3.5);
259
+ const cx = Math.floor(s.x * w);
260
+ const cy = Math.floor(s.y * h);
261
+ const y0 = clampi(cy - rad, 0, h - 1);
262
+ const y1 = clampi(cy + rad, 0, h - 1);
263
+ const x0 = clampi(cx - rad, 0, w - 1);
264
+ const x1 = clampi(cx + rad, 0, w - 1);
265
+ for (let y = y0; y <= y1; y++) {
266
+ const dy = y / h - s.y;
267
+ for (let x = x0; x <= x1; x++) {
268
+ const dx = x / w - s.x;
269
+ out[y * w + x] = Math.exp(-(dx * dx + dy * dy) / (2 * s.sigma * s.sigma));
270
+ }
271
+ }
272
+ return out;
273
+ }
274
+ function addSplatToGrid(grid, s, w, h) {
275
+ const rad = Math.floor(s.sigma * w * 3.5);
276
+ const cx = Math.floor(s.x * w);
277
+ const cy = Math.floor(s.y * h);
278
+ const y0 = clampi(cy - rad, 0, h - 1);
279
+ const y1 = clampi(cy + rad, 0, h - 1);
280
+ const x0 = clampi(cx - rad, 0, w - 1);
281
+ const x1 = clampi(cx + rad, 0, w - 1);
282
+ for (let y = y0; y <= y1; y++) {
283
+ const dy = y / h - s.y;
284
+ for (let x = x0; x <= x1; x++) {
285
+ const dx = x / w - s.x;
286
+ const weight = Math.exp(-(dx * dx + dy * dy) / (2 * s.sigma * s.sigma));
287
+ const idx = (y * w + x) * 3;
288
+ grid[idx] += s.l * weight;
289
+ if (!s.isLepton) {
290
+ grid[idx + 1] += s.a * weight;
291
+ grid[idx + 2] += s.b * weight;
292
+ }
293
+ }
294
+ }
295
+ }
296
+ // --- Image Helpers ---
297
+ function imageToOklabGrid(rgba, srcW, srcH, w, h) {
298
+ const out = new Float64Array(w * h * 3);
299
+ for (let y = 0; y < h; y++) {
300
+ const y0 = Math.floor((y * srcH) / h);
301
+ const y1 = Math.ceil(((y + 1) * srcH) / h);
302
+ for (let x = 0; x < w; x++) {
303
+ const x0 = Math.floor((x * srcW) / w);
304
+ const x1 = Math.ceil(((x + 1) * srcW) / w);
305
+ let rSum = 0, gSum = 0, bSum = 0, count = 0;
306
+ for (let iy = y0; iy < y1; iy++) {
307
+ if (iy >= srcH)
308
+ break;
309
+ for (let ix = x0; ix < x1; ix++) {
310
+ if (ix >= srcW)
311
+ break;
312
+ const idx = (iy * srcW + ix) * 4;
313
+ rSum += rgba[idx];
314
+ gSum += rgba[idx + 1];
315
+ bSum += rgba[idx + 2];
316
+ count++;
317
+ }
318
+ }
319
+ if (count === 0)
320
+ continue;
321
+ const r = rSum / count / 255.0;
322
+ const g = gSum / count / 255.0;
323
+ const b = bSum / count / 255.0;
324
+ const lab = srgbToOklab(r, g, b);
325
+ const idx = (y * w + x) * 3;
326
+ out[idx] = lab.l;
327
+ out[idx + 1] = lab.a;
328
+ out[idx + 2] = lab.b;
329
+ }
330
+ }
331
+ return out;
332
+ }
333
+ function srgbToOklab(r, g, b) {
334
+ const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
335
+ r = lin(r);
336
+ g = lin(g);
337
+ b = lin(b);
338
+ const l1 = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
339
+ const m1 = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
340
+ const s1 = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
341
+ const l_ = Math.cbrt(l1), m_ = Math.cbrt(m1), s_ = Math.cbrt(s1);
342
+ return {
343
+ l: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
344
+ a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
345
+ b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
346
+ };
347
+ }
348
+ function oklabToSrgb(l, a, b) {
349
+ const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
350
+ const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
351
+ const s_ = l - 0.0894841775 * a - 1.291485548 * b;
352
+ const l3 = l_ * l_ * l_;
353
+ const m3 = m_ * m_ * m_;
354
+ const s3 = s_ * s_ * s_;
355
+ let r = +4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
356
+ let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
357
+ let bl = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3;
358
+ const srt = (c) => c <= 0.0031308
359
+ ? 12.92 * c
360
+ : c < 0
361
+ ? 0
362
+ : 1.055 * Math.pow(c, 1.0 / 2.4) - 0.055;
363
+ return { r: srt(r), g: srt(g), b: srt(bl) };
364
+ }
365
+ // --- Bit Packing ---
366
+ class BitStream {
367
+ constructor() {
368
+ this.buf = [];
369
+ this.acc = 0n;
370
+ this.n = 0;
371
+ }
372
+ write(val, bits) {
373
+ this.acc = (this.acc << BigInt(bits)) | BigInt(val);
374
+ this.n += bits;
375
+ while (this.n >= 8) {
376
+ const shift = BigInt(this.n - 8);
377
+ const byteVal = Number((this.acc >> shift) & 0xffn);
378
+ this.buf.push(byteVal);
379
+ this.n -= 8;
380
+ }
381
+ }
382
+ getBytes() {
383
+ if (this.n > 0) {
384
+ this.buf.push(Number((this.acc << BigInt(8 - this.n)) & 0xffn));
385
+ }
386
+ return new Uint8Array(this.buf);
387
+ }
388
+ }
389
+ class BitReader {
390
+ constructor(data) {
391
+ this.pos = 0;
392
+ this.rem = 0;
393
+ this.curr = 0;
394
+ this.data = data;
395
+ }
396
+ read(bits) {
397
+ let val = 0;
398
+ let bitsRemaining = bits;
399
+ while (bitsRemaining > 0) {
400
+ if (this.rem === 0) {
401
+ if (this.pos >= this.data.length)
402
+ return val << bitsRemaining; // EOF
403
+ this.curr = this.data[this.pos++];
404
+ this.rem = 8;
405
+ }
406
+ // Optimization for partial read
407
+ const take = Math.min(this.rem, bitsRemaining);
408
+ const shift = this.rem - take;
409
+ const mask = (1 << take) - 1;
410
+ const chunk = (this.curr >> shift) & mask;
411
+ val = (val << take) | chunk;
412
+ this.rem -= take;
413
+ bitsRemaining -= take;
414
+ }
415
+ return val;
416
+ }
417
+ }
418
+ function packV4(mean, splats) {
419
+ const bw = new BitStream();
420
+ // Header (16 bits)
421
+ bw.write(mean, 16);
422
+ // Baryons (3x)
423
+ let count = 0;
424
+ for (const s of splats) {
425
+ if (s.isLepton)
426
+ continue;
427
+ if (count >= 3)
428
+ break;
429
+ const xi = clampi(Math.round(s.x * 15.0), 0, 15);
430
+ const yi = clampi(Math.round(s.y * 15.0), 0, 15);
431
+ const sigI = getSigmaIdx(s.sigma);
432
+ const lQ = quant(s.l, -0.8, 0.8, 4);
433
+ const aQ = quant(s.a, -0.4, 0.4, 4);
434
+ const bQ = quant(s.b, -0.4, 0.4, 4);
435
+ bw.write(xi, 4);
436
+ bw.write(yi, 4);
437
+ bw.write(sigI, 2);
438
+ bw.write(lQ, 4);
439
+ bw.write(aQ, 4);
440
+ bw.write(bQ, 4);
441
+ count++;
442
+ }
443
+ while (count < 3) {
444
+ bw.write(0, 22);
445
+ count++;
446
+ }
447
+ // Leptons (3x)
448
+ count = 0;
449
+ for (const s of splats) {
450
+ if (!s.isLepton)
451
+ continue;
452
+ if (count >= 3)
453
+ break;
454
+ const xi = clampi(Math.round(s.x * 15.0), 0, 15);
455
+ const yi = clampi(Math.round(s.y * 15.0), 0, 15);
456
+ const sigI = getSigmaIdx(s.sigma);
457
+ const lQ = quant(s.l, -0.8, 0.8, 5);
458
+ bw.write(xi, 4);
459
+ bw.write(yi, 4);
460
+ bw.write(sigI, 2);
461
+ bw.write(lQ, 5);
462
+ count++;
463
+ }
464
+ while (count < 3) {
465
+ bw.write(0, 15);
466
+ count++;
467
+ }
468
+ bw.write(0, 1); // Pad
469
+ return bw.getBytes();
470
+ }
471
+ function unpackV4(hash) {
472
+ const br = new BitReader(hash);
473
+ const meanMap = br.read(16);
474
+ const { l, a, b } = unpackMean(meanMap);
475
+ const splats = [];
476
+ // Baryons
477
+ for (let i = 0; i < 3; i++) {
478
+ const xi = br.read(4);
479
+ const yi = br.read(4);
480
+ const sigI = br.read(2);
481
+ const lQ = br.read(4);
482
+ const aQ = br.read(4);
483
+ const bQ = br.read(4);
484
+ if (xi === 0 && yi === 0 && lQ === 0 && aQ === 0 && bQ === 0)
485
+ continue;
486
+ splats.push({
487
+ x: xi / 15.0,
488
+ y: yi / 15.0,
489
+ sigma: exports.SIGMA_TABLE[sigI],
490
+ l: unquant(lQ, -0.8, 0.8, 4),
491
+ a: unquant(aQ, -0.4, 0.4, 4),
492
+ b: unquant(bQ, -0.4, 0.4, 4),
493
+ isLepton: false,
494
+ });
495
+ }
496
+ // Leptons
497
+ for (let i = 0; i < 3; i++) {
498
+ const xi = br.read(4);
499
+ const yi = br.read(4);
500
+ const sigI = br.read(2);
501
+ const lQ = br.read(5);
502
+ if (xi === 0 && yi === 0 && lQ === 0)
503
+ continue;
504
+ splats.push({
505
+ x: xi / 15.0,
506
+ y: yi / 15.0,
507
+ sigma: exports.SIGMA_TABLE[sigI],
508
+ l: unquant(lQ, -0.8, 0.8, 5),
509
+ a: 0,
510
+ b: 0,
511
+ isLepton: true,
512
+ });
513
+ }
514
+ return { meanL: l, meanA: a, meanB: b, splats };
515
+ }
516
+ function getSigmaIdx(s) {
517
+ let minD = 100.0;
518
+ let idx = 0;
519
+ for (let i = 0; i < exports.SIGMA_TABLE.length; i++) {
520
+ const d = Math.abs(exports.SIGMA_TABLE[i] - s);
521
+ if (d < minD) {
522
+ minD = d;
523
+ idx = i;
524
+ }
525
+ }
526
+ return idx;
527
+ }
528
+ function packMean(l, a, b) {
529
+ const li = clampi(Math.floor(l * 63.5), 0, 63);
530
+ const ai = clampi(Math.floor(((a + 0.2) / 0.4) * 31.5), 0, 31);
531
+ const bi = clampi(Math.floor(((b + 0.2) / 0.4) * 31.5), 0, 31);
532
+ return (li << 10) | (ai << 5) | bi;
533
+ }
534
+ function unpackMean(p) {
535
+ const li = (p >> 10) & 0x3f;
536
+ const ai = (p >> 5) & 0x1f;
537
+ const bi = p & 0x1f;
538
+ return {
539
+ l: li / 63.0,
540
+ a: (ai / 31.0) * 0.4 - 0.2,
541
+ b: (bi / 31.0) * 0.4 - 0.2,
542
+ };
543
+ }
544
+ function quant(v, min, max, bits) {
545
+ const steps = (1 << bits) - 1;
546
+ const norm = (v - min) / (max - min);
547
+ return clampi(Math.round(norm * steps), 0, steps);
548
+ }
549
+ function unquant(v, min, max, bits) {
550
+ const steps = (1 << bits) - 1;
551
+ const norm = v / steps;
552
+ return norm * (max - min) + min;
553
+ }
554
+ function clampi(v, min, max) {
555
+ return v < min ? min : v > max ? max : v;
556
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "splathash",
3
+ "version": "1.0.1",
4
+ "description": "SplatHash: compress any image to 16 bytes and reconstruct it",
5
+ "main": "dist/src/index.js",
6
+ "types": "dist/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/src/index.js",
10
+ "types": "./dist/src/index.d.ts"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist/src",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "image",
19
+ "hash",
20
+ "perceptual",
21
+ "thumbnail",
22
+ "blurhash"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/junevm/splathash"
27
+ },
28
+ "scripts": {
29
+ "test": "tsc && node --test dist/test/test.js",
30
+ "build": "tsc",
31
+ "example": "ts-node examples/simple.ts"
32
+ },
33
+ "author": "",
34
+ "license": "MIT",
35
+ "devDependencies": {
36
+ "@types/node": "^20.0.0",
37
+ "@types/sharp": "^0.31.1",
38
+ "sharp": "^0.34.5",
39
+ "ts-node": "^10.9.2",
40
+ "typescript": "^5.0.0"
41
+ }
42
+ }