UNPKG

romaine

Version:

React OpenCV Manipulation and Image Narration & Editing

1 lines 177 kB
{"version":3,"file":"index.jsx","sources":["../src/util/calculateDimensions.ts","../src/util/checkCrossOrigin.ts","../src/util/readFile.ts","../src/util/image/filter.ts","../src/util/image/warpPerspective.ts","../src/util/image/multiplyMatrices.ts","../src/util/image/addPadding.ts","../src/util/image/cropOpenCV.ts","../src/util/image/removeBackground.ts","../src/util/reducers/history.ts","../src/util/reducers/romaineReducer.ts","../src/util/reducers/index.ts","../src/util/configs/moduleConfig.ts","../src/components/romaine/index.tsx","../src/hooks/useRomaine.tsx","../src/components/Cropper/CropPoint.tsx","../src/components/Cropper/CropPoints.tsx","../src/components/Cropper/CropPointsDelimiters.tsx","../src/components/Cropper/CroppingCanvas.tsx","../src/util/buildImgContainerStyle.ts","../src/components/romaine/usePreview.tsx","../src/components/romaine/useCanvas.tsx","../src/util/image/flip.ts","../src/util/image/rotate.ts","../src/util/image/scale.ts","../src/util/image/mode.ts","../src/components/BrushRefine/BrushCanvas.tsx","../src/components/Canvas.tsx"],"sourcesContent":["interface CalculatedDimensions {\n width: number;\n height: number;\n ratio: number;\n}\ntype CalcDims = (\n width: number,\n height: number,\n externalMaxWidth: number,\n externalMaxHeight: number\n) => CalculatedDimensions;\n\nconst calcDims: CalcDims = (\n width,\n height,\n externalMaxWidth,\n externalMaxHeight\n) => {\n const ratio = width / height;\n\n const maxWidth = externalMaxWidth || window.innerWidth;\n const maxHeight = externalMaxHeight || window.innerHeight;\n const calculated = {\n width: maxWidth,\n height: Math.round(maxWidth / ratio),\n ratio: ratio,\n };\n\n if (calculated.height > maxHeight) {\n calculated.height = maxHeight;\n calculated.width = Math.round(maxHeight * ratio);\n }\n return calculated;\n};\n\nexport { CalculatedDimensions, calcDims };\n","export function isCrossOriginURL(url: string): boolean {\n const { location } = window;\n const parts = url.match(/^(\\w+:)\\/\\/([^:/?#]*):?(\\d*)/i);\n\n return (\n parts !== null &&\n (parts[1] !== location.protocol ||\n parts[2] !== location.hostname ||\n parts[3] !== location.port)\n );\n}\n","/**\n * Takes a file or a string, loads it and turns it into a DataURL\n * @param file as File | string\n * @returns DataURL (as string)\n */\nexport const readFile = (file: File | string): Promise<string> =>\n new Promise((resolve, reject) => {\n if (file instanceof File) {\n const reader = new FileReader();\n reader.onload = () => {\n resolve(reader.result as string);\n };\n reader.onerror = (err) => {\n reject(err);\n };\n reader.readAsDataURL(file);\n } else if (typeof file === \"string\") return resolve(file);\n else reject();\n });\n","import { OpenCV, OpenCVFilterProps } from \"../../types\";\n// https://amin-ahmadi.com/2016/03/24/sepia-filter-opencv/\nexport const applyFilter = async (\n cv: OpenCV,\n docCanvas: HTMLCanvasElement,\n filterCvParams: Partial<OpenCVFilterProps> = {}\n) => {\n // default options\n const options = {\n blur: false,\n th: true,\n thMode: cv.ADAPTIVE_THRESH_MEAN_C,\n thMeanCorrection: 15,\n thBlockSize: 25,\n thMax: 255,\n grayScale: true,\n ...filterCvParams,\n };\n const dst = cv.imread(docCanvas);\n\n if (options.grayScale) {\n cv.cvtColor(dst, dst, cv.COLOR_RGBA2GRAY, 0);\n }\n if (options.blur) {\n const ksize = new cv.Size(5, 5);\n cv.GaussianBlur(dst, dst, ksize, 0, 0, cv.BORDER_DEFAULT);\n }\n if (options.th) {\n if (options.grayScale) {\n cv.adaptiveThreshold(\n dst,\n dst,\n options.thMax,\n options.thMode,\n cv.THRESH_BINARY,\n options.thBlockSize,\n options.thMeanCorrection\n );\n } else {\n //@ts-ignore need to fix this type error (add to OpenCV type)\n dst.convertTo(dst, -1, 1, 60);\n cv.threshold(dst, dst, 170, 255, cv.THRESH_BINARY);\n }\n }\n cv.imshow(docCanvas, dst);\n};\n","import { ContourCoordinates } from \"../../components\";\nimport { ImagePtr, OpenCV } from \"../../types\";\n/**\n * perspective cropping utility (AKA keystone correction)\n * @param cv openCv\n * @param src The source image pointer\n * @param cropPoints\n * @param imageResizeRatio\n */\nexport const warpPerspective = (\n cv: OpenCV,\n src: ImagePtr,\n cropPoints: ContourCoordinates,\n imageResizeRatio: number\n): void => {\n // const src = cv.imread(docCanvas);\n const bR = cropPoints[\"right-bottom\"];\n const bL = cropPoints[\"left-bottom\"];\n const tR = cropPoints[\"right-top\"];\n const tL = cropPoints[\"left-top\"];\n\n // create source coordinates matrix\n const sourceCoordinates = [tL, tR, bR, bL].map((point) => [\n point.x / imageResizeRatio,\n point.y / imageResizeRatio,\n ]);\n\n // get max width\n const maxWidth = Math.max(bR.x - bL.x, tR.x - tL.x) / imageResizeRatio;\n // get max height\n const maxHeight = Math.max(bL.y - tL.y, bR.y - tR.y) / imageResizeRatio;\n\n // create dest coordinates matrix\n const destCoordinates = [\n [0, 0],\n [maxWidth - 1, 0],\n [maxWidth - 1, maxHeight - 1],\n [0, maxHeight - 1],\n ];\n\n // convert to open cv matrix objects\n const Ms = cv.matFromArray(\n 4,\n 1,\n cv.CV_32FC2,\n ([] as number[]).concat(...sourceCoordinates)\n );\n const Md = cv.matFromArray(\n 4,\n 1,\n cv.CV_32FC2,\n ([] as number[]).concat(...destCoordinates)\n );\n const transformMatrix = cv.getPerspectiveTransform(Ms, Md);\n // set new image size\n const dsize = new cv.Size(maxWidth, maxHeight);\n // perform warp\n cv.warpPerspective(\n src,\n src,\n transformMatrix,\n dsize,\n cv.INTER_LINEAR,\n cv.BORDER_CONSTANT,\n new cv.Scalar()\n );\n // cv.imshow(docCanvas, src);\n\n // src.delete();\n Ms.delete();\n Md.delete();\n transformMatrix.delete();\n};\n","export function multiplyMatrices(m1: number[][], m2: number[][]) {\n const result: number[][] = [];\n for (let i = 0; i < m1.length; i++) {\n result[i] = [];\n for (let j = 0; j < m2[0].length; j++) {\n let sum = 0;\n for (let k = 0; k < m1[0].length; k++) {\n sum += m1[i][k] * m2[k][j];\n }\n result[i][j] = sum;\n }\n }\n return result;\n}\n","import { AddPadding } from \"../../types/AddPadding\";\n\n/**\n * Function that adds transparent (or whatever the fill color is) to the image canvas\n * @see https://stackoverflow.com/questions/43391205/add-padding-to-images-to-get-them-into-the-same-shape#43391469\n * @example\n * addPadding(cv, canvasRef.current, { left: 500 }, { setPreviewPaneDimensions, showPreview });\n */\nexport const addPadding: AddPadding = (\n cv,\n src,\n { top = 0, bottom = 0, left = 0, right = 0 },\n { showPreview, setPreviewPaneDimensions }\n) => {\n const dst = new cv.Mat();\n // @ts-ignore\n cv.copyMakeBorder(\n cv.imread(src),\n dst,\n top,\n bottom,\n left,\n right,\n cv.BORDER_CONSTANT\n );\n\n setPreviewPaneDimensions({\n width: src.width + left + right,\n height: src.height + top + bottom,\n });\n cv.imshow(src, dst);\n showPreview();\n dst.delete();\n};\n","import { ContourCoordinates } from \"../../components\";\nimport { ImagePtr, OpenCV } from \"../../types\";\n/**\n * perspective cropping utility (AKA keystone correction)\n * @param cv openCv\n * @param docCanvas\n * @param cropPoints\n * @param imageResizeRatio\n * @param setPreviewPaneDimensions\n */\nexport const cropOpenCV = (\n cv: OpenCV,\n dst: ImagePtr,\n cropPoints: ContourCoordinates,\n imageResizeRatio: number\n // _canvasRef: HTMLCanvasElement,\n): void => {\n // const dst = cv.imread(docCanvas);\n\n // const bR = cropPoints[\"right-bottom\"];\n const bL = cropPoints[\"left-bottom\"];\n const tR = cropPoints[\"right-top\"];\n const tL = cropPoints[\"left-top\"];\n\n const l = tL.x / imageResizeRatio;\n const t = tL.y / imageResizeRatio;\n const r = tR.x / imageResizeRatio;\n const b = bL.y / imageResizeRatio;\n\n const M = cv.matFromArray(2, 3, cv.CV_64FC1, [1, 0, -l, 0, 1, -t]);\n // let dsize = new cv.Size(dst.rows - t - b, dst.cols - l - r);\n // const dsize = new cv.Size(dst.rows - l - r, dst.cols - t - b);\n const dsize = new cv.Size(\n dst.cols - l - (dst.cols - r),\n dst.rows - t - (dst.rows - b)\n );\n\n cv.warpAffine(\n dst,\n dst,\n M,\n dsize,\n cv.INTER_LINEAR,\n cv.BORDER_CONSTANT,\n new cv.Scalar()\n );\n // setTimeout(async () => {\n // cv.imshow(canvasRef, dst);\n // }, 0);\n // dst.delete();\n M.delete();\n};\n","import { ImagePtr, OpenCV } from \"../../types\";\n\nconst MAX_GRABCUT_DIM = 800;\n\n// --- Module-level GrabCut state storage ---\nlet storedMask: ImagePtr | null = null;\nlet storedBgdModel: ImagePtr | null = null;\nlet storedFgdModel: ImagePtr | null = null;\nlet storedScaleFactor = 1;\nlet storedSmallW = 0;\nlet storedSmallH = 0;\nlet lastAppliedStrokes: BrushStrokeData[] = [];\n\nexport interface BrushStrokeData {\n points: Array<{ x: number; y: number }>;\n mode: \"fg\" | \"bg\";\n brushRadius: number;\n}\n\nexport const clearGrabCutState = () => {\n try { storedMask?.delete(); } catch { /* noop */ }\n try { storedBgdModel?.delete(); } catch { /* noop */ }\n try { storedFgdModel?.delete(); } catch { /* noop */ }\n storedMask = null;\n storedBgdModel = null;\n storedFgdModel = null;\n storedScaleFactor = 1;\n storedSmallW = 0;\n storedSmallH = 0;\n lastAppliedStrokes = [];\n};\n\nexport const hasGrabCutMask = (): boolean => storedMask !== null;\nexport const getGrabCutScaleFactor = (): number => storedScaleFactor;\nexport const getGrabCutDims = (): { w: number; h: number } => ({\n w: storedSmallW,\n h: storedSmallH,\n});\nexport const setLastAppliedStrokes = (s: BrushStrokeData[]) => {\n lastAppliedStrokes = s;\n};\nexport const getLastAppliedStrokes = (): BrushStrokeData[] => lastAppliedStrokes;\n\n/**\n * Shared pipeline: convert raw GrabCut mask to binary, apply morphology,\n * blur, upscale, and write to src alpha channel.\n */\nconst applyMaskToAlpha = (\n cv: OpenCV,\n canvas: HTMLCanvasElement,\n src: ImagePtr,\n rawMask: ImagePtr,\n origW: number,\n origH: number,\n scaleFactor: number\n) => {\n const mats: ImagePtr[] = [];\n const del = (m: ImagePtr) => { mats.push(m); return m; };\n\n try {\n // Clone so we don't modify the stored mask\n const binMask = del(rawMask.clone());\n const maskData = (binMask as any).data as Uint8Array;\n for (let i = 0; i < maskData.length; i++) {\n maskData[i] = (maskData[i] === 1 || maskData[i] === 3) ? 255 : 0;\n }\n\n // Morphological cleanup if available\n const hasMorphology =\n typeof (cv as any).getStructuringElement === \"function\" &&\n typeof (cv as any).morphologyEx === \"function\";\n\n if (hasMorphology) {\n const kernel = del(\n (cv as any).getStructuringElement(\n (cv as any).MORPH_ELLIPSE,\n new cv.Size(5, 5)\n )\n );\n (cv as any).morphologyEx(binMask, binMask, (cv as any).MORPH_CLOSE, kernel);\n (cv as any).morphologyEx(binMask, binMask, (cv as any).MORPH_OPEN, kernel);\n }\n\n // Edge feathering\n const blurred = del(new cv.Mat());\n cv.GaussianBlur(binMask, blurred, new cv.Size(5, 5), 0, 0, cv.BORDER_DEFAULT);\n\n // Upscale mask back to original dimensions\n let finalMask: ImagePtr;\n if (scaleFactor < 1) {\n finalMask = del(new cv.Mat());\n cv.resize(blurred, finalMask, new cv.Size(origW, origH), 0, 0, cv.INTER_LINEAR);\n } else {\n finalMask = blurred;\n }\n\n // Apply mask as alpha channel\n const srcData = (src as any).data as Uint8Array;\n const finalMaskData = (finalMask as any).data as Uint8Array;\n const totalPixels = origW * origH;\n for (let i = 0; i < totalPixels; i++) {\n srcData[i * 4 + 3] = finalMaskData[i];\n }\n\n cv.imshow(canvas, src);\n } finally {\n for (const m of mats) {\n try { m.delete(); } catch { /* noop */ }\n }\n }\n};\n\n/**\n * Remove the background from an image using OpenCV's GrabCut algorithm.\n * Stores the GrabCut mask for subsequent refinement via refineBackground().\n */\nexport const removeBackground = (\n cv: OpenCV,\n canvas: HTMLCanvasElement,\n src: ImagePtr\n) => {\n if (!cv.grabCut) {\n throw new Error(\"cv.grabCut is not available in this OpenCV build\");\n }\n\n // Clear any previous stored state\n clearGrabCutState();\n\n const origW = src.cols;\n const origH = src.rows;\n const maxDim = Math.max(origW, origH);\n const sf = maxDim > MAX_GRABCUT_DIM ? MAX_GRABCUT_DIM / maxDim : 1;\n const sW = Math.round(origW * sf);\n const sH = Math.round(origH * sf);\n\n const mats: ImagePtr[] = [];\n const del = (m: ImagePtr) => { mats.push(m); return m; };\n\n try {\n // Downscale if needed\n let small: ImagePtr;\n if (sf < 1) {\n small = del(new cv.Mat());\n cv.resize(src, small, new cv.Size(sW, sH), 0, 0, cv.INTER_AREA);\n } else {\n small = del(src.clone());\n }\n\n const rgb = del(new cv.Mat());\n cv.cvtColor(small, rgb, cv.COLOR_RGBA2RGB, 0);\n\n const mask = del(new cv.Mat());\n const bgdModel = del(new cv.Mat());\n const fgdModel = del(new cv.Mat());\n\n const insetX = Math.max(1, Math.floor(sW * 0.03));\n const insetY = Math.max(1, Math.floor(sH * 0.03));\n const rect = new cv.Rect(insetX, insetY, sW - insetX * 2, sH - insetY * 2);\n\n // Pass 1: rectangle init\n cv.grabCut(rgb, mask, rect, bgdModel, fgdModel, 5, cv.GC_INIT_WITH_RECT);\n // Pass 2: mask refinement\n cv.grabCut(rgb, mask, rect, bgdModel, fgdModel, 3, (cv as any).GC_INIT_WITH_MASK ?? 1);\n\n // Store mask & models for future refinement (before binary conversion)\n storedMask = mask.clone();\n storedBgdModel = bgdModel.clone();\n storedFgdModel = fgdModel.clone();\n storedScaleFactor = sf;\n storedSmallW = sW;\n storedSmallH = sH;\n\n // Apply the mask to the image\n applyMaskToAlpha(cv, canvas, src, mask, origW, origH, sf);\n } finally {\n for (const m of mats) {\n try { m.delete(); } catch { /* noop */ }\n }\n }\n};\n\n/**\n * Refine the background removal using user-supplied brush strokes.\n * Strokes mark pixels as definite foreground or background, then\n * GrabCut re-runs with GC_INIT_WITH_MASK.\n */\nexport const refineBackground = (\n cv: OpenCV,\n canvas: HTMLCanvasElement,\n src: ImagePtr,\n strokes: BrushStrokeData[]\n) => {\n if (!storedMask || !storedBgdModel || !storedFgdModel) {\n throw new Error(\"No GrabCut mask available. Run removeBackground first.\");\n }\n if (!strokes || strokes.length === 0) return;\n\n const origW = src.cols;\n const origH = src.rows;\n\n const mats: ImagePtr[] = [];\n const del = (m: ImagePtr) => { mats.push(m); return m; };\n\n try {\n // Downscale current image for GrabCut\n let small: ImagePtr;\n if (storedScaleFactor < 1) {\n small = del(new cv.Mat());\n cv.resize(src, small, new cv.Size(storedSmallW, storedSmallH), 0, 0, cv.INTER_AREA);\n } else {\n small = del(src.clone());\n }\n\n const rgb = del(new cv.Mat());\n cv.cvtColor(small, rgb, cv.COLOR_RGBA2RGB, 0);\n\n // Clone the stored mask and paint user strokes onto it\n const refineMask = del(storedMask.clone());\n const maskData = (refineMask as any).data as Uint8Array;\n\n for (const stroke of strokes) {\n const gcValue = stroke.mode === \"fg\" ? 1 : 0; // GC_FGD=1, GC_BGD=0\n for (const pt of stroke.points) {\n const r = stroke.brushRadius;\n const rSq = r * r;\n const yMin = Math.max(0, pt.y - r);\n const yMax = Math.min(storedSmallH - 1, pt.y + r);\n const xMin = Math.max(0, pt.x - r);\n const xMax = Math.min(storedSmallW - 1, pt.x + r);\n for (let my = yMin; my <= yMax; my++) {\n const dy = my - pt.y;\n for (let mx = xMin; mx <= xMax; mx++) {\n const dx = mx - pt.x;\n if (dx * dx + dy * dy <= rSq) {\n maskData[my * storedSmallW + mx] = gcValue;\n }\n }\n }\n }\n }\n\n // Re-run GrabCut with user-updated mask\n const dummyRect = new cv.Rect(0, 0, 1, 1);\n cv.grabCut(\n rgb, refineMask, dummyRect,\n storedBgdModel, storedFgdModel,\n 3, (cv as any).GC_INIT_WITH_MASK ?? 1\n );\n\n // Update stored mask with refined result\n storedMask.delete();\n storedMask = refineMask.clone();\n\n // Apply the refined mask to the image\n applyMaskToAlpha(cv, canvas, src, refineMask, origW, origH, storedScaleFactor);\n } finally {\n for (const m of mats) {\n try { m.delete(); } catch { /* noop */ }\n }\n }\n};\n","import type { RomaineState, HistoryAction, RomaineHistory } from \".\";\nimport { getLastAppliedStrokes } from \"../image/removeBackground\";\n/**\n * function to add the current state to the history\n *\n * This function only depends on state inside romaine and therefore is called with no input\n * @example PushHistory();\n */\nexport type PushHistory = (customPayload?: unknown) => void;\n/**\n * function to clear the history on romaine\n * @example ClearHistory();\n */\nexport type ClearHistory = () => void;\n\n/**\n *\n * @param state RomaineState\n * @param payload HistoryAction[\"payload\"]\n * @todo\n * 1) reduce rotation to one command if previous history is also rotation\n */\nexport const history = (\n state: RomaineState,\n payload: HistoryAction[\"payload\"]\n): RomaineState => {\n switch (payload.cmd) {\n case \"CLEAR\": {\n // overwrite with empty array\n return {\n ...state,\n history: { commands: [], pointer: 0 },\n };\n }\n case \"PUSH\": {\n if (state.mode === \"undo\" || state.mode === \"redo\" || state.mode === null)\n return { ...state };\n const pointer = state.history.pointer;\n let newCommands = state.history.commands;\n if (pointer < newCommands.length) {\n newCommands = state.history.commands.reduce(\n (a, c, i) => (pointer > i ? [...a, c] : a),\n [] as typeof state.history.commands\n );\n }\n // Use custom payload from plugin if provided, otherwise derive from state\n const entry = payload.customPayload !== undefined\n ? { cmd: state.mode as string, payload: payload.customPayload }\n : getHistoryFromState(state);\n return {\n ...state,\n history: {\n ...state.history,\n commands: [...newCommands, entry],\n pointer: pointer + 1,\n },\n };\n }\n case \"UNDO\": {\n const pointer = state.history.pointer;\n return {\n ...state,\n history: { ...state.history, pointer: pointer ? pointer - 1 : 0 },\n };\n }\n case \"REDO\": {\n const pointer = state.history.pointer;\n return {\n ...state,\n history: { ...state.history, pointer: pointer ? pointer - 1 : 0 },\n };\n }\n default:\n return { ...state };\n }\n};\n\nconst getHistoryFromState = ({\n mode,\n angle,\n scale,\n cropPoints,\n}: RomaineState): RomaineHistory => {\n switch (mode) {\n case \"rotate-left\":\n return { cmd: mode, payload: angle % 360 };\n case \"rotate-right\":\n return { cmd: mode, payload: (360 - angle) % 360 };\n case \"flip-horizontal\":\n return { cmd: mode, payload: null };\n case \"flip-vertical\":\n return { cmd: mode, payload: null };\n case \"crop\":\n return { cmd: mode, payload: cropPoints };\n case \"perspective-crop\":\n return { cmd: mode, payload: cropPoints };\n case \"scale\":\n return { cmd: mode, payload: scale };\n case \"remove-background\":\n return { cmd: mode, payload: null };\n case \"refine-background\":\n return { cmd: mode, payload: getLastAppliedStrokes() };\n case \"full-reset\":\n throw new Error(\n 'error: action \"full-reset\" should not call `history`.`PUSH`'\n ).stack;\n case null:\n // handles null\n throw new Error(\n \"error: action of type null should not call `history`.`PUSH`\"\n ).stack;\n default:\n // Plugin modes — return generic entry (plugins should use customPayload instead)\n return { cmd: mode as string, payload: null };\n }\n};\n","import { RomaineState } from \".\";\nimport { ContourCoordinates } from \"../../components\";\nimport { history } from \"./history\";\nexport type RomaineCommands =\n | \"crop\"\n | \"perspective-crop\"\n | \"rotate-right\"\n | \"rotate-left\"\n | \"full-reset\"\n | \"undo\"\n | \"redo\"\n | \"preview\"\n | \"flip-horizontal\"\n | \"flip-vertical\"\n | \"scale\"\n | \"remove-background\"\n | \"refine-background\"\n | (string & {}); // allow plugin mode strings\nexport type RomaineModes = null | RomaineCommands;\n\nexport interface ModeAction {\n type: \"MODE\";\n payload: RomaineModes;\n}\nexport interface AngleAction {\n type: \"ANGLE\";\n payload: number;\n}\nexport interface ScaleAction {\n type: \"SCALE\";\n payload: {\n width: number;\n height: number;\n };\n}\nexport interface ImageUpdateAction {\n type: \"IMAGE-UPDATE\";\n payload: {\n width: number;\n height: number;\n id: string | null;\n };\n}\nexport interface HistoryAction {\n type: \"HISTORY\";\n payload: { cmd: \"PUSH\" | \"CLEAR\" | \"UNDO\" | \"REDO\"; customPayload?: unknown };\n}\nexport interface CropPointsAction {\n type: \"CROP_POINTS\";\n payload: ContourCoordinates | undefined;\n}\nexport type RomaineReducer =\n | {\n type: \"JOIN_PAYLOAD\" | \"REMOVE_CROPPER\";\n payload?: any;\n }\n | ModeAction\n | AngleAction\n | ScaleAction\n | HistoryAction\n | CropPointsAction\n | ImageUpdateAction;\nexport const romaineReducer = (\n state: RomaineState,\n action: RomaineReducer\n): RomaineState => {\n switch (action.type) {\n case \"JOIN_PAYLOAD\":\n return { ...state, ...action.payload };\n case \"MODE\":\n if (state.mode === action.payload) return { ...state, mode: null };\n return { ...state, mode: action.payload };\n case \"ANGLE\":\n return { ...state, angle: action.payload };\n case \"SCALE\":\n return { ...state, scale: action.payload };\n case \"CROP_POINTS\":\n if (!action.payload) return { ...state };\n return { ...state, cropPoints: action.payload };\n case \"HISTORY\":\n return { ...history(state, action.payload) };\n case \"IMAGE-UPDATE\":\n return { ...state, image: action.payload };\n default:\n return { ...state };\n }\n};\n","import type { ContourCoordinates } from \"../../components\";\nimport type { RomaineModes, RomaineCommands } from \"./romaineReducer\";\nexport interface RomaineHistory {\n cmd: RomaineCommands;\n payload: any;\n}\n\nexport interface RomaineState {\n mode: RomaineModes;\n angle: number;\n scale: {\n width: number;\n height: number;\n };\n image: {\n /** The original image width */\n width: number;\n /** The original image height */\n height: number;\n /** The dom id */\n id: string | null;\n };\n cropPoints: ContourCoordinates;\n /** A command and a payload.\n * The payload is automatically generated based on current state\n * (e.g. angle of rotation, crop point locations)\n */\n history: { commands: RomaineHistory[]; pointer: number };\n}\nexport const initialRomaineState: RomaineState = {\n mode: null,\n angle: 90,\n scale: {\n width: 0,\n height: 0,\n },\n image: {\n width: 0,\n height: 0,\n id: null,\n },\n history: { commands: [], pointer: 0 },\n cropPoints: {\n \"left-top\": { x: 0, y: 0 },\n \"left-bottom\": { x: 0, y: 0 },\n \"right-bottom\": { x: 0, y: 0 },\n \"right-top\": { x: 0, y: 0 },\n },\n};\n\nexport type SetCropPoints = (\n cropPoints:\n | undefined\n | ContourCoordinates\n | ((CPs?: ContourCoordinates) => ContourCoordinates | undefined)\n) => void;\nexport * from \"./romaineReducer\";\nexport * from \"./history\";\n","export interface ModuleConfig {\n wasmBinaryFile: string;\n usingWasm: boolean;\n onRuntimeInitialized?: () => void;\n}\n\nexport const moduleConfig: ModuleConfig = {\n wasmBinaryFile: \"opencv_js.wasm\",\n usingWasm: true,\n};\n","import React, {\n FC,\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n ReactNode,\n useReducer,\n} from \"react\";\nimport PropTypes from \"prop-types\";\nimport { moduleConfig } from \"../../util/configs\";\nimport { romaineReducer, initialRomaineState, ClearHistory } from \"../../util\";\nimport type { RomaineState, PushHistory, SetCropPoints } from \"../../util\";\nimport type { OpenCV } from \"../../types/openCV\";\nimport type { ImageExportOptions } from \"../Romaine.types\";\ndeclare global {\n interface Window {\n cv: OpenCV;\n Module: typeof moduleConfig;\n }\n}\nexport interface CanvasApi {\n getBlob: (opts?: Partial<ImageExportOptions>) => Promise<Blob | null>;\n setFromBlob: (blob: Blob) => Promise<void>;\n}\nexport interface RomaineContext {\n loaded: boolean;\n cv: OpenCV;\n romaine: RomaineState & {\n clearHistory: ClearHistory;\n };\n canvasApi: React.MutableRefObject<CanvasApi | null>;\n setImage: React.Dispatch<React.SetStateAction<string | File | null>>;\n setMode?: (mode: RomaineState[\"mode\"]) => void;\n setAngle?: (angle: RomaineState[\"angle\"]) => void;\n setScale?: (scale: RomaineState[\"scale\"]) => void;\n setCropPoints: SetCropPoints;\n pushHistory?: PushHistory;\n undo: PushHistory;\n redo: PushHistory;\n updateImageInformation?: (imageInformation: RomaineState[\"image\"]) => void;\n}\n\nconst OpenCvContext = createContext<RomaineContext>({\n loaded: false,\n romaine: initialRomaineState as unknown as RomaineContext[\"romaine\"],\n setCropPoints: null as unknown as SetCropPoints,\n undo: null as unknown as PushHistory,\n redo: null as unknown as PushHistory,\n} as RomaineContext);\nconst { Consumer: OpenCvConsumer, Provider } = OpenCvContext;\n\nconst scriptId = \"openCvScriptTag\";\nexport interface ROMAINE {\n openCvPath?: string;\n onLoad?: (openCv: OpenCV) => void;\n children?: ReactNode;\n /** Angle to use when rotating images @default 90 */\n angle?: number;\n}\n/**\n * a romaine context for use in getting openCV and the canvas ref element\n * @todo\n * 1) Add ref to provider\n * 2) See if nonce is really required here\n */\nconst Romaine: FC<ROMAINE> = ({\n openCvPath,\n children,\n onLoad,\n angle = 90,\n}: ROMAINE) => {\n const [loaded, setLoaded] = useState(false);\n const [_image, setImage] = useState<File | string | null>(null);\n const canvasApi = useRef<CanvasApi | null>(null);\n\n const handleOnLoad = useCallback(() => {\n onLoad && onLoad(window.cv);\n setLoaded(true);\n }, [onLoad, setLoaded]);\n\n const generateOpenCvScriptTag = useMemo(() => {\n // make sure we are in the browser\n if (typeof window !== \"undefined\") {\n if (!document.getElementById(scriptId) && !window.cv) {\n const js = document.createElement(\"script\");\n // append to head\n js.id = scriptId;\n js.nonce = \"8IBTHwOdqNKAWeKl7plt8g==\";\n js.defer = true;\n js.async = true;\n\n js.src = openCvPath || \"https://docs.opencv.org/3.4.13/opencv.js\";\n return js;\n } else if (document.getElementById(scriptId) && !window.cv) {\n return document.getElementById(scriptId);\n }\n }\n }, [openCvPath]);\n\n useEffect(() => {\n if (window.cv) {\n handleOnLoad();\n return;\n }\n\n // https://docs.opencv.org/3.4/dc/de6/tutorial_js_nodejs.html\n // https://medium.com/code-divoire/integrating-opencv-js-with-an-angular-application-20ae11c7e217\n // https://stackoverflow.com/questions/56671436/cv-mat-is-not-a-constructor-opencv\n moduleConfig.onRuntimeInitialized = handleOnLoad;\n window.Module = moduleConfig;\n\n // if (!document.getElementById(scriptId))\n if (generateOpenCvScriptTag && !document.getElementById(scriptId))\n document.body.appendChild(generateOpenCvScriptTag);\n // else handleOnLoad();\n }, [openCvPath, handleOnLoad, generateOpenCvScriptTag]);\n\n const [romaine, dispatchRomaine] = useReducer(\n romaineReducer,\n initialRomaineState\n );\n\n const setMode = useCallback(\n (mode: RomaineState[\"mode\"]) => {\n dispatchRomaine({ type: \"MODE\", payload: mode });\n },\n [dispatchRomaine]\n );\n const setAngle = useCallback(\n (angle: RomaineState[\"angle\"]) => {\n dispatchRomaine({ type: \"ANGLE\", payload: angle });\n },\n [dispatchRomaine]\n );\n const setScale = useCallback(\n (scale: RomaineState[\"scale\"]) => {\n dispatchRomaine({ type: \"SCALE\", payload: scale });\n },\n [dispatchRomaine]\n );\n const updateImageInformation = useCallback(\n (image: RomaineState[\"image\"]) => {\n dispatchRomaine({\n type: \"SCALE\",\n payload: {\n width: image.width,\n height: image.height,\n },\n });\n dispatchRomaine({ type: \"IMAGE-UPDATE\", payload: image });\n },\n [dispatchRomaine]\n );\n const {\n cropPoints,\n history: { pointer },\n } = romaine;\n\n const setCropPoints: SetCropPoints = useCallback(\n (payload) => {\n if (typeof payload === \"function\") payload = payload(cropPoints);\n dispatchRomaine({ type: \"CROP_POINTS\", payload });\n },\n [dispatchRomaine, cropPoints]\n );\n\n const pushHistory: PushHistory = useCallback((customPayload?: unknown) => {\n dispatchRomaine({ type: \"HISTORY\", payload: { cmd: \"PUSH\", customPayload } });\n }, [dispatchRomaine]);\n\n const clearHistory: ClearHistory = useCallback(() => {\n dispatchRomaine({ type: \"HISTORY\", payload: { cmd: \"CLEAR\" } });\n }, [dispatchRomaine]);\n\n const moveHistory: (direction: boolean) => PushHistory = useCallback(\n (direction: boolean) => {\n if (direction)\n return () => {\n dispatchRomaine({ type: \"MODE\", payload: null });\n dispatchRomaine({ type: \"HISTORY\", payload: { cmd: \"UNDO\" } });\n };\n else\n return () => {\n dispatchRomaine({ type: \"MODE\", payload: null });\n dispatchRomaine({ type: \"HISTORY\", payload: { cmd: \"REDO\" } });\n };\n },\n [dispatchRomaine]\n );\n\n useEffect(() => {\n setAngle(angle);\n }, [angle, setAngle]);\n const memoizedProviderValue: RomaineContext = useMemo(\n () => ({\n loaded,\n cv:\n typeof window !== \"undefined\"\n ? window?.cv\n : (null as unknown as OpenCV),\n romaine: { ...romaine, history: { ...romaine.history }, clearHistory },\n canvasApi,\n setImage,\n setMode,\n setAngle,\n setScale,\n updateImageInformation,\n pushHistory,\n setCropPoints,\n undo: moveHistory(true),\n redo: moveHistory(false),\n }),\n [\n loaded,\n romaine,\n pointer,\n setMode,\n setAngle,\n setScale,\n pushHistory,\n moveHistory,\n clearHistory,\n setCropPoints,\n updateImageInformation,\n ]\n );\n // const { canvasRef } = useCanvas({ image });\n // usePreview({ cv: memoizedProviderValue, canvasRef });\n return <Provider value={memoizedProviderValue}>{children}</Provider>;\n};\nRomaine.propTypes = {\n openCvPath: PropTypes.string,\n children: PropTypes.node,\n onLoad: PropTypes.func,\n angle: PropTypes.number,\n};\n\nexport { OpenCvConsumer, OpenCvContext, Romaine };\n","import { useContext } from \"react\";\nimport { OpenCvContext } from \"../components/romaine\";\nexport type { CanvasApi } from \"../components/romaine\";\n\nexport const useRomaine = () => useContext(OpenCvContext);\n","import { useMemo, useCallback, useRef, useState, CSSProperties } from \"react\";\nimport type { FC, RefObject } from \"react\";\nimport Draggable, {\n ControlPosition,\n DraggableEventHandler,\n DraggableProps,\n} from \"react-draggable\";\nimport { useRomaine } from \"../../hooks\";\nexport interface CoordinateXY {\n x: number;\n y: number;\n}\nexport interface ContourCoordinates {\n \"left-bottom\": CoordinateXY;\n \"left-top\": CoordinateXY;\n \"right-bottom\": CoordinateXY;\n \"right-top\": CoordinateXY;\n}\n/**\n * Add default styles to the points\n * Makes sure position absolute cannot be overwritten\n * @param cropPointStyles\n */\nconst buildCropPointStyle = (\n cropPointStyles: CSSProperties = {}\n): CSSProperties => ({\n backgroundColor: \"transparent\",\n border: \"4px solid #3cabe2\",\n zIndex: 1001,\n borderRadius: \"100%\",\n cursor: \"grab\",\n ...cropPointStyles,\n position: \"absolute\",\n});\n\ntype PointArea = keyof ContourCoordinates;\n\nexport interface CropPointProps {\n pointSize: number;\n defaultPosition?: ControlPosition;\n onStop: (\n position: CoordinateXY,\n area: keyof ContourCoordinates,\n cropPoints: ContourCoordinates\n ) => void;\n onDrag: (position: CoordinateXY, area: keyof ContourCoordinates) => void;\n bounds: DraggableProps[\"bounds\"];\n cropPointStyles?: CSSProperties;\n previewCanvasRef?: RefObject<HTMLCanvasElement | null>;\n}\n\nconst MAGNIFIER_SIZE = 120;\nconst ZOOM_LEVEL = 3;\nconst MAGNIFIER_OFFSET = 30;\n\nconst drawMagnifier = (\n magnifierCanvas: HTMLCanvasElement,\n previewCanvas: HTMLCanvasElement,\n pointX: number,\n pointY: number\n) => {\n const ctx = magnifierCanvas.getContext(\"2d\");\n if (!ctx) return;\n\n ctx.clearRect(0, 0, MAGNIFIER_SIZE, MAGNIFIER_SIZE);\n\n // Circular clip\n ctx.save();\n ctx.beginPath();\n ctx.arc(\n MAGNIFIER_SIZE / 2,\n MAGNIFIER_SIZE / 2,\n MAGNIFIER_SIZE / 2,\n 0,\n Math.PI * 2\n );\n ctx.clip();\n\n // Draw zoomed region from preview canvas\n const sourceSize = MAGNIFIER_SIZE / ZOOM_LEVEL;\n ctx.drawImage(\n previewCanvas,\n pointX - sourceSize / 2,\n pointY - sourceSize / 2,\n sourceSize,\n sourceSize,\n 0,\n 0,\n MAGNIFIER_SIZE,\n MAGNIFIER_SIZE\n );\n ctx.restore();\n\n // Border circle\n ctx.beginPath();\n ctx.arc(\n MAGNIFIER_SIZE / 2,\n MAGNIFIER_SIZE / 2,\n MAGNIFIER_SIZE / 2 - 2,\n 0,\n Math.PI * 2\n );\n ctx.strokeStyle = \"#3cabe2\";\n ctx.lineWidth = 3;\n ctx.stroke();\n\n // Crosshair\n ctx.beginPath();\n ctx.moveTo(MAGNIFIER_SIZE / 2 - 8, MAGNIFIER_SIZE / 2);\n ctx.lineTo(MAGNIFIER_SIZE / 2 + 8, MAGNIFIER_SIZE / 2);\n ctx.moveTo(MAGNIFIER_SIZE / 2, MAGNIFIER_SIZE / 2 - 8);\n ctx.lineTo(MAGNIFIER_SIZE / 2, MAGNIFIER_SIZE / 2 + 8);\n ctx.strokeStyle = \"rgba(255,255,255,0.8)\";\n ctx.lineWidth = 1;\n ctx.stroke();\n};\n\n/**\n * @returns A crop point to use during cropping and image rotation\n */\nexport const CropPoint: FC<CropPointProps & { pointArea: PointArea }> = ({\n pointSize,\n pointArea,\n defaultPosition,\n onStop: externalOnStop,\n onDrag: externalOnDrag,\n bounds,\n cropPointStyles = {},\n previewCanvasRef,\n}) => {\n const {\n romaine: { cropPoints },\n } = useRomaine();\n const cropPointStyle = useMemo(() => {\n cropPointStyles.width = pointSize;\n cropPointStyles.height = pointSize;\n\n return buildCropPointStyle(cropPointStyles);\n }, [cropPointStyles, pointSize]);\n\n const magnifierRef = useRef<HTMLCanvasElement>(null);\n const [isDragging, setIsDragging] = useState(false);\n\n const updateMagnifier = useCallback(\n (pointX: number, pointY: number) => {\n if (!magnifierRef.current || !previewCanvasRef?.current) return;\n drawMagnifier(magnifierRef.current, previewCanvasRef.current, pointX, pointY);\n },\n [previewCanvasRef]\n );\n\n const onStart: DraggableEventHandler = useCallback(\n (_, data) => {\n setIsDragging(true);\n updateMagnifier(data.x + pointSize / 2, data.y + pointSize / 2);\n },\n [pointSize, updateMagnifier]\n );\n\n const onDrag: DraggableEventHandler = useCallback(\n (_, position) => {\n const px = position.x + pointSize / 2;\n const py = position.y + pointSize / 2;\n externalOnDrag({ ...position, x: px, y: py }, pointArea);\n updateMagnifier(px, py);\n },\n [externalOnDrag, pointArea, pointSize, updateMagnifier]\n );\n\n const onStop: DraggableEventHandler = useCallback(\n (_, position) => {\n setIsDragging(false);\n externalOnStop(\n {\n ...position,\n x: position.x + pointSize / 2,\n y: position.y + pointSize / 2,\n },\n pointArea,\n cropPoints\n );\n },\n [externalOnStop, cropPoints, pointArea, pointSize]\n );\n\n const nodeRef = useRef<HTMLDivElement>(null);\n\n // Position magnifier above or below with hysteresis to prevent flipping\n const magnifierSide = useRef<\"above\" | \"below\">(\"above\");\n const pointPos = cropPoints[pointArea];\n const threshold = MAGNIFIER_SIZE + MAGNIFIER_OFFSET;\n const buffer = 40;\n\n if (magnifierSide.current === \"above\" && pointPos.y < threshold) {\n magnifierSide.current = \"below\";\n } else if (magnifierSide.current === \"below\" && pointPos.y > threshold + buffer) {\n magnifierSide.current = \"above\";\n }\n\n const magX = pointPos.x - MAGNIFIER_SIZE / 2;\n const magY = magnifierSide.current === \"above\"\n ? pointPos.y - MAGNIFIER_SIZE - MAGNIFIER_OFFSET\n : pointPos.y + pointSize + MAGNIFIER_OFFSET;\n\n return (\n <>\n <Draggable\n nodeRef={nodeRef as React.RefObject<HTMLElement>}\n bounds={bounds}\n defaultPosition={defaultPosition}\n position={{\n x: pointPos.x - pointSize / 2,\n y: pointPos.y - pointSize / 2,\n }}\n onStart={onStart}\n onDrag={onDrag}\n onStop={onStop}\n >\n <div ref={nodeRef} style={cropPointStyle} />\n </Draggable>\n <canvas\n ref={magnifierRef}\n width={MAGNIFIER_SIZE}\n height={MAGNIFIER_SIZE}\n style={{\n position: \"absolute\",\n left: magX,\n top: magY,\n width: MAGNIFIER_SIZE,\n height: MAGNIFIER_SIZE,\n borderRadius: \"50%\",\n zIndex: 1002,\n pointerEvents: \"none\",\n boxShadow: \"0 2px 10px rgba(0,0,0,0.3)\",\n display: isDragging ? \"block\" : \"none\",\n }}\n />\n </>\n );\n};\n","import T from \"prop-types\";\nimport { CropPoint, CropPointProps } from \"./CropPoint\";\n\nexport interface CropPointsProps extends CropPointProps {\n previewDims: {\n ratio: number;\n width: number;\n height: number;\n };\n}\n\nconst CropPoints = ({ previewDims, ...otherProps }: CropPointsProps) => {\n return (\n <>\n <CropPoint\n pointArea=\"left-top\"\n defaultPosition={{ x: 0, y: 0 }}\n {...otherProps}\n />\n <CropPoint\n pointArea=\"right-top\"\n defaultPosition={{ x: previewDims.width, y: 0 }}\n {...otherProps}\n />\n <CropPoint\n pointArea=\"right-bottom\"\n defaultPosition={{ x: 0, y: previewDims.height }}\n {...otherProps}\n />\n <CropPoint\n pointArea=\"left-bottom\"\n defaultPosition={{\n x: previewDims.width,\n y: previewDims.height,\n }}\n {...otherProps}\n />\n </>\n );\n};\n\nexport { CropPoints };\n\nCropPoints.propTypes = {\n previewDims: T.shape({\n ratio: T.number,\n width: T.number,\n height: T.number,\n }),\n};\n","import React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport T from \"prop-types\";\nimport { ContourCoordinates, CoordinateXY } from \"..\";\nimport { CropFunc } from \"../Romaine.types\";\n\ninterface CropPointsDelimiters {\n // romaineRef: React.RefObject<RomaineRef>;\n crop: CropFunc;\n cropPoints: ContourCoordinates;\n pointSize: number;\n lineColor?: string;\n lineWidth?: number;\n saltId?: string;\n previewDims: {\n width: number;\n height: number;\n ratio: number;\n };\n}\n/**\n * Create the lines for the cropper utility\n */\nconst CropPointsDelimiters = ({\n // romaineRef,\n crop,\n cropPoints,\n previewDims,\n lineWidth = 3,\n lineColor = \"#3cabe2\",\n pointSize,\n ...props\n}: CropPointsDelimiters) => {\n const canvas = useRef<HTMLCanvasElement>(null);\n\n const clearCanvas = useCallback(() => {\n if (canvas.current) {\n const ctx = canvas.current.getContext(\"2d\");\n ctx && ctx.clearRect(0, 0, previewDims.width, previewDims.height);\n }\n }, [previewDims]);\n /** This needs to do a better job. See Issue #2\n * @link https://github.com/DanielBailey-web/romaine/issues/2\n *\n */\n const sortPoints = useCallback(() => {\n const sortOrder = [\n \"left-top\",\n \"right-top\",\n \"right-bottom\",\n \"left-bottom\",\n ] as const;\n return sortOrder.reduce(\n (acc, pointPos) => [...acc, cropPoints[pointPos]],\n [] as CoordinateXY[]\n );\n }, [cropPoints]);\n\n const drawShape = useCallback(\n ([point1, point2, point3, point4]: CoordinateXY[]) => {\n const ctx = canvas.current && canvas.current.getContext(\"2d\");\n if (ctx) {\n ctx.lineWidth = lineWidth;\n ctx.strokeStyle = lineColor;\n\n ctx.beginPath();\n ctx.moveTo(point1.x + pointSize / 2, point1.y);\n ctx.lineTo(point2.x - pointSize / 2, point2.y);\n\n ctx.moveTo(point2.x, point2.y + pointSize / 2);\n ctx.lineTo(point3.x, point3.y - pointSize / 2);\n\n ctx.moveTo(point3.x - pointSize / 2, point3.y);\n ctx.lineTo(point4.x + pointSize / 2, point4.y);\n\n ctx.moveTo(point4.x, point4.y - pointSize / 2);\n ctx.lineTo(point1.x, point1.y + pointSize / 2);\n ctx.closePath();\n ctx.stroke();\n }\n },\n [lineColor, lineWidth, pointSize]\n );\n\n useEffect(() => {\n if (cropPoints && canvas.current) {\n clearCanvas();\n const sortedPoints = sortPoints();\n drawShape(sortedPoints);\n }\n }, [cropPoints, drawShape, clearCanvas, sortPoints]);\n\n /**\n * Takes in a `CoordinateXY` and makes sure that it is inside the `ContourCoordinates`\n *\n * @returns boolean\n *\n * @todo Should use point slope formula for when using perspective cropper\n */\n const xyInPoints = ({ x, y }: CoordinateXY) => {\n if (\n x > cropPoints[\"left-top\"].x &&\n y > cropPoints[\"left-top\"].y &&\n x < cropPoints[\"right-top\"].x &&\n y > cropPoints[\"right-top\"].y &&\n x < cropPoints[\"right-bottom\"].x &&\n y < cropPoints[\"right-bottom\"].y &&\n x > cropPoints[\"left-bottom\"].x &&\n y < cropPoints[\"left-bottom\"].y\n )\n return true;\n return false;\n };\n const getMousePosition = (\n event: React.MouseEvent<HTMLDivElement, MouseEvent>\n ) => {\n if (canvas?.current?.getBoundingClientRect) {\n const x = event.clientX - canvas.current.getBoundingClientRect().left;\n const y = event.clientY - canvas.current.getBoundingClientRect().top;\n if (xyInPoints({ x, y })) return \"inside\";\n else return \"outside\";\n }\n };\n const [cursor, setCursor] =\n useState<React.CSSProperties[\"cursor\"]>(\"default\");\n\n const handleMouseMove = (\n e: React.MouseEvent<HTMLDivElement, MouseEvent>\n ) => {\n const pos = getMousePosition(e);\n if (pos === \"inside\" && cursor !== \"crosshair\")\n setCursor(\"crosshair\");\n else if (pos === \"outside\" && cursor !== \"default\")\n setCursor(\"default\");\n };\n\n const handleClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {\n const pos = getMousePosition(e);\n if (pos === \"inside\")\n crop({\n preview: true,\n filterCvParams: {\n grayScale: false,\n th: false,\n },\n image: {\n quality: 1,\n type: \"image/jpeg\",\n },\n });\n };\n\n return (\n <>\n <canvas\n id={`${props.saltId ? props.saltId + \"-\" : \"\"}crop-point-delimiters`}\n ref={canvas}\n style={{\n position: \"absolute\",\n top: 0,\n right: 0,\n left: 0,\n bottom: 0,\n zIndex: 5,\n pointerEvents: \"none\",\n }}\n width={previewDims.width}\n height={previewDims.height}\n />\n <div\n style={{\n position: \"absolute\",\n top: 0,\n right: 0,\n left: 0,\n bottom: 0,\n zIndex: 4,\n cursor,\n }}\n onClick={handleClick}\n onMouseMove={handleMouseMove}\n onMouseLeave={() => setCursor(\"default\")}\n />\n </>\n );\n};\n\nexport { CropPointsDelimiters };\n\nCropPointsDelimiters.propTypes = {\n previewDims: T.shape({\n ratio: T.number,\n width: T.number,\n height: T.number,\n }),\n cropPoints: T.shape({\n \"left-top\": T.shape({ x: T.number, y: T.number }).isRequired,\n \"right-top\": T.shape({ x: T.number, y: T.number }).isRequired,\n \"right-bottom\": T.shape({ x: T.number, y: T.number }).isRequired,\n \"left-bottom\": T.shape({ x: T.number, y: T.number }).isRequired,\n }),\n lineColor: T.string,\n lineWidth: T.number,\n pointSize: T.number,\n saltId: T.string,\n};\n","import React, { useCallback, useEffect, useState } from \"react\";\nimport { useRomaine } from \"../../hooks\";\n\nimport {\n CalculatedDimensions,\n // readFile,\n // isCrossOriginURL,\n // applyFilter,\n warpPerspective,\n cropOpenCV,\n} from \"../../util\";\n\nimport { CropPoints } from \"./CropPoints\";\nimport { CropPointsDelimiters } from \"./CropPointsDelimiters\";\nimport { ContourCoordinates, CoordinateXY } from \".\";\nimport { CropFunc } from \"../Romaine.types\";\nimport { CanvasProps } from \"../Canvas\";\nimport { ImagePtr, ShowPreview, size } from \"../../types\";\n\n// const imageDimensions = { width: 0, height: 0 };\n\nexport interface CropperState extends ContourCoordinates {\n loading: boolean;\n}\n\nexport interface CropperSpecificProps\n extends Pick<\n CanvasProps,\n | \"lineColor\"\n | \"lineWidth\"\n | \"saltId\"\n | \"pointSize\"\n | \"image\"\n | \"onDragStop\"\n | \"onChange\"\n > {\n /** The canvas which we pass to OpenCV for manipulations */\n canvasRef: React.RefObject<HTMLCanvasElement | undefined>;\n /** The canvas we display images on */\n previewCanvasRef: React.RefObject<HTMLCanvasElement | null>;\n /** The canvas we draw the lines connecting the crop points on */\n previewDims: CalculatedDimensions | undefined;\n imageResizeRatio: number;\n showPreview: ShowPreview;\n setPreviewPaneDimensions: (dims?: size) => undefined | number;\n setCropFunc: React.Dispatch<React.SetStateAction<CropFunc | null>>;\n canvasPtr: React.RefObject<ImagePtr>;\n}\n\nexport const CroppingCanvas = ({\n image,\n onDragStop,\n onChange,\n previewCanvasRef,\n setCropFunc,\n pointSize = 30,\n lineWidth,\n lineColor,\n previewDims,\n imageResizeRatio,\n showPreview,\n setPreviewPaneDimensions,\n saltId,\n canvasPtr,\n}: CropperSpecificProps) => {\n const {\n loaded: cvLoaded,\n cv,\n romaine: { mode, cropPoints },\n setMode,\n pushHistory,\n setCropPoints,\n } = useRomaine();\n // const [_worker, setWorker] = useState<Worker | null>();\n // useEffect(() => {\n // if (window.Worker) {\n // const worker = new Worker(\"./test.js\");\n // worker.onmessage = function (e) {\n // console.log(e.data);\n // console.log(\"Message received from worker\");\n // };\n // worker.postMessage([20, 30]);\n // setWorker(worker);\n // }\n // }, []);\n\n const [loading, setLoading] = useState(false);\n\n const cropCB: CropFunc = useCallback(\n async (opts = {}) => {\n // need to figure out how to not need this and still render\n setLoading(true);\n // push can be moved to mode\n pushHistory?.();\n return new Promise<void>((resolve) => {\n if (!opts.cropPoints) opts.cropPoints = cropPoints;\n if (!opts.imageResizeRatio) opts.imageResizeRatio = imageResizeRatio;\n if (!opts.mode) opts.mode = mode;\n\n if (opts.cropPoints) {\n if (opts.mode === \"crop\")\n cropOpenCV(\n cv,\n canvasPtr.current,\n opts.cropPoints,\n opts.imageResizeRatio\n // canvasRef.current,\n );\n else if (opts.mode === \"perspective-crop\")\n warpPerspective(\n cv,\n canvasPtr.current,\n opts.cropPoints,\n opts.imageResizeRatio\n );\n else return setLoading(false);\n\n // @todo implement this somewhere\n // applyFilter(cv, canvasRef.current, opts.filterCvParams);\n if (opts.preview) {\n const dims = canvasPtr.current.size();\n const irr = setPreviewPaneDimensions(dims);\n showPreview(irr, canvasPtr.current, false);\n setMode?.(null);\n }\n setLoading(false);\n return resolve();\n }\n });\n },\n // disabling because eslint doesn't know that canvasRef is a ref\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n cv,\n cropPoints,\n mode,\n pushHistory,\n setLoading,\n setMode,\n setPreviewPaneDimensions,\n imageResizeRatio,\n ]\n );\n useEffect(() => {\n setCropFunc(() => cropCB);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [cropCB]);\n\n const detectContours = () => {\n /*\n * warning, this function w