Complete rewrite of Scry using TypeScript stack:
- Expo/React Native with Expo Router (file-based routing)
- Convex backend (serverless functions + real-time database)
- Adaptive camera system (expo-camera in Expo Go, Vision Camera in production)
- React Native Skia + fast-opencv for image processing
- GDPR-compliant auth setup with Zitadel OIDC (pending configuration)
Key features:
- Card recognition pipeline ported to TypeScript
- Perceptual hashing (192-bit color pHash)
- CLAHE preprocessing for lighting normalization
- Local SQLite cache with Convex sync
- Collection management with offline support
Removes all .NET/MAUI code (src/, test/, tools/).
💘 Generated with Crush
Assisted-by: Claude Opus 4.5 via Crush <crush@charm.land>
121 lines
2.8 KiB
TypeScript
121 lines
2.8 KiB
TypeScript
/**
|
|
* Skia-based image decoder for getting RGBA pixel data.
|
|
* Uses react-native-skia to decode images and extract pixel buffers.
|
|
*/
|
|
|
|
import {
|
|
Skia,
|
|
useImage,
|
|
SkImage,
|
|
} from "@shopify/react-native-skia";
|
|
|
|
/**
|
|
* Decode a base64 PNG/JPEG image and return RGBA pixel data.
|
|
*
|
|
* @param base64 - Base64 encoded image data (without data URI prefix)
|
|
* @returns RGBA pixel data with dimensions
|
|
*/
|
|
export function decodeImageBase64(base64: string): {
|
|
pixels: Uint8Array;
|
|
width: number;
|
|
height: number;
|
|
} | null {
|
|
try {
|
|
// Decode base64 to data
|
|
const data = Skia.Data.fromBase64(base64);
|
|
if (!data) return null;
|
|
|
|
// Create image from data
|
|
const image = Skia.Image.MakeImageFromEncoded(data);
|
|
if (!image) return null;
|
|
|
|
const width = image.width();
|
|
const height = image.height();
|
|
|
|
// Read pixels from the image
|
|
// Note: Skia images are in RGBA format
|
|
const pixels = image.readPixels(0, 0, {
|
|
width,
|
|
height,
|
|
colorType: 4, // RGBA_8888
|
|
alphaType: 1, // Unpremultiplied
|
|
});
|
|
|
|
if (!pixels) return null;
|
|
|
|
return {
|
|
pixels: new Uint8Array(pixels),
|
|
width,
|
|
height,
|
|
};
|
|
} catch (error) {
|
|
console.error("[SkiaDecoder] Failed to decode image:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode an image from a file URI and return RGBA pixel data.
|
|
*
|
|
* @param uri - File URI (e.g., file:///path/to/image.png)
|
|
* @returns Promise with RGBA pixel data
|
|
*/
|
|
export async function decodeImageFromUri(uri: string): Promise<{
|
|
pixels: Uint8Array;
|
|
width: number;
|
|
height: number;
|
|
} | null> {
|
|
try {
|
|
// Fetch the image data
|
|
const response = await fetch(uri);
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
const base64 = arrayBufferToBase64(arrayBuffer);
|
|
|
|
return decodeImageBase64(base64);
|
|
} catch (error) {
|
|
console.error("[SkiaDecoder] Failed to load image from URI:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert ArrayBuffer to base64 string.
|
|
*/
|
|
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
let binary = "";
|
|
const bytes = new Uint8Array(buffer);
|
|
const len = bytes.byteLength;
|
|
for (let i = 0; i < len; i++) {
|
|
binary += String.fromCharCode(bytes[i]);
|
|
}
|
|
return btoa(binary);
|
|
}
|
|
|
|
/**
|
|
* React hook to decode an image from URI.
|
|
* Uses Skia's useImage hook for caching.
|
|
*/
|
|
export function useDecodedImage(uri: string | null) {
|
|
const skiaImage = useImage(uri);
|
|
|
|
if (!skiaImage) {
|
|
return { loading: true, pixels: null, width: 0, height: 0 };
|
|
}
|
|
|
|
const width = skiaImage.width();
|
|
const height = skiaImage.height();
|
|
|
|
const pixels = skiaImage.readPixels(0, 0, {
|
|
width,
|
|
height,
|
|
colorType: 4, // RGBA_8888
|
|
alphaType: 1, // Unpremultiplied
|
|
});
|
|
|
|
return {
|
|
loading: false,
|
|
pixels: pixels ? new Uint8Array(pixels) : null,
|
|
width,
|
|
height,
|
|
};
|
|
}
|