mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-05-01 23:16:31 +02:00
Lazy load open cv (#6236)
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
This commit is contained in:
@@ -19,8 +19,5 @@
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="src/index.tsx"></script>
|
||||
<!-- jscanify and OpenCV for mobile scanner - loaded after React for non-blocking page load -->
|
||||
<script src="/vendor/jscanify/opencv.js" async></script>
|
||||
<script src="/vendor/jscanify/jscanify.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -20,14 +20,7 @@ import PhotoCameraRoundedIcon from "@mui/icons-material/PhotoCameraRounded";
|
||||
import UploadRoundedIcon from "@mui/icons-material/UploadRounded";
|
||||
import AddPhotoAlternateRoundedIcon from "@mui/icons-material/AddPhotoAlternateRounded";
|
||||
import CheckCircleRoundedIcon from "@mui/icons-material/CheckCircleRounded";
|
||||
|
||||
// jscanify is loaded via script tag in index.html as a global
|
||||
declare global {
|
||||
interface Window {
|
||||
jscanify: any;
|
||||
cv: any;
|
||||
}
|
||||
}
|
||||
import { loadJscanify } from "@app/utils/loadJscanify";
|
||||
|
||||
/**
|
||||
* MobileScannerPage
|
||||
@@ -136,69 +129,36 @@ export default function MobileScannerPage() {
|
||||
validateSession();
|
||||
}, [sessionId, t]);
|
||||
|
||||
// Initialize jscanify scanner and wait for OpenCV (loaded via script tags in index.html)
|
||||
useEffect(() => {
|
||||
let retryCount = 0;
|
||||
const MAX_RETRIES = 50; // 5 seconds max wait
|
||||
let cancelled = false;
|
||||
|
||||
const initScanner = () => {
|
||||
// Check if both OpenCV and jscanify are loaded
|
||||
if (!(window as any).cv || !(window as any).cv.Mat) {
|
||||
retryCount++;
|
||||
if (retryCount < MAX_RETRIES) {
|
||||
if (retryCount % 10 === 1) {
|
||||
setLoadingStatus(
|
||||
`Loading OpenCV... (${retryCount}/${MAX_RETRIES})`,
|
||||
);
|
||||
console.log(
|
||||
`[${retryCount}/${MAX_RETRIES}] Waiting for OpenCV to load...`,
|
||||
);
|
||||
}
|
||||
setTimeout(initScanner, 100);
|
||||
} else {
|
||||
const error =
|
||||
"OpenCV failed to load after 5 seconds. Check that /vendor/jscanify/opencv.js is accessible.";
|
||||
setLoadingStatus("OpenCV load failed ✗");
|
||||
console.error(error);
|
||||
loadJscanify({
|
||||
onStatus: (status) => {
|
||||
if (!cancelled) setLoadingStatus(status);
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
if (cancelled) return;
|
||||
try {
|
||||
scannerRef.current = new window.jscanify!();
|
||||
setOpenCvReady(true);
|
||||
console.log("✓ jscanify initialized with OpenCV");
|
||||
} catch (err) {
|
||||
setLoadingStatus("jscanify init failed ✗");
|
||||
console.error("Failed to initialize jscanify:", err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
setLoadingStatus(
|
||||
`Scanner library failed to load ✗: ${(err as Error).message}`,
|
||||
);
|
||||
console.error("Failed to load jscanify:", err);
|
||||
});
|
||||
|
||||
if (!window.jscanify) {
|
||||
retryCount++;
|
||||
if (retryCount < MAX_RETRIES) {
|
||||
if (retryCount % 10 === 1) {
|
||||
setLoadingStatus(
|
||||
`Loading jscanify... (${retryCount}/${MAX_RETRIES})`,
|
||||
);
|
||||
console.log(
|
||||
`[${retryCount}/${MAX_RETRIES}] Waiting for jscanify to load...`,
|
||||
);
|
||||
}
|
||||
setTimeout(initScanner, 100);
|
||||
} else {
|
||||
const error =
|
||||
"jscanify failed to load after 5 seconds. Check that /vendor/jscanify/jscanify.js is accessible.";
|
||||
setLoadingStatus("jscanify load failed ✗");
|
||||
console.error(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
scannerRef.current = new window.jscanify();
|
||||
setOpenCvReady(true);
|
||||
// Don't set status here - let camera/detection effects control status from now on
|
||||
console.log("✓ jscanify initialized with OpenCV");
|
||||
} catch (err) {
|
||||
setLoadingStatus("jscanify init failed ✗");
|
||||
console.error("Failed to initialize jscanify:", err);
|
||||
}
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
// Start initialization
|
||||
setLoadingStatus("Loading OpenCV...");
|
||||
initScanner();
|
||||
}, []);
|
||||
|
||||
// Initialize camera
|
||||
|
||||
115
frontend/src/core/utils/loadJscanify.ts
Normal file
115
frontend/src/core/utils/loadJscanify.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
cv?: any;
|
||||
jscanify?: any;
|
||||
}
|
||||
}
|
||||
|
||||
const OPENCV_SRC = "/vendor/jscanify/opencv.js";
|
||||
const JSCANIFY_SRC = "/vendor/jscanify/jscanify.js";
|
||||
|
||||
let loadPromise: Promise<void> | null = null;
|
||||
|
||||
function injectScript(src: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// data-loaded: load event fired. data-loading: in flight.
|
||||
// Neither: foreign tag (e.g. HMR-preserved); trust the downstream
|
||||
// global check rather than wait on a load event that may already
|
||||
// have dispatched.
|
||||
const existing = document.querySelector<HTMLScriptElement>(
|
||||
`script[src="${src}"]`,
|
||||
);
|
||||
if (existing) {
|
||||
if (existing.dataset.loaded === "true") {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (existing.dataset.loading === "true") {
|
||||
existing.addEventListener("load", () => resolve(), { once: true });
|
||||
existing.addEventListener(
|
||||
"error",
|
||||
() => reject(new Error(`Failed to load ${src}`)),
|
||||
{ once: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.async = true;
|
||||
script.dataset.loading = "true";
|
||||
script.onload = () => {
|
||||
script.dataset.loaded = "true";
|
||||
delete script.dataset.loading;
|
||||
resolve();
|
||||
};
|
||||
script.onerror = () => {
|
||||
delete script.dataset.loading;
|
||||
reject(new Error(`Failed to load ${src}`));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForGlobal(
|
||||
check: () => boolean,
|
||||
name: string,
|
||||
timeoutMs: number,
|
||||
onProgress?: (elapsedMs: number) => void,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
const tick = () => {
|
||||
if (check()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const elapsed = Date.now() - start;
|
||||
if (elapsed > timeoutMs) {
|
||||
reject(new Error(`Timed out waiting for ${name}`));
|
||||
return;
|
||||
}
|
||||
onProgress?.(elapsed);
|
||||
setTimeout(tick, 100);
|
||||
};
|
||||
tick();
|
||||
});
|
||||
}
|
||||
|
||||
export interface LoadJscanifyOptions {
|
||||
onStatus?: (status: string) => void;
|
||||
}
|
||||
|
||||
export function loadJscanify(options: LoadJscanifyOptions = {}): Promise<void> {
|
||||
const { onStatus } = options;
|
||||
|
||||
if (loadPromise) return loadPromise;
|
||||
|
||||
loadPromise = (async () => {
|
||||
onStatus?.("Loading OpenCV...");
|
||||
await injectScript(OPENCV_SRC);
|
||||
// OpenCV's script load event fires before the WASM runtime is ready.
|
||||
await waitForGlobal(
|
||||
() => !!window.cv && !!window.cv.Mat,
|
||||
"OpenCV runtime (cv.Mat)",
|
||||
15000,
|
||||
(elapsed) => {
|
||||
if (elapsed > 2000) onStatus?.("Initializing OpenCV runtime...");
|
||||
},
|
||||
);
|
||||
|
||||
onStatus?.("Loading jscanify...");
|
||||
await injectScript(JSCANIFY_SRC);
|
||||
await waitForGlobal(() => !!window.jscanify, "jscanify global", 5000);
|
||||
|
||||
onStatus?.("Scanner ready");
|
||||
})().catch((err) => {
|
||||
loadPromise = null;
|
||||
throw err;
|
||||
});
|
||||
|
||||
return loadPromise;
|
||||
}
|
||||
Reference in New Issue
Block a user