rn-remove-image-bg 0.0.23 → 0.0.24

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 CHANGED
@@ -324,26 +324,48 @@ try {
324
324
 
325
325
  > **First-time Use**: On Android, the ML Kit model downloads automatically on first use. The library waits for the download to complete (up to ~15 seconds with retries) before processing. Subsequent calls are instant.
326
326
 
327
- ### Web
328
-
329
- 1. **Add the Script**: You must add the `@imgly/background-removal` setup to your web `index.html`.
330
- Since the library is distributed as an ES Module, you need to import it and assign it to the window object:
331
- ```html
332
- <script type="module">
333
- import { removeBackground } from "https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.7.0/+esm";
334
- window.imglyRemoveBackground = removeBackground;
335
- </script>
336
- ```
327
+ ### Web (React Native Web)
328
+
329
+ This package supports **React Native Web** via the `@imgly/background-removal` library.
330
+
331
+ #### Setup
332
+
333
+ No manual setup is required! The library is installed automatically via npm and works with Metro bundler out of the box.
334
+
335
+ #### Usage
336
+
337
+ Use `removeBgImage` exactly like on native platforms.
338
+
339
+ ```typescript
340
+ import { removeBgImage } from 'rn-remove-image-bg';
341
+
342
+ const result = await removeBgImage(uri, {
343
+ maxDimension: 1024,
344
+ onProgress: (p) => console.log(p),
345
+ // Web-specific: Path to custom hosted models (optional)
346
+ // If omitted, models are downloaded from @imgly CDN (~30MB)
347
+ publicPath: '/assets/models/'
348
+ });
349
+ ```
350
+
351
+ #### Asset Handling & Offline Support
352
+
353
+ By default, the library downloads AI models (~30MB) from the **Internet (CDN)** on the first run and caches them in the browser (IndexedDB). This keeps your app bundle small (~2MB).
354
+
355
+ **For Offline Support / Self-Hosting:**
356
+ 1. Copy the `.wasm` and `.onnx` files from `node_modules/@imgly/background-removal/dist/` to your web `public/models` folder.
357
+ 2. Pass `publicPath: '/models/'` (or your chosen path) in the options.
358
+
359
+ #### Compatibility
337
360
 
338
- 2. **Usage**: The library will automatically detect the global `imglyRemoveBackground` function.
361
+ - **Browsers**: Chrome, Edge, Firefox, Safari (versions with WebGPU or WebGL2 support).
362
+ - **Bundlers**: Works with **Metro** (Expo) out of the box.
363
+ - **CORS**: If loading images from remote URLs, ensure they have `Access-Control-Allow-Origin: *` headers.
339
364
 
340
- - **Technology**: [@imgly/background-removal](https://github.com/imgly/background-removal-js) running in a Web Worker (managed by the library).
341
- - **Performance**: Zero UI blocking.
342
- - **Output**: Data URL (`data:image/png;base64,...`) or WEBP Blob.
343
- - **Requirements**:
344
- - **CORS**: Images loaded from external URLs must be CORS-enabled (`Access-Control-Allow-Origin: *`).
365
+ #### Troubleshooting (Web)
345
366
 
346
- > **Note**: The first call will download the WASM model files (~10MB) from the CDN. Ensure your CSP allows `cdn.jsdelivr.net`.
367
+ - **"Resource not found"**: If you see 404s for `.wasm` files, ensure you have internet access (for CDN) or have correctly configured `publicPath`.
368
+ - **CORS Errors**: "Tainted canvas" or "SecurityError" means the image server didn't send CORS headers. Proxy the image or configure your S3 bucket/server.
347
369
 
348
370
  ---
349
371
 
@@ -1,32 +1,25 @@
1
+ import type { RemoveBgImageOptions, CompressImageOptions, GenerateThumbhashOptions, OutputFormat } from './web/core/types';
2
+ export type { RemoveBgImageOptions, CompressImageOptions, GenerateThumbhashOptions, OutputFormat };
3
+ export type NativeRemoveBackgroundOptions = RemoveBgImageOptions;
1
4
  /**
2
- * Web implementation using @imgly/background-removal via Script Tag.
3
- * Requires the script to be added manually to index.html.
5
+ * Remove background from image (Web Implementation)
4
6
  */
5
- export type OutputFormat = 'PNG' | 'WEBP';
6
- export interface RemoveBgImageOptions {
7
- format?: OutputFormat;
8
- quality?: number;
9
- onProgress?: (progress: number) => void;
10
- debug?: boolean;
11
- }
12
7
  export declare function removeBgImage(uri: string, options?: RemoveBgImageOptions): Promise<string>;
8
+ /**
9
+ * Backward compatibility alias
10
+ * @deprecated Use removeBgImage
11
+ */
13
12
  export declare const removeBackground: typeof removeBgImage;
14
- export interface CompressImageOptions {
15
- maxSizeKB?: number;
16
- width?: number;
17
- height?: number;
18
- quality?: number;
19
- format?: 'webp' | 'png' | 'jpeg';
20
- }
21
- export interface GenerateThumbhashOptions {
22
- size?: number;
23
- }
13
+ /**
14
+ * Compress image (Web Implementation)
15
+ */
24
16
  export declare function compressImage(uri: string, options?: CompressImageOptions): Promise<string>;
25
- export declare function generateThumbhash(imageUri: string, options?: GenerateThumbhashOptions): Promise<string>;
26
- export declare function clearCache(): Promise<void>;
17
+ /**
18
+ * Generate thumbhash (Web Implementation)
19
+ */
20
+ export declare function generateThumbhash(uri: string, _options?: GenerateThumbhashOptions): Promise<string>;
21
+ export declare function clearCache(_deleteFiles?: boolean): Promise<void>;
27
22
  export declare function getCacheSize(): number;
28
- export declare function onLowMemory(): Promise<number>;
29
- export declare function configureCache(_config: {
30
- maxEntries?: number;
31
- }): void;
23
+ export declare function onLowMemory(_deleteFiles?: boolean): Promise<number>;
24
+ export declare function configureCache(_config: Record<string, unknown>): void;
32
25
  export declare function getCacheDirectory(): string;
@@ -1,126 +1,80 @@
1
+ import { BackgroundRemover } from './web/core/BackgroundRemover';
2
+ import { cacheManager } from './web/core/CacheManager';
3
+ import { compressImage as compressImageWeb } from './web/utils/CompressImage';
4
+ import { generateThumbhash as generateThumbhashWeb } from './web/utils/ThumbhashGenerator';
5
+ import { blobToDataUrl } from './web/utils/formatConverter';
1
6
  /**
2
- * Web implementation using @imgly/background-removal via Script Tag.
3
- * Requires the script to be added manually to index.html.
7
+ * Remove background from image (Web Implementation)
4
8
  */
5
9
  export async function removeBgImage(uri, options = {}) {
6
- const { onProgress, debug = false } = options;
7
- // Safety check
8
- // @ts-ignore
9
- if (typeof window !== 'undefined' && !window.imglyRemoveBackground) {
10
- // Also check if user defined it as imglyRemoveBackground globally
11
- // @ts-ignore
12
- if (typeof imglyRemoveBackground !== 'undefined') {
13
- // @ts-ignore
14
- window.imglyRemoveBackground = imglyRemoveBackground;
10
+ const { onProgress, useCache = true, debug = false } = options;
11
+ if (debug)
12
+ console.log('[Web] removeBgImage called with:', uri, options);
13
+ // 1. Check Cache
14
+ if (useCache) {
15
+ const cached = cacheManager.get(uri, options);
16
+ if (cached) {
17
+ if (debug)
18
+ console.log('[Web] Cache hit');
19
+ onProgress?.(100);
20
+ return cached;
15
21
  }
16
22
  }
17
- // @ts-ignore
18
- if (typeof window.imglyRemoveBackground === 'undefined') {
19
- throw new Error('[rn-remove-image-bg] Library not found. Please add the following script to your web index.html:\n' +
20
- '<script type="module">\n' +
21
- ' import { removeBackground } from "https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.7.0/+esm";\n' +
22
- ' window.imglyRemoveBackground = removeBackground;\n' +
23
- '</script>');
24
- }
25
- if (debug)
26
- console.log('[rmbg] Starting...');
27
- onProgress?.(1);
28
- // Config for imgly
29
- const config = {
30
- debug: debug,
31
- // Point publicPath to CDN for assets (wasm/models)
32
- publicPath: 'https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.7.0/dist/',
33
- progress: (_key, current, total) => {
34
- if (onProgress && total > 0) {
35
- onProgress((current / total) * 100);
36
- }
23
+ // 2. Process
24
+ onProgress?.(10); // Start
25
+ const blob = await BackgroundRemover.remove(uri, {
26
+ ...options,
27
+ onProgress: (p) => {
28
+ // Map progress to 10-90 range to leave room for start/end
29
+ const mapped = 10 + Math.round((p * 0.8));
30
+ onProgress?.(mapped);
37
31
  }
38
- };
39
- // @ts-ignore
40
- const blob = await window.imglyRemoveBackground(uri, config);
41
- onProgress?.(100);
42
- // Convert blob to DataURL
43
- return new Promise((resolve, reject) => {
44
- const reader = new FileReader();
45
- reader.onloadend = () => resolve(reader.result);
46
- reader.onerror = reject;
47
- reader.readAsDataURL(blob);
48
32
  });
33
+ // 3. Convert to Data URL
34
+ const dataUrl = await blobToDataUrl(blob);
35
+ onProgress?.(100);
36
+ // 4. Cache Result
37
+ if (useCache) {
38
+ cacheManager.set(uri, options, dataUrl);
39
+ }
40
+ return dataUrl;
49
41
  }
42
+ /**
43
+ * Backward compatibility alias
44
+ * @deprecated Use removeBgImage
45
+ */
50
46
  export const removeBackground = removeBgImage;
51
- // ==========================================
52
- // UTILITIES (Main Thread - lightweight)
53
- // ==========================================
54
- // Helper to load image
55
- function loadImage(src) {
56
- return new Promise((resolve, reject) => {
57
- const img = new Image();
58
- img.crossOrigin = 'anonymous';
59
- img.onload = () => resolve(img);
60
- img.onerror = reject;
61
- img.src = src;
62
- });
63
- }
47
+ /**
48
+ * Compress image (Web Implementation)
49
+ */
64
50
  export async function compressImage(uri, options = {}) {
65
- const { maxSizeKB = 250, width = 1024, height = 1024, quality = 0.85, format = 'webp', } = options;
66
- try {
67
- const img = await loadImage(uri);
68
- const scale = Math.min(width / img.width, height / img.height, 1);
69
- const targetWidth = Math.round(img.width * scale);
70
- const targetHeight = Math.round(img.height * scale);
71
- const canvas = document.createElement('canvas');
72
- canvas.width = targetWidth;
73
- canvas.height = targetHeight;
74
- const ctx = canvas.getContext('2d');
75
- if (!ctx)
76
- throw new Error('Could not get canvas context');
77
- ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
78
- const mimeType = format === 'png' ? 'image/png' : format === 'jpeg' ? 'image/jpeg' : 'image/webp';
79
- let dataUrl = canvas.toDataURL(mimeType, quality);
80
- // Reduce quality if over size limit
81
- let currentQuality = quality;
82
- const getSize = (url) => ((url.split(',')[1] || '').length * 3) / 4 / 1024;
83
- while (getSize(dataUrl) > maxSizeKB && currentQuality > 0.5) {
84
- currentQuality -= 0.1;
85
- dataUrl = canvas.toDataURL(mimeType, currentQuality);
86
- }
87
- return dataUrl;
88
- }
89
- catch (error) {
90
- console.warn('[rmbg] compressImage failed:', error);
91
- return uri;
92
- }
51
+ return compressImageWeb(uri, options);
93
52
  }
94
- export async function generateThumbhash(imageUri, options = {}) {
95
- const { size = 32 } = options;
96
- try {
97
- const img = await loadImage(imageUri);
98
- const canvas = document.createElement('canvas');
99
- canvas.width = size;
100
- canvas.height = size;
101
- const ctx = canvas.getContext('2d');
102
- if (!ctx)
103
- throw new Error('Could not get canvas context');
104
- ctx.drawImage(img, 0, 0, size, size);
105
- const imageData = ctx.getImageData(0, 0, size, size);
106
- // Load thumbhash from CDN
107
- // @ts-ignore
108
- const { rgbaToThumbHash } = await import(/* webpackIgnore: true */ 'https://cdn.jsdelivr.net/npm/thumbhash@0.1/+esm');
109
- const hash = rgbaToThumbHash(size, size, imageData.data);
110
- return btoa(String.fromCharCode(...hash));
111
- }
112
- catch (error) {
113
- console.warn('[rmbg] generateThumbhash failed:', error);
114
- return '';
115
- }
53
+ /**
54
+ * Generate thumbhash (Web Implementation)
55
+ */
56
+ export async function generateThumbhash(uri, _options = {}) {
57
+ // Web implementation currently doesn't use options (size hardcoded or auto-scaled)
58
+ return generateThumbhashWeb(uri);
59
+ }
60
+ // Cache Management APIs
61
+ export async function clearCache(_deleteFiles = false) {
62
+ cacheManager.clear();
63
+ console.log('[Web] Cache cleared');
64
+ }
65
+ export function getCacheSize() {
66
+ return cacheManager.size();
67
+ }
68
+ export async function onLowMemory(_deleteFiles = true) {
69
+ const size = cacheManager.size();
70
+ cacheManager.clear();
71
+ console.log(`[Web] Cleared ${size} items due to low memory`);
72
+ return size;
116
73
  }
117
- export async function clearCache() {
118
- // Caching is handled internally by the library
74
+ export function configureCache(_config) {
75
+ // Web cache is simple in-memory LRU, config not fully supported yet but stubbed
76
+ console.log('[Web] Cache configuration updated (no-op on web)');
119
77
  }
120
- export function getCacheSize() { return 0; }
121
- export async function onLowMemory() {
122
- await clearCache();
123
- return 0;
78
+ export function getCacheDirectory() {
79
+ return ''; // No file system on web
124
80
  }
125
- export function configureCache(_config) { }
126
- export function getCacheDirectory() { return ''; }
@@ -0,0 +1,8 @@
1
+ import type { RemoveBgImageOptions } from './types';
2
+ export declare const BackgroundRemover: {
3
+ /**
4
+ * Removes background from an image.
5
+ * Returns a Blobl of the processed image (PNG).
6
+ */
7
+ remove(uri: string, options: RemoveBgImageOptions): Promise<Blob>;
8
+ };
@@ -0,0 +1,33 @@
1
+ import { removeBackground as imglyRemove } from '@imgly/background-removal';
2
+ import { mapErrorToBackgroundRemovalError } from '../errors/WebErrorAdapter';
3
+ import { normalizeUri } from '../utils/uriHelper';
4
+ export const BackgroundRemover = {
5
+ /**
6
+ * Removes background from an image.
7
+ * Returns a Blobl of the processed image (PNG).
8
+ */
9
+ async remove(uri, options) {
10
+ try {
11
+ const normalizedUri = await normalizeUri(uri);
12
+ const config = {
13
+ // Pass publicPath if provided (for self-hosted assets)
14
+ publicPath: options.publicPath,
15
+ // Map progress callback
16
+ progress: (_key, current, total) => {
17
+ if (options.onProgress && total > 0) {
18
+ const p = Math.min(100, Math.round((current / total) * 100));
19
+ options.onProgress(p);
20
+ }
21
+ },
22
+ // Enable debug logging if requested
23
+ debug: options.debug ?? false,
24
+ };
25
+ // Execute removal
26
+ const blob = await imglyRemove(normalizedUri, config);
27
+ return blob;
28
+ }
29
+ catch (error) {
30
+ throw mapErrorToBackgroundRemovalError(error);
31
+ }
32
+ }
33
+ };
@@ -0,0 +1,16 @@
1
+ import type { RemoveBgImageOptions } from './types';
2
+ export declare class CacheManager {
3
+ private cache;
4
+ private readonly MAX_SIZE;
5
+ constructor();
6
+ /**
7
+ * Generates a unique cache key based on input URI and processing options
8
+ */
9
+ private generateKey;
10
+ get(uri: string, options: RemoveBgImageOptions): string | null;
11
+ set(uri: string, options: RemoveBgImageOptions, dataUrl: string): void;
12
+ clear(): void;
13
+ size(): number;
14
+ private evictOldest;
15
+ }
16
+ export declare const cacheManager: CacheManager;
@@ -0,0 +1,57 @@
1
+ export class CacheManager {
2
+ cache;
3
+ MAX_SIZE = 50; // Limit to 50 items to avoid memory leaks
4
+ constructor() {
5
+ this.cache = new Map();
6
+ }
7
+ /**
8
+ * Generates a unique cache key based on input URI and processing options
9
+ */
10
+ generateKey(uri, options) {
11
+ // We include relevant options that affect output
12
+ const { format = 'PNG', quality = 100, maxDimension = 0 } = options;
13
+ return `${uri}|${format}|${quality}|${maxDimension}`;
14
+ }
15
+ get(uri, options) {
16
+ const key = this.generateKey(uri, options);
17
+ const entry = this.cache.get(key);
18
+ if (entry) {
19
+ entry.timestamp = Date.now(); // Update usage timestamp (simple LRU)
20
+ return entry.dataUrl;
21
+ }
22
+ return null;
23
+ }
24
+ set(uri, options, dataUrl) {
25
+ const key = this.generateKey(uri, options);
26
+ // Evict if full
27
+ if (this.cache.size >= this.MAX_SIZE) {
28
+ this.evictOldest();
29
+ }
30
+ this.cache.set(key, {
31
+ dataUrl,
32
+ timestamp: Date.now()
33
+ });
34
+ }
35
+ clear() {
36
+ this.cache.clear();
37
+ }
38
+ size() {
39
+ return this.cache.size;
40
+ }
41
+ evictOldest() {
42
+ // Find oldest entry
43
+ let oldestKey = null;
44
+ let oldestTime = Infinity;
45
+ for (const [key, entry] of this.cache.entries()) {
46
+ if (entry.timestamp < oldestTime) {
47
+ oldestTime = entry.timestamp;
48
+ oldestKey = key;
49
+ }
50
+ }
51
+ if (oldestKey) {
52
+ this.cache.delete(oldestKey);
53
+ }
54
+ }
55
+ }
56
+ // Singleton instance
57
+ export const cacheManager = new CacheManager();
@@ -0,0 +1,47 @@
1
+ export type OutputFormat = 'PNG' | 'WEBP';
2
+ export interface RemoveBgImageOptions {
3
+ /**
4
+ * Output format of the processed image.
5
+ * Default: 'PNG'
6
+ */
7
+ format?: OutputFormat;
8
+ /**
9
+ * Quality of the output image (0-100). Only applies to WEBP/JPEG.
10
+ * Default: 100
11
+ */
12
+ quality?: number;
13
+ /**
14
+ * Callback to track download and processing progress (0-100).
15
+ */
16
+ onProgress?: (progress: number) => void;
17
+ /**
18
+ * Enable debug logging.
19
+ * Default: false
20
+ */
21
+ debug?: boolean;
22
+ /**
23
+ * Maximum dimension for the output image. Use this to resize large images before processing.
24
+ */
25
+ maxDimension?: number;
26
+ /**
27
+ * Public path to serve the model assets from.
28
+ * If not provided, it will attempt to fetch from @imgly CDN.
29
+ * Important for Metro bundler compatibility if not using CDN.
30
+ */
31
+ publicPath?: string;
32
+ /**
33
+ * Whether to use the cache for this request.
34
+ * Default: true
35
+ */
36
+ useCache?: boolean;
37
+ }
38
+ export interface CompressImageOptions {
39
+ maxSizeKB?: number;
40
+ width?: number;
41
+ height?: number;
42
+ quality?: number;
43
+ format?: 'webp' | 'png' | 'jpeg';
44
+ }
45
+ export interface GenerateThumbhashOptions {
46
+ size?: number;
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export declare class BackgroundRemovalError extends Error {
2
+ code: string;
3
+ constructor(message: string, code?: string);
4
+ }
5
+ /**
6
+ * Maps library errors to standardized BackgroundRemovalError
7
+ */
8
+ export declare function mapErrorToBackgroundRemovalError(error: unknown): BackgroundRemovalError;
@@ -0,0 +1,29 @@
1
+ export class BackgroundRemovalError extends Error {
2
+ code;
3
+ constructor(message, code = 'UNKNOWN_ERROR') {
4
+ super(message);
5
+ this.name = 'BackgroundRemovalError';
6
+ this.code = code;
7
+ }
8
+ }
9
+ /**
10
+ * Maps library errors to standardized BackgroundRemovalError
11
+ */
12
+ export function mapErrorToBackgroundRemovalError(error) {
13
+ if (error instanceof BackgroundRemovalError)
14
+ return error;
15
+ const message = error instanceof Error ? error.message : String(error);
16
+ // Model loading errors
17
+ if (message.includes('fetch') || message.includes('network') || message.includes('Failed to load resource')) {
18
+ return new BackgroundRemovalError(`Failed to download AI model. Please check your internet connection. (Details: ${message})`, 'MODEL_DOWNLOAD_ERROR');
19
+ }
20
+ // WASM errors
21
+ if (message.includes('wasm') || message.includes('WebAssembly')) {
22
+ return new BackgroundRemovalError(`WebAssembly failed to initialize. Your browser might not support it. (Details: ${message})`, 'WASM_INIT_ERROR');
23
+ }
24
+ // Processing errors
25
+ if (message.includes('memory') || message.includes('allocation')) {
26
+ return new BackgroundRemovalError('Out of memory. Try using a smaller maxDimension or closing other tabs.', 'MEMORY_ERROR');
27
+ }
28
+ return new BackgroundRemovalError(message);
29
+ }
@@ -0,0 +1,2 @@
1
+ import type { CompressImageOptions } from '../core/types';
2
+ export declare function compressImage(uri: string, options: CompressImageOptions): Promise<string>;
@@ -0,0 +1,32 @@
1
+ import { loadImage } from './uriHelper';
2
+ export async function compressImage(uri, options) {
3
+ const img = await loadImage(uri);
4
+ let { width, height, quality = 0.8, format = 'jpeg' } = options;
5
+ // Default dimensions to original if not specified
6
+ if (!width)
7
+ width = img.naturalWidth;
8
+ if (!height)
9
+ height = img.naturalHeight;
10
+ // Calculate aspect ratio if one dimension is missing (though simpler to just use natural if both default)
11
+ const ratio = img.naturalWidth / img.naturalHeight;
12
+ if (options.width && !options.height)
13
+ height = Math.round(width / ratio);
14
+ if (options.height && !options.width)
15
+ width = Math.round(height * ratio);
16
+ // Normalize format
17
+ const mimeType = format === 'png' ? 'image/png' : format === 'webp' ? 'image/webp' : 'image/jpeg';
18
+ // Create canvas
19
+ const canvas = document.createElement('canvas');
20
+ canvas.width = width;
21
+ canvas.height = height;
22
+ const ctx = canvas.getContext('2d');
23
+ if (!ctx) {
24
+ throw new Error('Canvas 2D context not available');
25
+ }
26
+ // Draw image
27
+ ctx.drawImage(img, 0, 0, width, height);
28
+ // Export
29
+ // Note: quality (0-1) is ignored for PNG
30
+ const dataUrl = canvas.toDataURL(mimeType, quality);
31
+ return dataUrl;
32
+ }
@@ -0,0 +1 @@
1
+ export declare function generateThumbhash(uri: string): Promise<string>;
@@ -0,0 +1,31 @@
1
+ import * as ThumbHash from 'thumbhash';
2
+ import { loadImage } from './uriHelper';
3
+ export async function generateThumbhash(uri) {
4
+ const img = await loadImage(uri);
5
+ // Thumbhash works best with images < 100x100
6
+ const maxSize = 100;
7
+ let width = img.naturalWidth;
8
+ let height = img.naturalHeight;
9
+ const scale = Math.min(maxSize / width, maxSize / height);
10
+ if (scale < 1) {
11
+ width = Math.round(width * scale);
12
+ height = Math.round(height * scale);
13
+ }
14
+ const canvas = document.createElement('canvas');
15
+ canvas.width = width;
16
+ canvas.height = height;
17
+ const ctx = canvas.getContext('2d');
18
+ if (!ctx) {
19
+ throw new Error('Canvas 2D context not available');
20
+ }
21
+ ctx.drawImage(img, 0, 0, width, height);
22
+ // Get RGBA data
23
+ const imageData = ctx.getImageData(0, 0, width, height);
24
+ const rgba = imageData.data;
25
+ // Generate binary hash
26
+ const hash = ThumbHash.rgbaToThumbHash(width, height, rgba);
27
+ // Convert to base64 using browser API
28
+ // hash is Uint8Array, spread into String.fromCharCode is safe for small thumbhashes (~30 bytes)
29
+ const binary = String.fromCharCode(...hash);
30
+ return window.btoa(binary);
31
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Converts a Blob to a Data URL string (base64)
3
+ */
4
+ export declare function blobToDataUrl(blob: Blob): Promise<string>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Converts a Blob to a Data URL string (base64)
3
+ */
4
+ export function blobToDataUrl(blob) {
5
+ return new Promise((resolve, reject) => {
6
+ const reader = new FileReader();
7
+ reader.onload = () => {
8
+ if (typeof reader.result === 'string') {
9
+ resolve(reader.result);
10
+ }
11
+ else {
12
+ reject(new Error('Failed to convert blob to data URL'));
13
+ }
14
+ };
15
+ reader.onerror = () => reject(reader.error);
16
+ reader.readAsDataURL(blob);
17
+ });
18
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Helper to normalize React Native URIs for Web consumption.
3
+ * Handles file://, asset://, and data: URIs.
4
+ */
5
+ export declare function normalizeUri(uri: string): Promise<string>;
6
+ /**
7
+ * Loads an image from a URI into an HTMLImageElement for processing.
8
+ * Handles CORS cross-origin issues.
9
+ */
10
+ export declare function loadImage(uri: string): Promise<HTMLImageElement>;