diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index 5746080b..f0d7d216 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -19,6 +19,7 @@ import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index"; import MountPopopover from "@/components/popovers/MountPopover"; import ExtensionPopover from "@/components/popovers/ExtensionPopover"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; + import OCRModal from "./popovers/OCRModal"; export default function Actionbar({ diff --git a/ui/src/components/popovers/OCRModal.tsx b/ui/src/components/popovers/OCRModal.tsx new file mode 100644 index 00000000..22da0544 --- /dev/null +++ b/ui/src/components/popovers/OCRModal.tsx @@ -0,0 +1,168 @@ +import { useCallback, useMemo, useState } from "react"; +import { LuCornerDownLeft } from "react-icons/lu"; +import { ExclamationCircleIcon } from "@heroicons/react/16/solid"; +import { type LoggerMessage } from "tesseract.js"; + +import { Button } from "@components/Button"; +import { GridCard } from "@components/Card"; +import { TextAreaWithLabel } from "@components/TextArea"; +import { SettingsPageHeader } from "@components/SettingsPageheader"; +import { useUiStore } from "@/hooks/stores"; +import useOCR from "@/hooks/useOCR"; +import { cx } from "@/cva.config"; + +export default function OCRModal({ videoElmRef }: { videoElmRef?: React.RefObject }) { + const { setDisableVideoFocusTrap } = useUiStore(); + const [ocrStatus, setOcrStatus] = useState(); + const [ocrError, setOcrError] = useState(null); + const handleOcrError = useCallback((error: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any + if (typeof error === "string") { + setOcrError(error); + } else { + setOcrError(error.message); + } + }, [setOcrError]); + const [ocrText, setOcrText] = useState(null); + + const { ocrImage } = useOCR(); + + const onConfirmOCR = useCallback(async () => { + setDisableVideoFocusTrap(true); + + setOcrText(null); + setOcrError(null); + setOcrStatus(undefined); + + if (!videoElmRef?.current) { + setOcrError("Video element not found"); + return; + } + + setOcrStatus({ + status: "Capturing image", + progress: 0, + jobId: "", + userJobId: "", + workerId: "", + }); + + // create a canvas from the video element then capture the image from the canvas + const video = videoElmRef?.current; + const canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + const ctx = canvas.getContext("2d"); + ctx?.drawImage(video, 0, 0, canvas.width, canvas.height); + + const text = await ocrImage(["eng"], canvas, { logger: setOcrStatus, errorHandler: handleOcrError }); + setOcrText(text); + + setOcrStatus(undefined); + setOcrError(null); + }, [ + videoElmRef, + setDisableVideoFocusTrap, + ocrImage, + setOcrStatus, + setOcrError, + handleOcrError, + ]); + + const ocrProgress = useMemo(() => { + if (!ocrStatus?.progress) return 0; + return Math.round(ocrStatus?.progress * 100); + }, [ocrStatus]); + + return ( + +
+
+
+
+ + +
+
+
e.stopPropagation()} + onKeyDown={e => e.stopPropagation()} + > + +
+
+
+ + {ocrStatus &&
+
+

+ {ocrStatus?.status} +

+ {ocrProgress}% +
+
+
+
+
} + + {ocrError && ( +
+ + + {ocrError} + +
+ )} +
+
+
+
+

+ Internet connectivity might be required to download Tesseract OCR trained data. +

+
+
+
+
+
+ ); +} diff --git a/ui/src/hooks/useOCR.ts b/ui/src/hooks/useOCR.ts index 6ced5578..085c7254 100644 --- a/ui/src/hooks/useOCR.ts +++ b/ui/src/hooks/useOCR.ts @@ -9,7 +9,9 @@ async function ocrImage( image: ImageLike, options?: Partial, ) { - const { createWorker } = await import('tesseract.js') + const tesseract = await import('tesseract.js') + const createWorker = tesseract.createWorker || tesseract.default.createWorker + const worker = await createWorker(language, undefined, options) const { data: { text } } = await worker.recognize(image) await worker.terminate()