UNPKG

romaine

Version:

React OpenCV Manipulation and Image Narration & Editing

1 lines 128 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/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/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 type { RomaineState, HistoryAction, RomaineHistory } from \".\";\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 = () => 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 return {\n ...state,\n history: {\n ...state.history,\n commands: [...newCommands, getHistoryFromState(state)],\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 console.warn(\"need to add crop points to history\");\n return { cmd: mode, payload: cropPoints };\n case \"perspective-crop\":\n console.warn(\"need to add crop points to history\");\n return { cmd: mode, payload: cropPoints };\n case \"scale\":\n return { cmd: mode, payload: scale };\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 // handles fall through mode\n throw new Error(\n `error: action of type ${mode} is not defined in switch \\`history\\`.\\`PUSH\\``\n ).stack;\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\";\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\" };\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 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\";\ndeclare global {\n interface Window {\n cv: OpenCV;\n Module: typeof moduleConfig;\n }\n}\nexport interface RomaineContext {\n loaded: boolean;\n cv: OpenCV;\n romaine: RomaineState & {\n clearHistory: ClearHistory;\n };\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\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(() => {\n dispatchRomaine({ type: \"HISTORY\", payload: { cmd: \"PUSH\" } });\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 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\";\n\nexport const useRomaine = () => useContext(OpenCvContext);\n","import React, { useMemo, useCallback, CSSProperties } from \"react\";\nimport type { FC } 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: \"move\",\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}\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}) => {\n const {\n romaine: { cropPoints },\n } = useRomaine();\n const cropPointStyle = useMemo(() => {\n if (\n cropPointStyles.width !== pointSize ||\n cropPointStyles.height !== pointSize\n ) {\n console.warn(\"\");\n }\n\n cropPointStyles.width = pointSize;\n cropPointStyles.height = pointSize;\n\n return buildCropPointStyle(cropPointStyles);\n }, [cropPointStyles, pointSize]);\n\n const onDrag: DraggableEventHandler = useCallback(\n (_, position) => {\n externalOnDrag(\n {\n ...position,\n x: position.x + pointSize / 2,\n y: position.y + pointSize / 2,\n },\n pointArea\n );\n },\n [externalOnDrag]\n );\n\n const onStop: DraggableEventHandler = useCallback(\n (_, position) => {\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 [externalOnDrag, cropPoints]\n );\n\n return (\n <Draggable\n bounds={bounds}\n defaultPosition={defaultPosition}\n position={{\n x: cropPoints[pointArea].x - pointSize / 2,\n y: cropPoints[pointArea].y - pointSize / 2,\n }}\n onDrag={onDrag}\n onStop={onStop}\n >\n <div style={cropPointStyle} />\n </Draggable>\n );\n};\n","import React from \"react\";\nimport 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]) => {\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 getCursorPosition = (\n event: React.MouseEvent<HTMLCanvasElement, 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<HTMLCanvasElement, MouseEvent>\n ) => {\n const cursorPosition = getCursorPosition(e);\n if (cursorPosition === \"inside\" && cursor !== \"crosshair\")\n setCursor(\"crosshair\");\n else if (cursorPosition === \"outside\" && cursor !== \"default\")\n setCursor(\"default\");\n };\n\n const handleClick = (e: React.MouseEvent<HTMLCanvasElement, MouseEvent>) => {\n const cursorPosition = getCursorPosition(e);\n if (cursorPosition === \"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 <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 cursor,\n }}\n width={previewDims.width}\n height={previewDims.height}\n onClick={handleClick}\n onMouseMove={handleMouseMove}\n onMouseLeave={() => setCursor(\"default\")}\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.MutableRefObject<HTMLCanvasElement | undefined>;\n /** The canvas we display images on */\n previewCanvasRef: React.RefObject<HTMLCanvasElement>;\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.MutableRefObject<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 will alter the canvasPtr\n *\n * Remember to copy pointer to new variable if you want to keep it\n * Make sure to clean up variable also\n */\n const dst = new cv.Mat();\n canvasPtr.current.copyTo(dst);\n const ksize = new cv.Size(5, 5);\n // convert the image to grayscale, blur it, and find edges in the image\n cv.cvtColor(dst, dst, cv.COLOR_RGBA2GRAY, 0);\n cv.GaussianBlur(dst, dst, ksize, 0, 0, cv.BORDER_DEFAULT);\n cv.Canny(dst, dst, 75, 200);\n // find contours\n cv.threshold(dst, dst, 120, 200, cv.THRESH_BINARY);\n const contours = new cv.MatVector();\n const hierarchy = new cv.Mat();\n cv.findContours(\n dst,\n contours,\n hierarchy,\n cv.RETR_CCOMP,\n cv.CHAIN_APPROX_SIMPLE\n );\n const rect = cv.boundingRect(dst);\n dst.delete();\n hierarchy.delete();\n contours.delete();\n // transform the rectangle into a set of points\n Object.keys(rect).forEach((key) => {\n rect[key] = rect[key] * imageResizeRatio;\n });\n\n const contourCoordinates: ContourCoordinates = {\n \"left-top\": { x: rect.x, y: rect.y },\n \"right-top\": { x: rect.x + rect.width, y: rect.y },\n \"right-bottom\": {\n x: rect.x + rect.width,\n y: rect.y + rect.height,\n },\n \"left-bottom\": { x: rect.x, y: rect.y + rect.height },\n };\n\n setCropPoints(contourCoordinates);\n };\n\n useEffect(() => {\n if (onChange && cropPoints) {\n onChange({ ...cropPoints, loading });\n }\n }, [cropPoints, loading]);\n /**\n * @todo\n * 1) Need to make sure that this is valid to never run the other code\n * if it is, then need to remove the code in the else block\n */\n const bootstrap = async () => {\n detectContours();\n setLoading(false);\n // else {\n // // imread is SLOW\n // const src = cv.imread(previewCanvasRef.current);\n // const contourCoordinates = {\n // \"left-top\": { x: 0, y: 0 },\n // \"right-top\": { x: src.cols, y: 0 },\n // \"right-bottom\": {\n // x: src.cols,\n // y: src.rows,\n // },\n // \"left-bottom\": { x: 0, y: src.rows },\n // };\n\n // setCropPoints(contourCoordinates);\n // }\n };\n\n useEffect(() => {\n if (\n image &&\n previewCanvasRef.current &&\n cvLoaded &&\n (mode === \"crop\" || mode === \"perspective-crop\")\n ) {\n bootstrap();\n } else {\n setLoading(true);\n }\n }, [image, cvLoaded, mode]);\n\n const handleNormalCornerMove = useCallback(\n (\n position: CoordinateXY,\n area: keyof ContourCoordinates,\n cPs: ContourCoordinates | undefined\n ): ContourCoordinates | undefined => {\n const { x, y } = position;\n if (cPs) {\n switch (area) {\n case \"left-bottom\":\n return {\n ...cPs,\n [area]: { x, y },\n \"left-top\": { x, y: cPs[\"left-top\"].y },\n \"right-bottom\": { x: cPs[\"right-bottom\"].x, y },\n };\n case \"right-bottom\":\n return {\n ...cPs,\n [area]: { x, y },\n \"right-top\": { x, y: cPs[\"right-top\"].y },\n \"left-bottom\": { x: cPs[\"left-bottom\"].x, y },\n };\n case \"right-top\":\n return {\n ...cPs,\n [area]: { x, y },\n \"left-top\": { x: cPs[\"left-top\"].x, y },\n \"right-bottom\": { x, y: cPs[\"right-bottom\"].y },\n };\n case \"left-top\":\n return {\n ...cPs,\n [area]: { x, y },\n \"left-bottom\": { x, y: cPs[\"left-bottom\"].y },\n \"right-top\": { x: cPs[\"right-top\"].x, y },\n };\n }\n }\n },\n []\n );\n\n const onCornerDrag = useCallback(\n (position: CoordinateXY, area: keyof ContourCoordinates) => {\n // if (magnifierCanvasRef.current && previewCanvasRef.current) {\n // const { x, y } = position;\n // const magnCtx = magnifierCanvasRef.current.getContext(\"2d\");\n // clearCanvasByRef(magnifierCanvasRef);\n // TODO we should make those 5, 10 and 20 values proportionate\n // to the point size\n // magnCtx &&\n // magnCtx.drawImage(\n // previewCanvasRef.current,\n // x - (pointSize - 10),\n // y - (pointSize - 10),\n // pointSize + 5,\n // pointSize + 5,\n // x + 10,\n // y - 90,\n // pointSize + 20,\n // pointSize + 20\n // );\n setCropPoints((cPs) => {\n if (cPs && mode === \"perspective-crop\")\n return { ...cPs, [area]: position };\n return handleNormalCornerMove(position, area, cPs);\n });\n // }\n },\n [mode, setCropPoints, handleNormalCornerMove]\n );\n\n const onCornerStop = useCallback(\n (\n position: CoordinateXY,\n area: keyof ContourCoordinates,\n cropPoints: ContourCoordinates\n ) => {\n const { x, y } = position;\n\n // clearCanvasByRef(magnifierCanvasRef);\n setCropPoints((cPs) => {\n if (cPs && mode === \"perspective-crop\")\n return { ...cPs, [area]: { x, y } as CoordinateXY };\n return handleNormalCornerMove(position, area, cPs);\n });\n onDragStop?.({ ...cropPoints, [area]: { x, y }, loading });\n },\n [mode, setCropPoints, handleNormalCornerMove]\n );\n\n return (\n <>\n {previewDims &&\n (mode === \"crop\" || mode === \"perspective-crop\") &&\n cropPoints && (\n <>\n <CropPoints\n pointSize={pointSize}\n previewDims={previewDims}\n onDrag={onCornerDrag}\n onStop={onCornerStop}\n bounds={{\n left:\n (previewCanvasRef?.current?.offsetLeft || 0) - pointSize / 2,\n top:\n (previewCanvasRef?.current?.offsetTop || 0) - pointSize / 2,\n right:\n (previewCanvasRef?.current?.offsetLeft || 0) -\n pointSize / 2 +\n (previewCanvasRef?.current?.offsetWidth || 0),\n bottom:\n (previewCanvasRef?.current?.offsetTop || 0) -\n pointSize / 2 +\n (previewCanvasRef?.current?.offsetHeight || 0),\n }}\n />\n <CropPointsDelimiters\n // romaineRef={romaineRef as React.RefObject<RomaineRef>}\n crop={cropCB}\n previewDims={previewDims}\n cropPoints={cropPoints}\n lineWidth={lineWidth}\n lineColor={lineColor}\n pointSize={pointSize}\n saltId={saltId}\n />\n </>\n )}\n </>\n );\n};\n","import { CalculatedDimensions } from \".\";\n\nexport const buildImgContainerStyle = (previewDims: CalculatedDimensions) => ({\n width: previewDims.width,\n height: previewDims.height,\n});\n","import { MutableRefObject, useCallback, useRef, useState } from \"react\";\nimport { useRomaine } from \"../../hooks\";\nimport { SetPreviewPaneDimensions, ShowPreview } from \"../../types\";\nimport { calcDims, CalculatedDimensions } from \"../../util\";\ninterface Props {\n canvasRef: MutableRefObject<HTMLCanvasElement | undefined>;\n maxDims: {\n maxHeight: number;\n maxWidth: number;\n };\n originalImageDims: {\n width: number;\n height: number;\n };\n}\n\nexport const usePreview = ({\n canvasRef,\n maxDims,\n originalImageDims,\n}: Props) => {\n const { maxHeight, maxWidth } = maxDims;\n const { cv } = useRomaine();\n\n const previewRef = useRef<HTMLCanvasElement>(null);\n const [imageResizeRatio, setImageResizeRatio] = useState(1);\n const [previewDims, setPreviewDims] = useState<CalculatedDimensions>({\n height: maxHeight,\n width: maxWidth,\n ratio: 1,\n });\n const createPreview: ShowPreview = (\n resizeRatio = imageResizeRatio,\n source,\n cleanup = true\n ) => {\n if (!source && canvasRef.current) source == cv.imread(canvasRef.current);\n if (!source?.$$.ptr) return;\n if (cv && previewRef.current) {\n const dst = new cv.Mat();\n const dsize = new cv.Size(0, 0);\n cv.resize(source, dst, dsize, resizeRatio, resizeRatio, cv.INTER_AREA);\n cv.imshow(previewRef.current, dst);\n if (cleanup) source.delete();\n dst.delete();\n }\n };\n const setPreviewPaneDimensions: SetPreviewPaneDimensions = useCallback(\n (dims = originalImageDims) => {\n if (dims && previewRef?.current) {\n const newPreviewDims = calcDims(\n dims.width,\n dims.height,\n maxWidth,\n maxHeight\n );\n setPreviewDims(newPreviewDims);\n\n previewRef.current.width = newPreviewDims.width;\n previewRef.current.height = newPreviewDims.height;\n const irr = newPreviewDims.width / dims.width;\n\n setImageResizeRatio(irr);\n return irr;\n }\n },\n [maxHeight, maxWidth]\n );\n return {\n previewRef,\n createPreview,\n imageResizeRatio,\n setPreviewPaneDimensions,\n previewDims,\n };\n};\n\nexport type UsePreviewReturnType = ReturnType<typeof usePreview>;\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useRomaine } from \"../../hooks\";\nimport { ImagePtr } from \"../../types\";\nimport { isCrossOriginURL, readFile } from \"../../util\";\n\ninterface Props {\n image: string | File | null;\n saltId?: string;\n}\n\nexport const useCanvas = ({ image, saltId }: Props) => {\n const {\n cv,\n romaine: { mode },\n updateImageInformation,\n } = useRomaine();\n const [loaded, setLoaded] = useState(false);\n\n const [originalImageDims, setOriginalImageDims] = useState({\n width: 0,\n height: 0,\n });\n const canvasRef = useRef<HTMLCanvasElement>(document.createElement(\"canvas\"));\n const canvasPtr = useRef<ImagePtr | undefined>();\n const getFile = useCallback(async (image) => await readFile(image), []);\n\n const createCanvas = useCallback(\n () =>\n new Promise<void>(async (resolve, reject) => {\n if (!image) return reject(\"Image source is invalid\");\n const src = await getFile(image);\n try {\n const img = document.createElement(\"img\");\n img.style.display = \"none\";\n img.id = `${saltId ? saltId + \"-\" : \"\"}working-image`;\n img.onload = async () => {\n // set edited image canvas and dimensions\n canvasRef.current = document.createElement(\"canvas\");\n canvasRef.current.style.display = \"none\";\n canvasRef.current.id = `${\n saltId ? saltId + \"-\" : \"\"\n }working-canvas`;\n canvasRef.current.width = img.width;\n canvasRef.current.height = img.height;\n setOriginalImageDims({ height: img.height, width: img.width });\n updateImageInformation?.({\n height: img.height,\n width: img.width,\n id: img.id,\n });\n const ctx = canvasRef.current.getContext(\"2d\");\n if (ctx) {\n ctx.fillStyle = \"#fff0\"; // transparent\n ctx.fillRect(0, 0, img.width, img.height);\n ctx.drawImage(img, 0, 0);\n\n canvasPtr.current = cv.imread(canvasRef.current);\n setLoaded(true);\n return resolve();\n }\n return reject();\n };\n if (isCrossOriginURL(src)) img.crossOrigin = \"anonymous\";\n img.src = src;\n } catch (err) {\n console.error(\"Error in create canvas: \", err);\n reject(\"unknown error while creating canvas\");\n }\n }),\n [image]\n );\n\n const resetImage = useCallback(() => {\n // set loaded to false before we destroy the pointer to avoid race conditions\n setLoaded(false);\n canvasPtr.current?.delete();\n createCanvas();\n }, [image]);\n\n useEffect(() => {\n if (image) resetImage();\n }, [image]);\n\n useEffect(() => {\n if (mode === \"undo\") {\n resetImage();\n }\n }, [mode]);\n\n return {\n canvasRef,\n createCanvas,\n originalImageDims,\n canvasPtr,\n resetImage,\n /** loaded: Is the image loaded onto the canvas and does the pointer exist */\n loaded,\n };\n};\nexport type UseCanvasReturnType = ReturnType<typeof useCanvas>;\n","import { ImagePtr, OpenCV } from \"../../types\";\n\nexport const flip = async (\n cv: OpenCV,\n canvas: HTMLCanvasElement,\n src: ImagePtr,\n orientation: \"horizontal\" | \"vertical\"\n) => {\n if (orientation === \"horizontal\") {\n cv.flip(src,