diff --git a/apps/expo-glyphnet/app.json b/apps/expo-glyphnet/app.json index 1ebef30..7ac011f 100644 --- a/apps/expo-glyphnet/app.json +++ b/apps/expo-glyphnet/app.json @@ -13,9 +13,7 @@ "android": { "adaptiveIcon": { "backgroundColor": "#E6F4FE", - "foregroundImage": "./assets/images/android-icon-foreground.png", - "backgroundImage": "./assets/images/android-icon-background.png", - "monochromeImage": "./assets/images/android-icon-monochrome.png" + "foregroundImage": "./assets/images/adaptive-icon.png" }, "predictiveBackGestureEnabled": false, "package": "dev.triformine.glyphnet" diff --git a/apps/expo-glyphnet/assets/images/adaptive-icon.png b/apps/expo-glyphnet/assets/images/adaptive-icon.png new file mode 100644 index 0000000..5bd4f5f Binary files /dev/null and b/apps/expo-glyphnet/assets/images/adaptive-icon.png differ diff --git a/apps/expo-glyphnet/assets/images/icon.png b/apps/expo-glyphnet/assets/images/icon.png index 67c777a..5bd4f5f 100644 Binary files a/apps/expo-glyphnet/assets/images/icon.png and b/apps/expo-glyphnet/assets/images/icon.png differ 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}" - 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..473bc8f 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,103 @@ 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) {