Migrate from .NET MAUI to Expo + Convex
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>
This commit is contained in:
parent
56499d5af9
commit
83ab4df537
138 changed files with 19136 additions and 7681 deletions
121
lib/recognition/skiaDecoder.ts
Normal file
121
lib/recognition/skiaDecoder.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue