From ddca839dd86961143274757ce1bd62a446f0bcc6 Mon Sep 17 00:00:00 2001 From: TriForMine Date: Thu, 28 May 2026 22:48:12 +0200 Subject: [PATCH 1/7] fix(ci,jni): install Rust Android targets before JNI builds --- .github/workflows/android-jni.yml | 6 +++++- Cargo.lock | 4 ++++ apps/expo-glyphnet/scripts/build-glyphnet-jni-android.sh | 3 +++ crates/glyphnet-wasm/Cargo.toml | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/android-jni.yml b/.github/workflows/android-jni.yml index a3b5fdc..3a8ac1a 100644 --- a/.github/workflows/android-jni.yml +++ b/.github/workflows/android-jni.yml @@ -30,6 +30,11 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - name: Install Rust Android targets + run: | + rustup target add aarch64-linux-android + rustup target add armv7-linux-androideabi + rustup target add x86_64-linux-android - uses: Swatinem/rust-cache@v2 - name: Setup Java uses: actions/setup-java@v4 @@ -61,4 +66,3 @@ jobs: name: glyphnet-android-jni-libs path: out/jniLibs if-no-files-found: error - diff --git a/Cargo.lock b/Cargo.lock index e34b425..9b88747 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -934,6 +934,7 @@ dependencies = [ "glyphnet-render", "glyphnet-scanner", "image", + "instant", "serde", "serde_json", "time", @@ -1036,6 +1037,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] diff --git a/apps/expo-glyphnet/scripts/build-glyphnet-jni-android.sh b/apps/expo-glyphnet/scripts/build-glyphnet-jni-android.sh index 34723a6..73fec62 100644 --- a/apps/expo-glyphnet/scripts/build-glyphnet-jni-android.sh +++ b/apps/expo-glyphnet/scripts/build-glyphnet-jni-android.sh @@ -11,6 +11,9 @@ if ! command -v cargo-ndk >/dev/null 2>&1; then exit 0 fi +echo "Ensuring Rust Android targets are installed..." +rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORKSPACE_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" OUT_ROOT="${WORKSPACE_ROOT}/apps/expo-glyphnet/modules/glyphnet-scanner/android/src/main/jniLibs" diff --git a/crates/glyphnet-wasm/Cargo.toml b/crates/glyphnet-wasm/Cargo.toml index 0727eaf..8b421b9 100644 --- a/crates/glyphnet-wasm/Cargo.toml +++ b/crates/glyphnet-wasm/Cargo.toml @@ -26,6 +26,7 @@ time.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen.workspace = true +instant = { version = "0.1.13", features = ["wasm-bindgen"] } [features] default = ["svg"] From 6df282a6f31c6ea86d72992c2bd735185a62bb3f Mon Sep 17 00:00:00 2001 From: TriForMine Date: Thu, 28 May 2026 22:56:18 +0200 Subject: [PATCH 2/7] Update deny.toml --- deny.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deny.toml b/deny.toml index 692c599..7ad115e 100644 --- a/deny.toml +++ b/deny.toml @@ -5,7 +5,11 @@ confidence-threshold = 0.9 [advisories] unmaintained = "workspace" unsound = "all" -ignore = [] +ignore = [ + # Transitive via parking_lot 0.11 -> parking_lot_core 0.8 / instant 0.1. + # No safe upstream upgrade path is available in current dependency graph. + "RUSTSEC-2024-0384", +] [bans] multiple-versions = "warn" From d03d7a5ff20120f6b0ba598f468f51364aae47ab Mon Sep 17 00:00:00 2001 From: TriForMine Date: Thu, 28 May 2026 23:07:20 +0200 Subject: [PATCH 3/7] Update eas-fetch-jni-libs.sh --- .../scripts/eas-fetch-jni-libs.sh | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/apps/expo-glyphnet/scripts/eas-fetch-jni-libs.sh b/apps/expo-glyphnet/scripts/eas-fetch-jni-libs.sh index 15dd196..12c1cf4 100644 --- a/apps/expo-glyphnet/scripts/eas-fetch-jni-libs.sh +++ b/apps/expo-glyphnet/scripts/eas-fetch-jni-libs.sh @@ -28,6 +28,15 @@ if ! command -v unzip >/dev/null 2>&1; then exit 0 fi +if command -v node >/dev/null 2>&1; then + JSON_EXTRACTOR="node" +elif command -v python3 >/dev/null 2>&1; then + JSON_EXTRACTOR="python3" +else + echo "[jni-fetch] neither node nor python3 available; skipping" + exit 0 +fi + OWNER="${GLYPHNET_GITHUB_OWNER:-TriForMine}" REPO="${GLYPHNET_GITHUB_REPO:-glyphnet}" WORKFLOW_FILE="${GLYPHNET_JNI_WORKFLOW_FILE:-android-jni.yml}" @@ -43,7 +52,11 @@ WORKFLOW_ID="$( -H "${AUTH_HEADER}" \ -H "Accept: application/vnd.github+json" \ "${API_ROOT}/actions/workflows/${WORKFLOW_FILE}" \ - | python -c "import sys, json; print(json.load(sys.stdin)['id'])" + | if [[ "${JSON_EXTRACTOR}" == "node" ]]; then + node -e "const fs=require('fs'); const d=JSON.parse(fs.readFileSync(0,'utf8')); console.log(d.id ?? '');" + else + python3 -c "import sys, json; print(json.load(sys.stdin).get('id',''))" + fi )" echo "[jni-fetch] resolving latest successful workflow run" @@ -52,7 +65,11 @@ RUN_ID="$( -H "${AUTH_HEADER}" \ -H "Accept: application/vnd.github+json" \ "${API_ROOT}/actions/workflows/${WORKFLOW_ID}/runs?status=success&per_page=1" \ - | python -c "import sys, json; d=json.load(sys.stdin); runs=d.get('workflow_runs', []); print(runs[0]['id'] if runs else '')" + | if [[ "${JSON_EXTRACTOR}" == "node" ]]; then + node -e "const fs=require('fs'); const d=JSON.parse(fs.readFileSync(0,'utf8')); const runs=d.workflow_runs||[]; console.log(runs.length ? (runs[0].id ?? '') : '');" + else + python3 -c "import sys, json; d=json.load(sys.stdin); runs=d.get('workflow_runs', []); print(runs[0]['id'] if runs else '')" + fi )" if [[ -z "${RUN_ID}" ]]; then @@ -66,7 +83,11 @@ ARTIFACT_ID="$( -H "${AUTH_HEADER}" \ -H "Accept: application/vnd.github+json" \ "${API_ROOT}/actions/runs/${RUN_ID}/artifacts?per_page=100" \ - | python -c "import sys, json; d=json.load(sys.stdin); arts=d.get('artifacts', []); name='${ARTIFACT_NAME}'; m=[a for a in arts if a.get('name')==name and not a.get('expired', False)]; print(m[0]['id'] if m else '')" + | if [[ "${JSON_EXTRACTOR}" == "node" ]]; then + node -e "const fs=require('fs'); const d=JSON.parse(fs.readFileSync(0,'utf8')); const arts=d.artifacts||[]; const name=process.argv[1]; const m=arts.find(a => a && a.name===name && !a.expired); console.log(m ? (m.id ?? '') : '');" "${ARTIFACT_NAME}" + else + python3 -c "import sys, json; d=json.load(sys.stdin); arts=d.get('artifacts', []); name='${ARTIFACT_NAME}'; m=[a for a in arts if a.get('name')==name and not a.get('expired', False)]; print(m[0]['id'] if m else '')" + fi )" if [[ -z "${ARTIFACT_ID}" ]]; then @@ -85,9 +106,36 @@ curl -fL \ -o "${TMP_ZIP}" unzip -q "${TMP_ZIP}" -d "${TMP_DIR}" + +ABI_DIRS=("arm64-v8a" "armeabi-v7a" "x86_64") +SOURCE_ROOT="" +for candidate in \ + "${TMP_DIR}" \ + "${TMP_DIR}/jniLibs" \ + "${TMP_DIR}/out/jniLibs" +do + if [[ -d "${candidate}" ]]; then + for abi in "${ABI_DIRS[@]}"; do + if [[ -d "${candidate}/${abi}" ]]; then + SOURCE_ROOT="${candidate}" + break 2 + fi + done + fi +done + +if [[ -z "${SOURCE_ROOT}" ]]; then + echo "[jni-fetch] unable to find ABI folders in artifact; aborting" + find "${TMP_DIR}" -maxdepth 3 -type d | sed 's/^/[jni-fetch] found dir: /' + exit 1 +fi + mkdir -p "${DEST_DIR}" rm -rf "${DEST_DIR:?}"/* -cp -R "${TMP_DIR}/"*/. "${DEST_DIR}/" +for abi in "${ABI_DIRS[@]}"; do + if [[ -d "${SOURCE_ROOT}/${abi}" ]]; then + cp -R "${SOURCE_ROOT}/${abi}" "${DEST_DIR}/" + fi +done echo "[jni-fetch] installed JNI libs into ${DEST_DIR}" - From 34473dc815fc26df86ae1cc2306401f683f3a0a7 Mon Sep 17 00:00:00 2001 From: TriForMine Date: Thu, 28 May 2026 23:35:14 +0200 Subject: [PATCH 4/7] feat(expo): wire real scan capture flow and SVG preview rendering --- .../src/adapters/scanner/types.ts | 4 + .../src/features/encode/EncodePanel.tsx | 31 +++- .../src/features/scan/ScanPanel.tsx | 22 ++- crates/glyphnet-jni/src/lib.rs | 144 +++++++++++------- 4 files changed, 137 insertions(+), 64 deletions(-) diff --git a/apps/expo-glyphnet/src/adapters/scanner/types.ts b/apps/expo-glyphnet/src/adapters/scanner/types.ts index 6067a7d..2e02a29 100644 --- a/apps/expo-glyphnet/src/adapters/scanner/types.ts +++ b/apps/expo-glyphnet/src/adapters/scanner/types.ts @@ -29,6 +29,10 @@ export interface ScanRequest { mode: ScanMode; verifyKeyHex?: string; verifyKeyId?: number; + imageBase64?: string; + width?: number; + height?: number; + rgbaBase64?: string; } export interface ScannerAdapter { diff --git a/apps/expo-glyphnet/src/features/encode/EncodePanel.tsx b/apps/expo-glyphnet/src/features/encode/EncodePanel.tsx index dc9b1ae..95ac3e6 100644 --- a/apps/expo-glyphnet/src/features/encode/EncodePanel.tsx +++ b/apps/expo-glyphnet/src/features/encode/EncodePanel.tsx @@ -1,11 +1,13 @@ import { useMemo, useState } from "react"; import { scannerAdapter } from "@/adapters/scanner"; +import { Image } from "@/tw/image"; import { Pressable, Text, TextInput, View } from "@/tw"; export function EncodePanel() { const [payload, setPayload] = useState("hello glyphnet"); const [svgPreview, setSvgPreview] = useState(""); + const [encodeError, setEncodeError] = useState(null); const [loading, setLoading] = useState(false); const payloadBytes = useMemo(() => new TextEncoder().encode(payload).length, [payload]); @@ -13,8 +15,20 @@ export function EncodePanel() { const runEncode = async () => { setLoading(true); try { + setEncodeError(null); const svg = await scannerAdapter.encodeSvg(payload); - setSvgPreview(svg); + if (svg.trim().startsWith(" SVG output - - {svgPreview || "No output yet"} - + {svgPreview ? ( + + ) : ( + + {encodeError ?? "No output yet"} + + )} ); } - diff --git a/apps/expo-glyphnet/src/features/scan/ScanPanel.tsx b/apps/expo-glyphnet/src/features/scan/ScanPanel.tsx index dbde488..f417af2 100644 --- a/apps/expo-glyphnet/src/features/scan/ScanPanel.tsx +++ b/apps/expo-glyphnet/src/features/scan/ScanPanel.tsx @@ -1,5 +1,5 @@ import { CameraView, useCameraPermissions } from "expo-camera"; -import { useMemo, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { scannerAdapter } from "@/adapters/scanner"; import { Pressable, Text, TextInput, View } from "@/tw"; @@ -7,6 +7,7 @@ import { Pressable, Text, TextInput, View } from "@/tw"; const MODES = ["print", "screen", "burst"] as const; export function ScanPanel() { + const cameraRef = useRef(null); const [permission, requestPermission] = useCameraPermissions(); const [mode, setMode] = useState<(typeof MODES)[number]>("print"); const [verifyKeyHex, setVerifyKeyHex] = useState(""); @@ -25,10 +26,23 @@ export function ScanPanel() { const runScan = async () => { setLoading(true); try { + if (!cameraRef.current?.takePictureAsync) { + setResult("Camera capture is not ready yet."); + return; + } + const shot = await cameraRef.current.takePictureAsync({ + base64: true, + quality: 0.9, + }); + if (!shot?.base64) { + setResult("Failed to capture image from camera."); + return; + } const json = await scannerAdapter.scanStill({ mode, verifyKeyHex: verifyKeyHex || undefined, verifyKeyId: 1, + imageBase64: shot.base64, }); setResult(JSON.stringify(json, null, 2)); } finally { @@ -44,7 +58,11 @@ export function ScanPanel() { {permission?.granted ? ( - + ) : ( diff --git a/crates/glyphnet-jni/src/lib.rs b/crates/glyphnet-jni/src/lib.rs index 125ba87..6e526b7 100644 --- a/crates/glyphnet-jni/src/lib.rs +++ b/crates/glyphnet-jni/src/lib.rs @@ -21,6 +21,8 @@ struct ScanStillRequest { height: Option, #[serde(rename = "rgbaBase64")] rgba_base64: Option, + #[serde(rename = "imageBase64")] + image_base64: Option, } fn make_java_string(env: JNIEnv<'_>, value: &str) -> jstring { @@ -131,74 +133,102 @@ pub extern "system" fn Java_expo_modules_glyphnetscanner_GlyphNetNativeBridge_sc let verify_key_provided = parsed.verify_key_hex.is_some(); let verify_key_id = parsed.verify_key_id; - let width = match parsed.width { - Some(v) => v, - None => { - let err = error_json(mode, verify_key_provided, verify_key_id, "missing_width"); - return make_java_string(env, &err); - } - }; - let height = match parsed.height { - Some(v) => v, - None => { - let err = error_json(mode, verify_key_provided, verify_key_id, "missing_height"); - return make_java_string(env, &err); - } - }; - let rgba_base64 = match parsed.rgba_base64 { - Some(v) => v, - None => { - let err = error_json( - mode, - verify_key_provided, - verify_key_id, - "missing_rgba_base64", - ); - return make_java_string(env, &err); - } - }; + let image = if let Some(image_base64) = parsed.image_base64 { + let bytes = match base64::engine::general_purpose::STANDARD.decode(image_base64.as_bytes()) { + Ok(v) => v, + Err(error) => { + let err = error_json( + mode, + verify_key_provided, + verify_key_id, + &format!("invalid_image_base64: {error}"), + ); + return make_java_string(env, &err); + } + }; - let rgba = match base64::engine::general_purpose::STANDARD.decode(rgba_base64.as_bytes()) { - Ok(v) => v, - Err(error) => { - let err = error_json( - mode, - verify_key_provided, - verify_key_id, - &format!("invalid_rgba_base64: {error}"), - ); - return make_java_string(env, &err); + match image::load_from_memory(&bytes) { + Ok(v) => v, + Err(error) => { + let err = error_json( + mode, + verify_key_provided, + verify_key_id, + &format!("invalid_image_data: {error}"), + ); + return make_java_string(env, &err); + } } - }; + } else { + let width = match parsed.width { + Some(v) => v, + None => { + let err = error_json(mode, verify_key_provided, verify_key_id, "missing_width"); + return make_java_string(env, &err); + } + }; + let height = match parsed.height { + Some(v) => v, + None => { + let err = error_json(mode, verify_key_provided, verify_key_id, "missing_height"); + return make_java_string(env, &err); + } + }; + let rgba_base64 = match parsed.rgba_base64 { + Some(v) => v, + None => { + let err = error_json( + mode, + verify_key_provided, + verify_key_id, + "missing_rgba_base64", + ); + return make_java_string(env, &err); + } + }; - let expected = width - .checked_mul(height) - .and_then(|pixels| pixels.checked_mul(4)) - .unwrap_or(0) as usize; - if rgba.len() != expected { - let err = error_json( - mode, - verify_key_provided, - verify_key_id, - &format!( - "invalid_rgba_length: expected {expected}, got {}", - rgba.len() - ), - ); - return make_java_string(env, &err); - } + let rgba = match base64::engine::general_purpose::STANDARD.decode(rgba_base64.as_bytes()) { + Ok(v) => v, + Err(error) => { + let err = error_json( + mode, + verify_key_provided, + verify_key_id, + &format!("invalid_rgba_base64: {error}"), + ); + return make_java_string(env, &err); + } + }; - let image = match RgbaImage::from_raw(width, height, rgba) { - Some(v) => DynamicImage::ImageRgba8(v), - None => { + let expected = width + .checked_mul(height) + .and_then(|pixels| pixels.checked_mul(4)) + .unwrap_or(0) as usize; + if rgba.len() != expected { let err = error_json( mode, verify_key_provided, verify_key_id, - "failed_to_construct_rgba_image", + &format!( + "invalid_rgba_length: expected {expected}, got {}", + rgba.len() + ), ); return make_java_string(env, &err); } + + match RgbaImage::from_raw(width, height, rgba) { + Some(v) => DynamicImage::ImageRgba8(v), + None => { + let err = error_json( + mode, + verify_key_provided, + verify_key_id, + "failed_to_construct_rgba_image", + ); + return make_java_string(env, &err); + } + } }; let response = match scan_still_with_diagnostics(&image, mode) { From 9da176fcf12126405471d4c9748174437b8e9c02 Mon Sep 17 00:00:00 2001 From: TriForMine Date: Thu, 28 May 2026 23:07:20 +0200 Subject: [PATCH 5/7] Update eas-fetch-jni-libs.sh --- .../scripts/eas-fetch-jni-libs.sh | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/apps/expo-glyphnet/scripts/eas-fetch-jni-libs.sh b/apps/expo-glyphnet/scripts/eas-fetch-jni-libs.sh index 15dd196..12c1cf4 100644 --- a/apps/expo-glyphnet/scripts/eas-fetch-jni-libs.sh +++ b/apps/expo-glyphnet/scripts/eas-fetch-jni-libs.sh @@ -28,6 +28,15 @@ if ! command -v unzip >/dev/null 2>&1; then exit 0 fi +if command -v node >/dev/null 2>&1; then + JSON_EXTRACTOR="node" +elif command -v python3 >/dev/null 2>&1; then + JSON_EXTRACTOR="python3" +else + echo "[jni-fetch] neither node nor python3 available; skipping" + exit 0 +fi + OWNER="${GLYPHNET_GITHUB_OWNER:-TriForMine}" REPO="${GLYPHNET_GITHUB_REPO:-glyphnet}" WORKFLOW_FILE="${GLYPHNET_JNI_WORKFLOW_FILE:-android-jni.yml}" @@ -43,7 +52,11 @@ WORKFLOW_ID="$( -H "${AUTH_HEADER}" \ -H "Accept: application/vnd.github+json" \ "${API_ROOT}/actions/workflows/${WORKFLOW_FILE}" \ - | python -c "import sys, json; print(json.load(sys.stdin)['id'])" + | if [[ "${JSON_EXTRACTOR}" == "node" ]]; then + node -e "const fs=require('fs'); const d=JSON.parse(fs.readFileSync(0,'utf8')); console.log(d.id ?? '');" + else + python3 -c "import sys, json; print(json.load(sys.stdin).get('id',''))" + fi )" echo "[jni-fetch] resolving latest successful workflow run" @@ -52,7 +65,11 @@ RUN_ID="$( -H "${AUTH_HEADER}" \ -H "Accept: application/vnd.github+json" \ "${API_ROOT}/actions/workflows/${WORKFLOW_ID}/runs?status=success&per_page=1" \ - | python -c "import sys, json; d=json.load(sys.stdin); runs=d.get('workflow_runs', []); print(runs[0]['id'] if runs else '')" + | if [[ "${JSON_EXTRACTOR}" == "node" ]]; then + node -e "const fs=require('fs'); const d=JSON.parse(fs.readFileSync(0,'utf8')); const runs=d.workflow_runs||[]; console.log(runs.length ? (runs[0].id ?? '') : '');" + else + python3 -c "import sys, json; d=json.load(sys.stdin); runs=d.get('workflow_runs', []); print(runs[0]['id'] if runs else '')" + fi )" if [[ -z "${RUN_ID}" ]]; then @@ -66,7 +83,11 @@ ARTIFACT_ID="$( -H "${AUTH_HEADER}" \ -H "Accept: application/vnd.github+json" \ "${API_ROOT}/actions/runs/${RUN_ID}/artifacts?per_page=100" \ - | python -c "import sys, json; d=json.load(sys.stdin); arts=d.get('artifacts', []); name='${ARTIFACT_NAME}'; m=[a for a in arts if a.get('name')==name and not a.get('expired', False)]; print(m[0]['id'] if m else '')" + | if [[ "${JSON_EXTRACTOR}" == "node" ]]; then + node -e "const fs=require('fs'); const d=JSON.parse(fs.readFileSync(0,'utf8')); const arts=d.artifacts||[]; const name=process.argv[1]; const m=arts.find(a => a && a.name===name && !a.expired); console.log(m ? (m.id ?? '') : '');" "${ARTIFACT_NAME}" + else + python3 -c "import sys, json; d=json.load(sys.stdin); arts=d.get('artifacts', []); name='${ARTIFACT_NAME}'; m=[a for a in arts if a.get('name')==name and not a.get('expired', False)]; print(m[0]['id'] if m else '')" + fi )" if [[ -z "${ARTIFACT_ID}" ]]; then @@ -85,9 +106,36 @@ curl -fL \ -o "${TMP_ZIP}" unzip -q "${TMP_ZIP}" -d "${TMP_DIR}" + +ABI_DIRS=("arm64-v8a" "armeabi-v7a" "x86_64") +SOURCE_ROOT="" +for candidate in \ + "${TMP_DIR}" \ + "${TMP_DIR}/jniLibs" \ + "${TMP_DIR}/out/jniLibs" +do + if [[ -d "${candidate}" ]]; then + for abi in "${ABI_DIRS[@]}"; do + if [[ -d "${candidate}/${abi}" ]]; then + SOURCE_ROOT="${candidate}" + break 2 + fi + done + fi +done + +if [[ -z "${SOURCE_ROOT}" ]]; then + echo "[jni-fetch] unable to find ABI folders in artifact; aborting" + find "${TMP_DIR}" -maxdepth 3 -type d | sed 's/^/[jni-fetch] found dir: /' + exit 1 +fi + mkdir -p "${DEST_DIR}" rm -rf "${DEST_DIR:?}"/* -cp -R "${TMP_DIR}/"*/. "${DEST_DIR}/" +for abi in "${ABI_DIRS[@]}"; do + if [[ -d "${SOURCE_ROOT}/${abi}" ]]; then + cp -R "${SOURCE_ROOT}/${abi}" "${DEST_DIR}/" + fi +done echo "[jni-fetch] installed JNI libs into ${DEST_DIR}" - From e21af014877818e4edd2f2a068679fc9aaabc032 Mon Sep 17 00:00:00 2001 From: TriForMine Date: Thu, 28 May 2026 23:35:14 +0200 Subject: [PATCH 6/7] feat(expo): wire real scan capture flow and SVG preview rendering --- .../src/adapters/scanner/types.ts | 4 + .../src/features/encode/EncodePanel.tsx | 31 +++- .../src/features/scan/ScanPanel.tsx | 22 ++- crates/glyphnet-jni/src/lib.rs | 144 +++++++++++------- 4 files changed, 137 insertions(+), 64 deletions(-) diff --git a/apps/expo-glyphnet/src/adapters/scanner/types.ts b/apps/expo-glyphnet/src/adapters/scanner/types.ts index 6067a7d..2e02a29 100644 --- a/apps/expo-glyphnet/src/adapters/scanner/types.ts +++ b/apps/expo-glyphnet/src/adapters/scanner/types.ts @@ -29,6 +29,10 @@ export interface ScanRequest { mode: ScanMode; verifyKeyHex?: string; verifyKeyId?: number; + imageBase64?: string; + width?: number; + height?: number; + rgbaBase64?: string; } export interface ScannerAdapter { diff --git a/apps/expo-glyphnet/src/features/encode/EncodePanel.tsx b/apps/expo-glyphnet/src/features/encode/EncodePanel.tsx index dc9b1ae..95ac3e6 100644 --- a/apps/expo-glyphnet/src/features/encode/EncodePanel.tsx +++ b/apps/expo-glyphnet/src/features/encode/EncodePanel.tsx @@ -1,11 +1,13 @@ import { useMemo, useState } from "react"; import { scannerAdapter } from "@/adapters/scanner"; +import { Image } from "@/tw/image"; import { Pressable, Text, TextInput, View } from "@/tw"; export function EncodePanel() { const [payload, setPayload] = useState("hello glyphnet"); const [svgPreview, setSvgPreview] = useState(""); + const [encodeError, setEncodeError] = useState(null); const [loading, setLoading] = useState(false); const payloadBytes = useMemo(() => new TextEncoder().encode(payload).length, [payload]); @@ -13,8 +15,20 @@ export function EncodePanel() { const runEncode = async () => { setLoading(true); try { + setEncodeError(null); const svg = await scannerAdapter.encodeSvg(payload); - setSvgPreview(svg); + if (svg.trim().startsWith(" SVG output - - {svgPreview || "No output yet"} - + {svgPreview ? ( + + ) : ( + + {encodeError ?? "No output yet"} + + )} ); } - diff --git a/apps/expo-glyphnet/src/features/scan/ScanPanel.tsx b/apps/expo-glyphnet/src/features/scan/ScanPanel.tsx index dbde488..f417af2 100644 --- a/apps/expo-glyphnet/src/features/scan/ScanPanel.tsx +++ b/apps/expo-glyphnet/src/features/scan/ScanPanel.tsx @@ -1,5 +1,5 @@ import { CameraView, useCameraPermissions } from "expo-camera"; -import { useMemo, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { scannerAdapter } from "@/adapters/scanner"; import { Pressable, Text, TextInput, View } from "@/tw"; @@ -7,6 +7,7 @@ import { Pressable, Text, TextInput, View } from "@/tw"; const MODES = ["print", "screen", "burst"] as const; export function ScanPanel() { + const cameraRef = useRef(null); const [permission, requestPermission] = useCameraPermissions(); const [mode, setMode] = useState<(typeof MODES)[number]>("print"); const [verifyKeyHex, setVerifyKeyHex] = useState(""); @@ -25,10 +26,23 @@ export function ScanPanel() { const runScan = async () => { setLoading(true); try { + if (!cameraRef.current?.takePictureAsync) { + setResult("Camera capture is not ready yet."); + return; + } + const shot = await cameraRef.current.takePictureAsync({ + base64: true, + quality: 0.9, + }); + if (!shot?.base64) { + setResult("Failed to capture image from camera."); + return; + } const json = await scannerAdapter.scanStill({ mode, verifyKeyHex: verifyKeyHex || undefined, verifyKeyId: 1, + imageBase64: shot.base64, }); setResult(JSON.stringify(json, null, 2)); } finally { @@ -44,7 +58,11 @@ export function ScanPanel() { {permission?.granted ? ( - + ) : ( diff --git a/crates/glyphnet-jni/src/lib.rs b/crates/glyphnet-jni/src/lib.rs index 125ba87..6e526b7 100644 --- a/crates/glyphnet-jni/src/lib.rs +++ b/crates/glyphnet-jni/src/lib.rs @@ -21,6 +21,8 @@ struct ScanStillRequest { height: Option, #[serde(rename = "rgbaBase64")] rgba_base64: Option, + #[serde(rename = "imageBase64")] + image_base64: Option, } fn make_java_string(env: JNIEnv<'_>, value: &str) -> jstring { @@ -131,74 +133,102 @@ pub extern "system" fn Java_expo_modules_glyphnetscanner_GlyphNetNativeBridge_sc let verify_key_provided = parsed.verify_key_hex.is_some(); let verify_key_id = parsed.verify_key_id; - let width = match parsed.width { - Some(v) => v, - None => { - let err = error_json(mode, verify_key_provided, verify_key_id, "missing_width"); - return make_java_string(env, &err); - } - }; - let height = match parsed.height { - Some(v) => v, - None => { - let err = error_json(mode, verify_key_provided, verify_key_id, "missing_height"); - return make_java_string(env, &err); - } - }; - let rgba_base64 = match parsed.rgba_base64 { - Some(v) => v, - None => { - let err = error_json( - mode, - verify_key_provided, - verify_key_id, - "missing_rgba_base64", - ); - return make_java_string(env, &err); - } - }; + let image = if let Some(image_base64) = parsed.image_base64 { + let bytes = match base64::engine::general_purpose::STANDARD.decode(image_base64.as_bytes()) { + Ok(v) => v, + Err(error) => { + let err = error_json( + mode, + verify_key_provided, + verify_key_id, + &format!("invalid_image_base64: {error}"), + ); + return make_java_string(env, &err); + } + }; - let rgba = match base64::engine::general_purpose::STANDARD.decode(rgba_base64.as_bytes()) { - Ok(v) => v, - Err(error) => { - let err = error_json( - mode, - verify_key_provided, - verify_key_id, - &format!("invalid_rgba_base64: {error}"), - ); - return make_java_string(env, &err); + match image::load_from_memory(&bytes) { + Ok(v) => v, + Err(error) => { + let err = error_json( + mode, + verify_key_provided, + verify_key_id, + &format!("invalid_image_data: {error}"), + ); + return make_java_string(env, &err); + } } - }; + } else { + let width = match parsed.width { + Some(v) => v, + None => { + let err = error_json(mode, verify_key_provided, verify_key_id, "missing_width"); + return make_java_string(env, &err); + } + }; + let height = match parsed.height { + Some(v) => v, + None => { + let err = error_json(mode, verify_key_provided, verify_key_id, "missing_height"); + return make_java_string(env, &err); + } + }; + let rgba_base64 = match parsed.rgba_base64 { + Some(v) => v, + None => { + let err = error_json( + mode, + verify_key_provided, + verify_key_id, + "missing_rgba_base64", + ); + return make_java_string(env, &err); + } + }; - let expected = width - .checked_mul(height) - .and_then(|pixels| pixels.checked_mul(4)) - .unwrap_or(0) as usize; - if rgba.len() != expected { - let err = error_json( - mode, - verify_key_provided, - verify_key_id, - &format!( - "invalid_rgba_length: expected {expected}, got {}", - rgba.len() - ), - ); - return make_java_string(env, &err); - } + let rgba = match base64::engine::general_purpose::STANDARD.decode(rgba_base64.as_bytes()) { + Ok(v) => v, + Err(error) => { + let err = error_json( + mode, + verify_key_provided, + verify_key_id, + &format!("invalid_rgba_base64: {error}"), + ); + return make_java_string(env, &err); + } + }; - let image = match RgbaImage::from_raw(width, height, rgba) { - Some(v) => DynamicImage::ImageRgba8(v), - None => { + let expected = width + .checked_mul(height) + .and_then(|pixels| pixels.checked_mul(4)) + .unwrap_or(0) as usize; + if rgba.len() != expected { let err = error_json( mode, verify_key_provided, verify_key_id, - "failed_to_construct_rgba_image", + &format!( + "invalid_rgba_length: expected {expected}, got {}", + rgba.len() + ), ); return make_java_string(env, &err); } + + match RgbaImage::from_raw(width, height, rgba) { + Some(v) => DynamicImage::ImageRgba8(v), + None => { + let err = error_json( + mode, + verify_key_provided, + verify_key_id, + "failed_to_construct_rgba_image", + ); + return make_java_string(env, &err); + } + } }; let response = match scan_still_with_diagnostics(&image, mode) { From 531ea24c2669e8c528f28909c7f8c2094ca8c62f Mon Sep 17 00:00:00 2001 From: TriForMine Date: Thu, 28 May 2026 23:38:01 +0200 Subject: [PATCH 7/7] Update lib.rs --- crates/glyphnet-jni/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/glyphnet-jni/src/lib.rs b/crates/glyphnet-jni/src/lib.rs index 6e526b7..473bc8f 100644 --- a/crates/glyphnet-jni/src/lib.rs +++ b/crates/glyphnet-jni/src/lib.rs @@ -134,7 +134,8 @@ pub extern "system" fn Java_expo_modules_glyphnetscanner_GlyphNetNativeBridge_sc let verify_key_id = parsed.verify_key_id; let image = if let Some(image_base64) = parsed.image_base64 { - let bytes = match base64::engine::general_purpose::STANDARD.decode(image_base64.as_bytes()) { + let bytes = match base64::engine::general_purpose::STANDARD.decode(image_base64.as_bytes()) + { Ok(v) => v, Err(error) => { let err = error_json(