diff --git a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx index 21989f05e..d21803bb4 100644 --- a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx +++ b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { resetSyntheticImageDetectionImage, @@ -13,9 +13,12 @@ import { Box, Card, CardHeader, + FormControlLabel, + FormGroup, Grid, LinearProgress, Stack, + Switch, } from "@mui/material"; import useMyStyles from "../../../Shared/MaterialUiStyles/useMyStyles"; @@ -31,6 +34,7 @@ import StringFileUploadField from "../../../Shared/StringFileUploadField"; import { preprocessFileUpload } from "../../../Shared/Utils/fileUtils"; import { syntheticImageDetectionAlgorithms } from "./SyntheticImageDetectionAlgorithms"; import { useLocation } from "react-router-dom"; +import { ROLES } from "../../../../constants/roles"; const SyntheticImageDetection = () => { const location = useLocation(); @@ -59,6 +63,8 @@ const SyntheticImageDetection = () => { const [imageType, setImageType] = useState(undefined); + const [autoResizeLocalFile, setAutoResizeLocalFile] = useState(true); + const dispatch = useDispatch(); const IMAGE_FROM = { @@ -66,6 +72,40 @@ const SyntheticImageDetection = () => { UPLOAD: "local", }; + const workerRef = useRef(null); + + useEffect(() => { + workerRef.current = new Worker( + new URL("../../../../workers/resizeImageWorker", import.meta.url), + ); + + return () => { + workerRef.current.terminate(); + }; + }, []); + + /** + * + * @param image + */ + const resizeImageWithWorker = (image) => { + return new Promise((resolve, reject) => { + const workerInstance = new Worker( + new URL("../../../../workers/resizeImageWorker", import.meta.url), + ); + workerInstance.postMessage(image); + + workerInstance.onerror = function (e) { + reject(e.error); + }; + + workerInstance.onmessage = function (e) { + // console.log(e); + resolve(e.data); + }; + }); + }; + const getSyntheticImageScores = async ( url, processURL, @@ -249,6 +289,14 @@ const SyntheticImageDetection = () => { * @returns {Promise} */ const handleSubmit = async (url) => { + const processedFile = autoResizeLocalFile + ? await resizeImageWithWorker(imageFile) + : imageFile; + + if (autoResizeLocalFile && processedFile) { + setImageFile(processedFile); + } + dispatch(resetSyntheticImageDetectionImage()); const urlInput = url ? url : input; const type = @@ -256,7 +304,13 @@ const SyntheticImageDetection = () => { ? IMAGE_FROM.URL : IMAGE_FROM.UPLOAD; - await getSyntheticImageScores(urlInput, true, dispatch, type, imageFile); + await getSyntheticImageScores( + urlInput, + true, + dispatch, + type, + processedFile, + ); }; useEffect(() => { @@ -298,6 +352,10 @@ const SyntheticImageDetection = () => { } }, [imageFile, input, result]); + const toggleAutoResizeLocalFile = () => { + setAutoResizeLocalFile((prev) => !prev); + }; + return ( { /> -
- - - - {isLoading && ( - - - - )} + +
+ + + + {role.includes(ROLES.EXTRA_FEATURE) && ( + + + } + label="Auto-Resize" + /> + + )} + + {isLoading && ( + + + + )} +
diff --git a/src/components/Shared/Utils/imageUtils.jsx b/src/components/Shared/Utils/imageUtils.jsx new file mode 100644 index 000000000..87640464b --- /dev/null +++ b/src/components/Shared/Utils/imageUtils.jsx @@ -0,0 +1,81 @@ +/** + * Converts an ImageData object to File + * @param imageData {ImageData} + * @param fileName {string} + * @param imageType {string} + * @returns {Promise} + */ +const imageDataToFile = async (imageData, fileName, imageType) => { + const offscreenCanvas = new OffscreenCanvas( + imageData.width, + imageData.height, + ); + + //1. Create an OffscreenCanvas + const ctx = offscreenCanvas.getContext("2d"); + ctx.putImageData(imageData, 0, 0); + + //2. Convert the OffscreenCanvas to a Blob + const blob = await offscreenCanvas.convertToBlob({ type: imageType }); + if (!blob) { + return new Error("OffscreenCanvas to Blob conversion failed"); + } + + //3. Convert the Blob to a File + return new File([blob], fileName, { type: blob.type }, imageType); +}; + +/** + * + * @param imageData {File | Blob} + * @returns {Promise} + */ +export const resizeImage = async (imageData) => { + const MAX_PIXEL_SIZE = 2073599; // < 2Mpx + + // Create ImageBitmap from image data + const imageBitmap = await createImageBitmap(imageData); + + // Check if the image exceeds the max pixel size + const originalPixelSize = imageBitmap.width * imageBitmap.height; + + if (originalPixelSize <= MAX_PIXEL_SIZE) { + // If the image is within the max pixel size, return as is + return imageData; + } + + // Calculate new dimensions maintaining the aspect ratio + const aspectRatio = imageBitmap.width / imageBitmap.height; + let newWidth, newHeight; + + if (aspectRatio > 1) { + newWidth = Math.sqrt(MAX_PIXEL_SIZE * aspectRatio); + newHeight = newWidth / aspectRatio; + } else { + newHeight = Math.sqrt(MAX_PIXEL_SIZE / aspectRatio); + newWidth = newHeight * aspectRatio; + } + + // Resize the image using an OffscreenCanvas + const resizedImageData = await resizeUsingCanvas( + imageBitmap, + newWidth, + newHeight, + ); + + return imageDataToFile( + resizedImageData, + imageData.name ?? "image", + imageData.type, + ); +}; + +// Helper function to resize the image using a canvas +const resizeUsingCanvas = async (imageBitmap, newWidth, newHeight) => { + const offscreenCanvas = new OffscreenCanvas(newWidth, newHeight); + const context = offscreenCanvas.getContext("2d"); + context.drawImage(imageBitmap, 0, 0, newWidth, newHeight); + + // Get ImageData from the OffscreenCanvas + return context.getImageData(0, 0, newWidth, newHeight); +}; diff --git a/src/workers/resizeImageWorker.js b/src/workers/resizeImageWorker.js new file mode 100644 index 000000000..bff134680 --- /dev/null +++ b/src/workers/resizeImageWorker.js @@ -0,0 +1,12 @@ +import { resizeImage } from "../components/Shared/Utils/imageUtils"; + +self.onmessage = async function (e) { + const data = e.data; + + // console.log(e); + const result = await resizeImage(data); + // console.log(result); + + // Send the result back to the main thread + self.postMessage(result); +};