rosetta-squint 1.1.0 → 1.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.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Browser entry for rosetta-squint.
3
+ *
4
+ * Same convenience API as the main entry except the path-based functions
5
+ * (which read from disk via `node:fs`) are omitted. Use the `_Bytes`
6
+ * variants in browsers; obtain bytes from `fetch(url).then(r => r.arrayBuffer())`,
7
+ * `FileReader`, drag-and-drop, etc.
8
+ *
9
+ * import { phashBytes } from "rosetta-squint/browser";
10
+ *
11
+ * const resp = await fetch("/photo.jpg");
12
+ * const bytes = new Uint8Array(await resp.arrayBuffer());
13
+ * const hash = await phashBytes(bytes, 8);
14
+ * console.log(hash.toString()); // "c3f8a1b27d0e4f96"
15
+ *
16
+ * Underlying decoders (mozjpeg, libwebp, libheif) run in browser via WASM.
17
+ * TIFF (utif2) and HEIC (libheif-js) are CommonJS — your bundler must
18
+ * support CJS→ESM interop (esbuild, vite, webpack 5+, rollup with
19
+ * @rollup/plugin-commonjs all do).
20
+ */
21
+ import * as rih from "rosetta-squint-hash/browser";
22
+ export type { Hash, RgbImage } from "rosetta-squint-hash/browser";
23
+ export type { Format } from "rosetta-squint-decode";
24
+ export { ImageMultiHash, hexToHash, hexToFlathash, hexToMultiHash, } from "rosetta-squint-hash/browser";
25
+ export declare function decodeBytes(bytes: Uint8Array): Promise<rih.RgbImage>;
26
+ export declare function averageHashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
27
+ export declare function phashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
28
+ export declare function phashSimpleBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
29
+ export declare function dhashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
30
+ export declare function dhashVerticalBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
31
+ export declare function whashHaarBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
32
+ export declare function whashDb4Bytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
33
+ export declare function whashDb4RobustBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
34
+ export declare function colorhashBytes(bytes: Uint8Array, binbits: number): Promise<rih.Hash>;
35
+ export declare function cropResistantHashBytes(bytes: Uint8Array, limitSegments?: number): Promise<rih.ImageMultiHash>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Browser entry for rosetta-squint.
3
+ *
4
+ * Same convenience API as the main entry except the path-based functions
5
+ * (which read from disk via `node:fs`) are omitted. Use the `_Bytes`
6
+ * variants in browsers; obtain bytes from `fetch(url).then(r => r.arrayBuffer())`,
7
+ * `FileReader`, drag-and-drop, etc.
8
+ *
9
+ * import { phashBytes } from "rosetta-squint/browser";
10
+ *
11
+ * const resp = await fetch("/photo.jpg");
12
+ * const bytes = new Uint8Array(await resp.arrayBuffer());
13
+ * const hash = await phashBytes(bytes, 8);
14
+ * console.log(hash.toString()); // "c3f8a1b27d0e4f96"
15
+ *
16
+ * Underlying decoders (mozjpeg, libwebp, libheif) run in browser via WASM.
17
+ * TIFF (utif2) and HEIC (libheif-js) are CommonJS — your bundler must
18
+ * support CJS→ESM interop (esbuild, vite, webpack 5+, rollup with
19
+ * @rollup/plugin-commonjs all do).
20
+ */
21
+ import { decode } from "rosetta-squint-decode";
22
+ import * as rih from "rosetta-squint-hash/browser";
23
+ export { ImageMultiHash, hexToHash, hexToFlathash, hexToMultiHash, } from "rosetta-squint-hash/browser";
24
+ function decodedToRgbImage(d) {
25
+ return {
26
+ width: d.width,
27
+ height: d.height,
28
+ data: d.data,
29
+ channels: d.channels,
30
+ };
31
+ }
32
+ export async function decodeBytes(bytes) {
33
+ const decoded = await decode(bytes);
34
+ return decodedToRgbImage(decoded);
35
+ }
36
+ // Bytes-based convenience hash functions (no path variants in browser).
37
+ export async function averageHashBytes(bytes, hashSize) {
38
+ return rih.averageHash(await decodeBytes(bytes), hashSize);
39
+ }
40
+ export async function phashBytes(bytes, hashSize) {
41
+ return rih.phash(await decodeBytes(bytes), hashSize);
42
+ }
43
+ export async function phashSimpleBytes(bytes, hashSize) {
44
+ return rih.phashSimple(await decodeBytes(bytes), hashSize);
45
+ }
46
+ export async function dhashBytes(bytes, hashSize) {
47
+ return rih.dhash(await decodeBytes(bytes), hashSize);
48
+ }
49
+ export async function dhashVerticalBytes(bytes, hashSize) {
50
+ return rih.dhashVertical(await decodeBytes(bytes), hashSize);
51
+ }
52
+ export async function whashHaarBytes(bytes, hashSize) {
53
+ return rih.whashHaar(await decodeBytes(bytes), hashSize);
54
+ }
55
+ export async function whashDb4Bytes(bytes, hashSize) {
56
+ return rih.whashDb4(await decodeBytes(bytes), hashSize);
57
+ }
58
+ export async function whashDb4RobustBytes(bytes, hashSize) {
59
+ return rih.whashDb4Robust(await decodeBytes(bytes), hashSize);
60
+ }
61
+ export async function colorhashBytes(bytes, binbits) {
62
+ return rih.colorhash(await decodeBytes(bytes), binbits);
63
+ }
64
+ export async function cropResistantHashBytes(bytes, limitSegments) {
65
+ return rih.cropResistantHash(await decodeBytes(bytes), limitSegments);
66
+ }
@@ -0,0 +1,57 @@
1
+ import * as rih from "rosetta-squint-hash";
2
+ export type { Hash } from "rosetta-squint-hash";
3
+ export type { Format } from "rosetta-squint-decode";
4
+ export { ImageMultiHash, hexToHash, hexToFlathash, hexToMultiHash } from "rosetta-squint-hash";
5
+ /**
6
+ * Maximum allowed size for path-based decode inputs. Refuse anything larger
7
+ * BEFORE reading bytes. Callers that genuinely need to process images larger
8
+ * than this should decode via rosetta-squint-decode directly after explicit
9
+ * validation.
10
+ */
11
+ export declare const MAX_FILE_SIZE: number;
12
+ /** Decode raw bytes (any supported format) into the rgb image shape used by the hash lib. */
13
+ export declare function decodeBytes(bytes: Uint8Array): Promise<rih.RgbImage>;
14
+ /** Read a file from disk and decode.
15
+ *
16
+ * Refuses symlinks (via `lstat`), non-regular files (FIFOs, /dev/zero,
17
+ * character devices, etc.) and files larger than MAX_FILE_SIZE BEFORE
18
+ * reading bytes — without these guards `readFile("/dev/zero")` would loop
19
+ * until OOM and a 300 MiB sparse file would allocate 300 MiB even though
20
+ * it contains no image. Callers who genuinely want symlink resolution
21
+ * must do it explicitly (e.g. `fs.promises.realpath`) before calling
22
+ * this function.
23
+ *
24
+ * Node's `fs.open` doesn't directly expose `O_NOFOLLOW`, so we lstat the
25
+ * path first and reject symlinks. The window between the lstat and the
26
+ * subsequent open is narrow — the attacker would have to swap the target
27
+ * between those two syscalls, much harder than swapping the symlink
28
+ * destination across an unrelated stat→read window.
29
+ *
30
+ * The file is then opened ONCE via `fs.open`, and `fhandle.stat()` plus
31
+ * `fhandle.read()` operate on the same fd. `fhandle.stat()` ultimately
32
+ * calls fstat(2) on the open descriptor, not stat(2) on the path, which
33
+ * closes the TOCTOU window between the size check and the read. The read
34
+ * is bounded by `MAX_FILE_SIZE + 1` so a concurrent writer that grows the
35
+ * file after the size check is still rejected.
36
+ */
37
+ export declare function decodeFile(path: string): Promise<rih.RgbImage>;
38
+ export declare function averageHash(path: string, hashSize: number): Promise<rih.Hash>;
39
+ export declare function averageHashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
40
+ export declare function phash(path: string, hashSize: number): Promise<rih.Hash>;
41
+ export declare function phashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
42
+ export declare function phashSimple(path: string, hashSize: number): Promise<rih.Hash>;
43
+ export declare function phashSimpleBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
44
+ export declare function dhash(path: string, hashSize: number): Promise<rih.Hash>;
45
+ export declare function dhashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
46
+ export declare function dhashVertical(path: string, hashSize: number): Promise<rih.Hash>;
47
+ export declare function dhashVerticalBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
48
+ export declare function whashHaar(path: string, hashSize: number): Promise<rih.Hash>;
49
+ export declare function whashHaarBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
50
+ export declare function whashDb4(path: string, hashSize: number): Promise<rih.Hash>;
51
+ export declare function whashDb4Bytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
52
+ export declare function whashDb4Robust(path: string, hashSize: number): Promise<rih.Hash>;
53
+ export declare function whashDb4RobustBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash>;
54
+ export declare function colorhash(path: string, binbits: number): Promise<rih.Hash>;
55
+ export declare function colorhashBytes(bytes: Uint8Array, binbits: number): Promise<rih.Hash>;
56
+ export declare function cropResistantHash(path: string, limitSegments?: number): Promise<rih.ImageMultiHash>;
57
+ export declare function cropResistantHashBytes(bytes: Uint8Array, limitSegments?: number): Promise<rih.ImageMultiHash>;
@@ -1,12 +1,7 @@
1
1
  import { open, lstat } from "node:fs/promises";
2
- import { decode, type DecodedImage } from "rosetta-squint-decode";
2
+ import { decode } from "rosetta-squint-decode";
3
3
  import * as rih from "rosetta-squint-hash";
4
-
5
- // Re-export key types and utilities from the underlying packages for ergonomics.
6
- export type { Hash } from "rosetta-squint-hash";
7
- export type { Format } from "rosetta-squint-decode";
8
4
  export { ImageMultiHash, hexToHash, hexToFlathash, hexToMultiHash } from "rosetta-squint-hash";
9
-
10
5
  /**
11
6
  * Maximum allowed size for path-based decode inputs. Refuse anything larger
12
7
  * BEFORE reading bytes. Callers that genuinely need to process images larger
@@ -14,9 +9,8 @@ export { ImageMultiHash, hexToHash, hexToFlathash, hexToMultiHash } from "rosett
14
9
  * validation.
15
10
  */
16
11
  export const MAX_FILE_SIZE = 256 * 1024 * 1024; // 256 MiB
17
-
18
12
  /** Adapt a rosetta-squint-decode DecodedImage into the rosetta-squint-hash RgbImage shape. */
19
- function decodedToRgbImage(d: DecodedImage): rih.RgbImage {
13
+ function decodedToRgbImage(d) {
20
14
  return {
21
15
  width: d.width,
22
16
  height: d.height,
@@ -24,13 +18,11 @@ function decodedToRgbImage(d: DecodedImage): rih.RgbImage {
24
18
  channels: d.channels,
25
19
  };
26
20
  }
27
-
28
21
  /** Decode raw bytes (any supported format) into the rgb image shape used by the hash lib. */
29
- export async function decodeBytes(bytes: Uint8Array): Promise<rih.RgbImage> {
22
+ export async function decodeBytes(bytes) {
30
23
  const decoded = await decode(bytes);
31
24
  return decodedToRgbImage(decoded);
32
25
  }
33
-
34
26
  /** Read a file from disk and decode.
35
27
  *
36
28
  * Refuses symlinks (via `lstat`), non-regular files (FIFOs, /dev/zero,
@@ -54,7 +46,7 @@ export async function decodeBytes(bytes: Uint8Array): Promise<rih.RgbImage> {
54
46
  * is bounded by `MAX_FILE_SIZE + 1` so a concurrent writer that grows the
55
47
  * file after the size check is still rejected.
56
48
  */
57
- export async function decodeFile(path: string): Promise<rih.RgbImage> {
49
+ export async function decodeFile(path) {
58
50
  // Lstat-then-open is unfortunately not atomic on Node — there's no
59
51
  // public API for O_NOFOLLOW. The race is much narrower than the
60
52
  // stat→read race we already close below.
@@ -69,11 +61,9 @@ export async function decodeFile(path: string): Promise<rih.RgbImage> {
69
61
  throw new Error(`not a regular file: ${path}`);
70
62
  }
71
63
  if (st.size > MAX_FILE_SIZE) {
72
- throw new Error(
73
- `input file too large: ${st.size} bytes (max ${MAX_FILE_SIZE} `
64
+ throw new Error(`input file too large: ${st.size} bytes (max ${MAX_FILE_SIZE} `
74
65
  + `bytes / 256 MiB). For images above this threshold, decode `
75
- + `via rosetta-squint-decode directly after explicit validation.`,
76
- );
66
+ + `via rosetta-squint-decode directly after explicit validation.`);
77
67
  }
78
68
  // Read up to MAX_FILE_SIZE+1 bytes so a concurrent writer that
79
69
  // grows the file post-stat is rejected rather than silently
@@ -82,102 +72,83 @@ export async function decodeFile(path: string): Promise<rih.RgbImage> {
82
72
  const buf = new Uint8Array(cap);
83
73
  let total = 0;
84
74
  while (total < cap) {
85
- const { bytesRead } = await fh.read(
86
- buf, total, cap - total, total,
87
- );
88
- if (bytesRead === 0) break;
75
+ const { bytesRead } = await fh.read(buf, total, cap - total, total);
76
+ if (bytesRead === 0)
77
+ break;
89
78
  total += bytesRead;
90
79
  }
91
80
  if (total > MAX_FILE_SIZE) {
92
- throw new Error(
93
- `input file too large: ${total} bytes (max ${MAX_FILE_SIZE} `
81
+ throw new Error(`input file too large: ${total} bytes (max ${MAX_FILE_SIZE} `
94
82
  + `bytes / 256 MiB). For images above this threshold, decode `
95
- + `via rosetta-squint-decode directly after explicit validation.`,
96
- );
83
+ + `via rosetta-squint-decode directly after explicit validation.`);
97
84
  }
98
85
  const bytes = buf.subarray(0, total);
99
86
  return decodeBytes(bytes);
100
- } finally {
87
+ }
88
+ finally {
101
89
  await fh.close();
102
90
  }
103
91
  }
104
-
105
92
  // ---------------------------------------------------------------------------
106
93
  // Convenience hash functions — each accepts either a file path or raw bytes.
107
94
  // ---------------------------------------------------------------------------
108
-
109
- export async function averageHash(path: string, hashSize: number): Promise<rih.Hash> {
95
+ export async function averageHash(path, hashSize) {
110
96
  return rih.averageHash(await decodeFile(path), hashSize);
111
97
  }
112
- export async function averageHashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
98
+ export async function averageHashBytes(bytes, hashSize) {
113
99
  return rih.averageHash(await decodeBytes(bytes), hashSize);
114
100
  }
115
-
116
- export async function phash(path: string, hashSize: number): Promise<rih.Hash> {
101
+ export async function phash(path, hashSize) {
117
102
  return rih.phash(await decodeFile(path), hashSize);
118
103
  }
119
- export async function phashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
104
+ export async function phashBytes(bytes, hashSize) {
120
105
  return rih.phash(await decodeBytes(bytes), hashSize);
121
106
  }
122
-
123
- export async function phashSimple(path: string, hashSize: number): Promise<rih.Hash> {
107
+ export async function phashSimple(path, hashSize) {
124
108
  return rih.phashSimple(await decodeFile(path), hashSize);
125
109
  }
126
- export async function phashSimpleBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
110
+ export async function phashSimpleBytes(bytes, hashSize) {
127
111
  return rih.phashSimple(await decodeBytes(bytes), hashSize);
128
112
  }
129
-
130
- export async function dhash(path: string, hashSize: number): Promise<rih.Hash> {
113
+ export async function dhash(path, hashSize) {
131
114
  return rih.dhash(await decodeFile(path), hashSize);
132
115
  }
133
- export async function dhashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
116
+ export async function dhashBytes(bytes, hashSize) {
134
117
  return rih.dhash(await decodeBytes(bytes), hashSize);
135
118
  }
136
-
137
- export async function dhashVertical(path: string, hashSize: number): Promise<rih.Hash> {
119
+ export async function dhashVertical(path, hashSize) {
138
120
  return rih.dhashVertical(await decodeFile(path), hashSize);
139
121
  }
140
- export async function dhashVerticalBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
122
+ export async function dhashVerticalBytes(bytes, hashSize) {
141
123
  return rih.dhashVertical(await decodeBytes(bytes), hashSize);
142
124
  }
143
-
144
- export async function whashHaar(path: string, hashSize: number): Promise<rih.Hash> {
125
+ export async function whashHaar(path, hashSize) {
145
126
  return rih.whashHaar(await decodeFile(path), hashSize);
146
127
  }
147
- export async function whashHaarBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
128
+ export async function whashHaarBytes(bytes, hashSize) {
148
129
  return rih.whashHaar(await decodeBytes(bytes), hashSize);
149
130
  }
150
-
151
- export async function whashDb4(path: string, hashSize: number): Promise<rih.Hash> {
131
+ export async function whashDb4(path, hashSize) {
152
132
  return rih.whashDb4(await decodeFile(path), hashSize);
153
133
  }
154
- export async function whashDb4Bytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
134
+ export async function whashDb4Bytes(bytes, hashSize) {
155
135
  return rih.whashDb4(await decodeBytes(bytes), hashSize);
156
136
  }
157
-
158
- export async function whashDb4Robust(path: string, hashSize: number): Promise<rih.Hash> {
137
+ export async function whashDb4Robust(path, hashSize) {
159
138
  return rih.whashDb4Robust(await decodeFile(path), hashSize);
160
139
  }
161
- export async function whashDb4RobustBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
140
+ export async function whashDb4RobustBytes(bytes, hashSize) {
162
141
  return rih.whashDb4Robust(await decodeBytes(bytes), hashSize);
163
142
  }
164
-
165
- export async function colorhash(path: string, binbits: number): Promise<rih.Hash> {
143
+ export async function colorhash(path, binbits) {
166
144
  return rih.colorhash(await decodeFile(path), binbits);
167
145
  }
168
- export async function colorhashBytes(bytes: Uint8Array, binbits: number): Promise<rih.Hash> {
146
+ export async function colorhashBytes(bytes, binbits) {
169
147
  return rih.colorhash(await decodeBytes(bytes), binbits);
170
148
  }
171
-
172
- export async function cropResistantHash(
173
- path: string,
174
- limitSegments?: number,
175
- ): Promise<rih.ImageMultiHash> {
149
+ export async function cropResistantHash(path, limitSegments) {
176
150
  return rih.cropResistantHash(await decodeFile(path), limitSegments);
177
151
  }
178
- export async function cropResistantHashBytes(
179
- bytes: Uint8Array,
180
- limitSegments?: number,
181
- ): Promise<rih.ImageMultiHash> {
152
+ export async function cropResistantHashBytes(bytes, limitSegments) {
182
153
  return rih.cropResistantHash(await decodeBytes(bytes), limitSegments);
183
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rosetta-squint",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Cross-language byte-exact perceptual image hashing — decode + hash in one call. Umbrella package combining rosetta-squint-decode + rosetta-squint-hash.",
5
5
  "keywords": [
6
6
  "perceptual-hashing",
@@ -21,6 +21,11 @@
21
21
  },
22
22
  "author": "Will Metcalf <william.metcalf@gmail.com>",
23
23
  "license": "BSD-2-Clause",
24
+ "files": [
25
+ "dist",
26
+ "scripts",
27
+ "README.md"
28
+ ],
24
29
  "type": "module",
25
30
  "main": "./dist/index.js",
26
31
  "types": "./dist/index.d.ts",
package/src/browser.ts DELETED
@@ -1,82 +0,0 @@
1
- /**
2
- * Browser entry for rosetta-squint.
3
- *
4
- * Same convenience API as the main entry except the path-based functions
5
- * (which read from disk via `node:fs`) are omitted. Use the `_Bytes`
6
- * variants in browsers; obtain bytes from `fetch(url).then(r => r.arrayBuffer())`,
7
- * `FileReader`, drag-and-drop, etc.
8
- *
9
- * import { phashBytes } from "rosetta-squint/browser";
10
- *
11
- * const resp = await fetch("/photo.jpg");
12
- * const bytes = new Uint8Array(await resp.arrayBuffer());
13
- * const hash = await phashBytes(bytes, 8);
14
- * console.log(hash.toString()); // "c3f8a1b27d0e4f96"
15
- *
16
- * Underlying decoders (mozjpeg, libwebp, libheif) run in browser via WASM.
17
- * TIFF (utif2) and HEIC (libheif-js) are CommonJS — your bundler must
18
- * support CJS→ESM interop (esbuild, vite, webpack 5+, rollup with
19
- * @rollup/plugin-commonjs all do).
20
- */
21
-
22
- import { decode, type DecodedImage } from "rosetta-squint-decode";
23
- import * as rih from "rosetta-squint-hash/browser";
24
-
25
- export type { Hash, RgbImage } from "rosetta-squint-hash/browser";
26
- export type { Format } from "rosetta-squint-decode";
27
- export {
28
- ImageMultiHash,
29
- hexToHash,
30
- hexToFlathash,
31
- hexToMultiHash,
32
- } from "rosetta-squint-hash/browser";
33
-
34
- function decodedToRgbImage(d: DecodedImage): rih.RgbImage {
35
- return {
36
- width: d.width,
37
- height: d.height,
38
- data: d.data,
39
- channels: d.channels,
40
- };
41
- }
42
-
43
- export async function decodeBytes(bytes: Uint8Array): Promise<rih.RgbImage> {
44
- const decoded = await decode(bytes);
45
- return decodedToRgbImage(decoded);
46
- }
47
-
48
- // Bytes-based convenience hash functions (no path variants in browser).
49
-
50
- export async function averageHashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
51
- return rih.averageHash(await decodeBytes(bytes), hashSize);
52
- }
53
- export async function phashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
54
- return rih.phash(await decodeBytes(bytes), hashSize);
55
- }
56
- export async function phashSimpleBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
57
- return rih.phashSimple(await decodeBytes(bytes), hashSize);
58
- }
59
- export async function dhashBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
60
- return rih.dhash(await decodeBytes(bytes), hashSize);
61
- }
62
- export async function dhashVerticalBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
63
- return rih.dhashVertical(await decodeBytes(bytes), hashSize);
64
- }
65
- export async function whashHaarBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
66
- return rih.whashHaar(await decodeBytes(bytes), hashSize);
67
- }
68
- export async function whashDb4Bytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
69
- return rih.whashDb4(await decodeBytes(bytes), hashSize);
70
- }
71
- export async function whashDb4RobustBytes(bytes: Uint8Array, hashSize: number): Promise<rih.Hash> {
72
- return rih.whashDb4Robust(await decodeBytes(bytes), hashSize);
73
- }
74
- export async function colorhashBytes(bytes: Uint8Array, binbits: number): Promise<rih.Hash> {
75
- return rih.colorhash(await decodeBytes(bytes), binbits);
76
- }
77
- export async function cropResistantHashBytes(
78
- bytes: Uint8Array,
79
- limitSegments?: number,
80
- ) {
81
- return rih.cropResistantHash(await decodeBytes(bytes), limitSegments);
82
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * Browser-entry smoke test for rosetta-squint.
3
- *
4
- * Imports `rosetta-squint/browser` and verifies:
5
- * 1. All bytes-based convenience functions are exported and callable
6
- * 2. Path-based functions are NOT exported (no `node:fs`)
7
- * 3. Hash output matches the main entry for the same byte input
8
- * (no behavioral drift between entries)
9
- *
10
- * vitest runs this in Node, so we can't directly verify browser execution,
11
- * but the import-tree check + dynamic-import-of-node:fs branch logic in
12
- * loadWasm.ts means the same dist/browser.js bundle that loads here will
13
- * load in browsers when fetched via CDN / served by a bundler.
14
- */
15
-
16
- import { describe, expect, it } from "vitest";
17
- import { readFile } from "node:fs/promises";
18
- import * as browserEntry from "../src/browser.js";
19
- import * as mainEntry from "../src/index.js";
20
-
21
- const FIXTURES = new URL("../../../../hash/spec/fixtures/", import.meta.url);
22
-
23
- describe("rosetta-squint/browser entry", () => {
24
- it("exposes bytes-based functions for all 10 algorithms", () => {
25
- expect(typeof browserEntry.averageHashBytes).toBe("function");
26
- expect(typeof browserEntry.phashBytes).toBe("function");
27
- expect(typeof browserEntry.phashSimpleBytes).toBe("function");
28
- expect(typeof browserEntry.dhashBytes).toBe("function");
29
- expect(typeof browserEntry.dhashVerticalBytes).toBe("function");
30
- expect(typeof browserEntry.whashHaarBytes).toBe("function");
31
- expect(typeof browserEntry.whashDb4Bytes).toBe("function");
32
- expect(typeof browserEntry.whashDb4RobustBytes).toBe("function");
33
- expect(typeof browserEntry.colorhashBytes).toBe("function");
34
- expect(typeof browserEntry.cropResistantHashBytes).toBe("function");
35
- expect(typeof browserEntry.decodeBytes).toBe("function");
36
- });
37
-
38
- it("does NOT expose path-based functions (no node:fs)", () => {
39
- // @ts-expect-error — phash (path) is intentionally not exported from /browser
40
- expect(browserEntry.phash).toBeUndefined();
41
- // @ts-expect-error
42
- expect(browserEntry.decodeFile).toBeUndefined();
43
- });
44
-
45
- it("phashBytes on imagehash.png matches main entry's phash", async () => {
46
- const path = new URL("imagehash.png", FIXTURES);
47
- const bytes = new Uint8Array(await readFile(path));
48
- const hBrowser = await browserEntry.phashBytes(bytes, 8);
49
- const hMain = await mainEntry.phashBytes(bytes, 8);
50
- expect(hBrowser.toString()).toBe(hMain.toString());
51
- // And matches the cross-port reference value from Go/Java/JS reports
52
- expect(hBrowser.toString()).toBe("ba8c84536bd3c366");
53
- });
54
-
55
- it("dhashBytes on imagehash.png produces non-empty hex", async () => {
56
- const path = new URL("imagehash.png", FIXTURES);
57
- const bytes = new Uint8Array(await readFile(path));
58
- const h = await browserEntry.dhashBytes(bytes, 8);
59
- expect(h.toString()).toMatch(/^[0-9a-f]+$/);
60
- expect(h.toString().length).toBe(16);
61
- });
62
-
63
- it("decodeBytes produces an RgbImage shape", async () => {
64
- const path = new URL("imagehash.png", FIXTURES);
65
- const bytes = new Uint8Array(await readFile(path));
66
- const img = await browserEntry.decodeBytes(bytes);
67
- expect(img.width).toBeGreaterThan(0);
68
- expect(img.height).toBeGreaterThan(0);
69
- expect(img.channels === 3 || img.channels === 4).toBe(true);
70
- expect(img.data.byteLength).toBe(img.width * img.height * img.channels);
71
- });
72
- });
@@ -1,325 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { readFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
-
5
- import {
6
- phash, phashBytes,
7
- averageHash, averageHashBytes,
8
- dhash, dhashBytes,
9
- dhashVertical, dhashVerticalBytes,
10
- phashSimple, phashSimpleBytes,
11
- whashHaar, whashHaarBytes,
12
- whashDb4, whashDb4Bytes,
13
- colorhash, colorhashBytes,
14
- cropResistantHash, cropResistantHashBytes,
15
- decodeFile,
16
- hexToHash, hexToFlathash, hexToMultiHash,
17
- ImageMultiHash,
18
- } from "../src/index.js";
19
- import * as rih from "rosetta-squint-hash";
20
-
21
- const FIXTURES_PNG = join(
22
- new URL(".", import.meta.url).pathname,
23
- "../../../../decode/spec/fixtures/png/valid",
24
- );
25
- const FIXTURES_JPEG = join(
26
- new URL(".", import.meta.url).pathname,
27
- "../../../../decode/spec/fixtures/jpeg/valid",
28
- );
29
-
30
- const PNG_FILES = [
31
- "imagehash.png",
32
- "peppers.png",
33
- "checker-256.png",
34
- ];
35
- const JPEG_FILES = [
36
- "larger-photo-128.jpg",
37
- "64x64-quality-50.jpg",
38
- ];
39
-
40
- // ---------------------------------------------------------------------------
41
- // phash
42
- // ---------------------------------------------------------------------------
43
- describe("phash", () => {
44
- for (const name of PNG_FILES) {
45
- const path = join(FIXTURES_PNG, name);
46
-
47
- it(`returns a Hash for ${name}`, async () => {
48
- const h = await phash(path, 8);
49
- expect(h).toBeInstanceOf(rih.Hash);
50
- expect(h.toHex().length).toBe(16); // 8*8 bits = 64 bits = 16 hex chars
51
- });
52
-
53
- it(`phash === phashBytes for ${name}`, async () => {
54
- const [hPath, hBytes] = await Promise.all([
55
- phash(path, 8),
56
- readFile(path).then(b => phashBytes(new Uint8Array(b), 8)),
57
- ]);
58
- expect(hPath.toHex()).toBe(hBytes.toHex());
59
- });
60
-
61
- it(`phash matches rih.phash(decodeFile) for ${name}`, async () => {
62
- const [hSquint, img] = await Promise.all([
63
- phash(path, 8),
64
- decodeFile(path),
65
- ]);
66
- const hRih = rih.phash(img, 8);
67
- expect(hSquint.toHex()).toBe(hRih.toHex());
68
- });
69
- }
70
-
71
- for (const name of JPEG_FILES) {
72
- const path = join(FIXTURES_JPEG, name);
73
-
74
- it(`returns a Hash for ${name}`, async () => {
75
- const h = await phash(path, 8);
76
- expect(h).toBeInstanceOf(rih.Hash);
77
- });
78
-
79
- it(`phash === phashBytes for ${name}`, async () => {
80
- const [hPath, hBytes] = await Promise.all([
81
- phash(path, 8),
82
- readFile(path).then(b => phashBytes(new Uint8Array(b), 8)),
83
- ]);
84
- expect(hPath.toHex()).toBe(hBytes.toHex());
85
- });
86
- }
87
- });
88
-
89
- // ---------------------------------------------------------------------------
90
- // averageHash
91
- // ---------------------------------------------------------------------------
92
- describe("averageHash", () => {
93
- for (const name of PNG_FILES) {
94
- const path = join(FIXTURES_PNG, name);
95
-
96
- it(`returns a Hash for ${name}`, async () => {
97
- const h = await averageHash(path, 8);
98
- expect(h).toBeInstanceOf(rih.Hash);
99
- });
100
-
101
- it(`averageHash === averageHashBytes for ${name}`, async () => {
102
- const [hPath, hBytes] = await Promise.all([
103
- averageHash(path, 8),
104
- readFile(path).then(b => averageHashBytes(new Uint8Array(b), 8)),
105
- ]);
106
- expect(hPath.toHex()).toBe(hBytes.toHex());
107
- });
108
-
109
- it(`averageHash matches rih.averageHash(decodeFile) for ${name}`, async () => {
110
- const [hSquint, img] = await Promise.all([
111
- averageHash(path, 8),
112
- decodeFile(path),
113
- ]);
114
- const hRih = rih.averageHash(img, 8);
115
- expect(hSquint.toHex()).toBe(hRih.toHex());
116
- });
117
- }
118
- });
119
-
120
- // ---------------------------------------------------------------------------
121
- // dhash
122
- // ---------------------------------------------------------------------------
123
- describe("dhash", () => {
124
- for (const name of ["imagehash.png", "checker-256.png"]) {
125
- const path = join(FIXTURES_PNG, name);
126
-
127
- it(`returns a Hash for ${name}`, async () => {
128
- const h = await dhash(path, 8);
129
- expect(h).toBeInstanceOf(rih.Hash);
130
- });
131
-
132
- it(`dhash === dhashBytes for ${name}`, async () => {
133
- const [hPath, hBytes] = await Promise.all([
134
- dhash(path, 8),
135
- readFile(path).then(b => dhashBytes(new Uint8Array(b), 8)),
136
- ]);
137
- expect(hPath.toHex()).toBe(hBytes.toHex());
138
- });
139
- }
140
- });
141
-
142
- // ---------------------------------------------------------------------------
143
- // dhashVertical
144
- // ---------------------------------------------------------------------------
145
- describe("dhashVertical", () => {
146
- for (const name of ["imagehash.png", "checker-256.png"]) {
147
- const path = join(FIXTURES_PNG, name);
148
-
149
- it(`returns a Hash for ${name}`, async () => {
150
- const h = await dhashVertical(path, 8);
151
- expect(h).toBeInstanceOf(rih.Hash);
152
- });
153
-
154
- it(`dhashVertical === dhashVerticalBytes for ${name}`, async () => {
155
- const [hPath, hBytes] = await Promise.all([
156
- dhashVertical(path, 8),
157
- readFile(path).then(b => dhashVerticalBytes(new Uint8Array(b), 8)),
158
- ]);
159
- expect(hPath.toHex()).toBe(hBytes.toHex());
160
- });
161
- }
162
- });
163
-
164
- // ---------------------------------------------------------------------------
165
- // phashSimple
166
- // ---------------------------------------------------------------------------
167
- describe("phashSimple", () => {
168
- for (const name of ["imagehash.png", "peppers.png"]) {
169
- const path = join(FIXTURES_PNG, name);
170
-
171
- it(`returns a Hash for ${name}`, async () => {
172
- const h = await phashSimple(path, 8);
173
- expect(h).toBeInstanceOf(rih.Hash);
174
- });
175
-
176
- it(`phashSimple === phashSimpleBytes for ${name}`, async () => {
177
- const [hPath, hBytes] = await Promise.all([
178
- phashSimple(path, 8),
179
- readFile(path).then(b => phashSimpleBytes(new Uint8Array(b), 8)),
180
- ]);
181
- expect(hPath.toHex()).toBe(hBytes.toHex());
182
- });
183
- }
184
- });
185
-
186
- // ---------------------------------------------------------------------------
187
- // whashHaar
188
- // ---------------------------------------------------------------------------
189
- describe("whashHaar", () => {
190
- for (const name of ["imagehash.png", "peppers.png"]) {
191
- const path = join(FIXTURES_PNG, name);
192
-
193
- it(`returns a Hash for ${name}`, async () => {
194
- const h = await whashHaar(path, 8);
195
- expect(h).toBeInstanceOf(rih.Hash);
196
- });
197
-
198
- it(`whashHaar === whashHaarBytes for ${name}`, async () => {
199
- const [hPath, hBytes] = await Promise.all([
200
- whashHaar(path, 8),
201
- readFile(path).then(b => whashHaarBytes(new Uint8Array(b), 8)),
202
- ]);
203
- expect(hPath.toHex()).toBe(hBytes.toHex());
204
- });
205
- }
206
- });
207
-
208
- // ---------------------------------------------------------------------------
209
- // whashDb4
210
- // ---------------------------------------------------------------------------
211
- describe("whashDb4", () => {
212
- for (const name of ["imagehash.png", "peppers.png"]) {
213
- const path = join(FIXTURES_PNG, name);
214
-
215
- it(`returns a Hash for ${name}`, async () => {
216
- const h = await whashDb4(path, 8);
217
- expect(h).toBeInstanceOf(rih.Hash);
218
- });
219
-
220
- it(`whashDb4 === whashDb4Bytes for ${name}`, async () => {
221
- const [hPath, hBytes] = await Promise.all([
222
- whashDb4(path, 8),
223
- readFile(path).then(b => whashDb4Bytes(new Uint8Array(b), 8)),
224
- ]);
225
- expect(hPath.toHex()).toBe(hBytes.toHex());
226
- });
227
- }
228
- });
229
-
230
- // ---------------------------------------------------------------------------
231
- // colorhash
232
- // ---------------------------------------------------------------------------
233
- describe("colorhash", () => {
234
- for (const name of ["imagehash.png", "peppers.png"]) {
235
- const path = join(FIXTURES_PNG, name);
236
-
237
- it(`returns a Hash for ${name}`, async () => {
238
- const h = await colorhash(path, 3);
239
- expect(h).toBeInstanceOf(rih.Hash);
240
- });
241
-
242
- it(`colorhash === colorhashBytes for ${name}`, async () => {
243
- const [hPath, hBytes] = await Promise.all([
244
- colorhash(path, 3),
245
- readFile(path).then(b => colorhashBytes(new Uint8Array(b), 3)),
246
- ]);
247
- expect(hPath.toHex()).toBe(hBytes.toHex());
248
- });
249
-
250
- it(`colorhash matches rih.colorhash(decodeFile) for ${name}`, async () => {
251
- const [hSquint, img] = await Promise.all([
252
- colorhash(path, 3),
253
- decodeFile(path),
254
- ]);
255
- const hRih = rih.colorhash(img, 3);
256
- expect(hSquint.toHex()).toBe(hRih.toHex());
257
- });
258
- }
259
- });
260
-
261
- // ---------------------------------------------------------------------------
262
- // cropResistantHash
263
- // ---------------------------------------------------------------------------
264
- describe("cropResistantHash", () => {
265
- for (const name of ["imagehash.png", "peppers.png"]) {
266
- const path = join(FIXTURES_PNG, name);
267
-
268
- it(`returns an ImageMultiHash for ${name}`, async () => {
269
- const mh = await cropResistantHash(path);
270
- expect(mh).toBeInstanceOf(ImageMultiHash);
271
- expect(mh.segmentHashes.length).toBeGreaterThan(0);
272
- });
273
-
274
- it(`cropResistantHash === cropResistantHashBytes for ${name}`, async () => {
275
- const [mhPath, mhBytes] = await Promise.all([
276
- cropResistantHash(path),
277
- readFile(path).then(b => cropResistantHashBytes(new Uint8Array(b))),
278
- ]);
279
- expect(mhPath.toString()).toBe(mhBytes.toString());
280
- });
281
-
282
- it(`cropResistantHash matches rih.cropResistantHash(decodeFile) for ${name}`, async () => {
283
- const [mhSquint, img] = await Promise.all([
284
- cropResistantHash(path),
285
- decodeFile(path),
286
- ]);
287
- const mhRih = rih.cropResistantHash(img);
288
- expect(mhSquint.toString()).toBe(mhRih.toString());
289
- });
290
- }
291
- });
292
-
293
- // ---------------------------------------------------------------------------
294
- // Re-export sanity checks
295
- // ---------------------------------------------------------------------------
296
- describe("re-exports", () => {
297
- it("hexToHash round-trips a 16-char hex string", () => {
298
- const hex = "ffffffffffffffff";
299
- const h = hexToHash(hex);
300
- expect(h.toHex()).toBe(hex);
301
- });
302
-
303
- it("hexToFlathash round-trips a colorhash string", async () => {
304
- const path = join(FIXTURES_PNG, "imagehash.png");
305
- const h = await colorhash(path, 3);
306
- const hex = h.toHex();
307
- const h2 = hexToFlathash(hex, 3);
308
- expect(h2.toHex()).toBe(hex);
309
- });
310
-
311
- it("hexToMultiHash round-trips a cropResistantHash string", async () => {
312
- const path = join(FIXTURES_PNG, "imagehash.png");
313
- const mh = await cropResistantHash(path);
314
- const str = mh.toString();
315
- const mh2 = hexToMultiHash(str);
316
- expect(mh2.toString()).toBe(str);
317
- });
318
-
319
- it("ImageMultiHash is exported and constructible", () => {
320
- const bits = [[true, false], [false, true]];
321
- const h = new rih.Hash(bits);
322
- const mh = new ImageMultiHash([h]);
323
- expect(mh.segmentHashes.length).toBe(1);
324
- });
325
- });
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "Node16",
5
- "moduleResolution": "Node16",
6
- "strict": true,
7
- "declaration": true,
8
- "outDir": "dist",
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "isolatedModules": true,
12
- "resolveJsonModule": true,
13
- "noUncheckedIndexedAccess": false
14
- },
15
- "include": ["src/**/*"]
16
- }
package/vitest.config.ts DELETED
@@ -1,7 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- include: ["tests/**/*.test.ts"],
6
- },
7
- });