diff --git a/package.json b/package.json index 7f8b73de2..6b1b8520c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ ], "dependencies": { "@excalidraw/random-username": "1.0.0", + "@imgly/background-removal": "1.5.3", "@sentry/browser": "6.2.5", "@sentry/integrations": "6.2.5", "firebase": "8.3.3", diff --git a/packages/excalidraw/actions/actionRemoveBackground.tsx b/packages/excalidraw/actions/actionRemoveBackground.tsx new file mode 100644 index 000000000..b0099bdfe --- /dev/null +++ b/packages/excalidraw/actions/actionRemoveBackground.tsx @@ -0,0 +1,113 @@ +import { generateIdFromFile, getDataURL } from "../data/blob"; +import { mutateElement } from "../element/mutateElement"; +import { isInitializedImageElement } from "../element/typeChecks"; +import type { InitializedExcalidrawImageElement } from "../element/types"; +import type { BinaryFileData } from "../types"; +import { register } from "./register"; + +export const actionRemoveBackground = register({ + name: "removeBackground", + label: "stats.fullTitle", + trackEvent: false, + viewMode: false, + async perform(elements, appState, _, app) { + const selectedElements = app.scene.getSelectedElements(appState); + + if ( + selectedElements.length > 0 && + selectedElements.every(isInitializedImageElement) + ) { + const filesToProcess = selectedElements.reduce( + ( + acc: Map< + BinaryFileData["id"], + { + file: BinaryFileData; + elements: InitializedExcalidrawImageElement[]; + } + >, + imageElement, + ) => { + const file = app.files[imageElement.fileId]; + + if (file) { + const fileWithRemovedBackground = Object.values(app.files).find( + (_file) => + _file.customData?.source === "backgroundRemoval" && + _file.customData.parentFileId === file.id, + ); + + if (fileWithRemovedBackground) { + mutateElement( + imageElement, + { fileId: fileWithRemovedBackground.id }, + false, + ); + } else if (acc.has(file.id)) { + acc.get(file.id)!.elements.push(imageElement); + } else { + acc.set(file.id, { file, elements: [imageElement] }); + } + } + return acc; + }, + new Map(), + ); + + if (filesToProcess.size) { + const backgroundRemoval = await await import( + "@imgly/background-removal" + ); + + console.time("removeBackground"); + + for (const [, { file, elements }] of filesToProcess) { + const res = await backgroundRemoval.removeBackground(file.dataURL, { + // debug: true, + progress: (...args) => { + console.log("progress", args); + }, + device: "gpu", + proxyToWorker: true, + }); + + const fileId = await generateIdFromFile(res); + const dataURL = await getDataURL(res); + + for (const imageElement of elements) { + mutateElement(imageElement, { fileId }, false); + } + + app.addFiles([ + { + ...file, + id: fileId, + dataURL, + customData: { + source: "backgroundRemoval", + version: 1, + parentFileId: file.id, + }, + }, + ]); + } + + console.timeEnd("removeBackground"); + } + + app.scene.triggerUpdate(); + } + return false as false; + }, + PanelComponent: ({ updateData }) => { + return ( + + ); + }, +}); diff --git a/packages/excalidraw/actions/index.ts b/packages/excalidraw/actions/index.ts index 092060425..13bdf92b2 100644 --- a/packages/excalidraw/actions/index.ts +++ b/packages/excalidraw/actions/index.ts @@ -86,3 +86,4 @@ export { actionUnbindText, actionBindText } from "./actionBoundText"; export { actionLink } from "./actionLink"; export { actionToggleElementLock } from "./actionElementLock"; export { actionToggleLinearEditor } from "./actionLinearEditor"; +export { actionRemoveBackground } from "./actionRemoveBackground"; diff --git a/packages/excalidraw/actions/types.ts b/packages/excalidraw/actions/types.ts index 6597ec0f0..e73c9cac2 100644 --- a/packages/excalidraw/actions/types.ts +++ b/packages/excalidraw/actions/types.ts @@ -136,7 +136,8 @@ export type ActionName = | "wrapTextInContainer" | "commandPalette" | "autoResize" - | "elementStats"; + | "elementStats" + | "removeBackground"; export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index c49b4a5f0..0420bc05f 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -25,6 +25,7 @@ import { hasStrokeColor } from "../scene/comparisons"; import { trackEvent } from "../analytics"; import { hasBoundTextElement, + isInitializedImageElement, isLinearElement, isTextElement, } from "../element/typeChecks"; @@ -125,6 +126,10 @@ export const SelectedShapeActions = ({ return (
+ {targetElements.length > 0 && + targetElements.every(isInitializedImageElement) && ( +
{renderAction("removeBackground")}
+ )}
{canChangeStrokeColor(appState, targetElements) && renderAction("changeStrokeColor")} diff --git a/packages/excalidraw/data/blob.ts b/packages/excalidraw/data/blob.ts index 1eb1e1bed..bd46c1adc 100644 --- a/packages/excalidraw/data/blob.ts +++ b/packages/excalidraw/data/blob.ts @@ -235,7 +235,9 @@ export const canvasToBlob = async ( /** generates SHA-1 digest from supplied file (if not supported, falls back to a 40-char base64 random id) */ -export const generateIdFromFile = async (file: File): Promise => { +export const generateIdFromFile = async ( + file: File | Blob, +): Promise => { try { const hashBuffer = await window.crypto.subtle.digest( "SHA-1", diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index cb428f695..fe0c09e93 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -108,6 +108,7 @@ export type BinaryFileData = { * Epoch timestamp in milliseconds. */ lastRetrieved?: number; + customData?: Record; }; export type BinaryFileMetadata = Omit; diff --git a/yarn.lock b/yarn.lock index b9124e646..46807c497 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2238,6 +2238,19 @@ resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@imgly/background-removal@1.5.3": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@imgly/background-removal/-/background-removal-1.5.3.tgz#5fb39eb97e0f26fefd6a270b18f771c1c905593d" + integrity sha512-Q5DI5EtOvTvsWueimB0XUlkDObdcQsN2hTEsQUnJXym7x7oH8dn1qrOZ6UklJSIHK7hkiqKtaDONvvk0lKVWmA== + dependencies: + "@types/lodash-es" "^4.17.12" + "@types/ndarray" "~1.0.14" + "@types/node" "~20.3.0" + lodash-es "^4.17.21" + ndarray "~1.0.0" + onnxruntime-web "~1.18.0" + zod "^3.23.8" + "@istanbuljs/schema@^0.1.2": version "0.1.3" resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" @@ -3193,6 +3206,13 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash-es@^4.17.12": + version "4.17.12" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b" + integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ== + dependencies: + "@types/lodash" "*" + "@types/lodash.throttle@4.1.7": version "4.1.7" resolved "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.7.tgz#4ef379eb4f778068022310ef166625f420b6ba58" @@ -3222,6 +3242,11 @@ resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== +"@types/ndarray@~1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23" + integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg== + "@types/node@*", "@types/node@>=13.7.0", "@types/node@^20": version "20.12.4" resolved "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz#af5921bd75ccdf3a3d8b3fa75bf3d3359268cd11" @@ -3229,6 +3254,11 @@ dependencies: undici-types "~5.26.4" +"@types/node@~20.3.0": + version "20.3.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6" + integrity sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw== + "@types/pako@1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@types/pako/-/pako-1.0.3.tgz#2e61c2b02020b5f44e2e5e946dfac74f4ec33c58" @@ -6406,6 +6436,11 @@ flat@^5.0.2: resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== +flatbuffers@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-1.12.0.tgz#72e87d1726cb1b216e839ef02658aa87dcef68aa" + integrity sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ== + flatted@^3.2.7, flatted@^3.2.9: version "3.3.1" resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" @@ -6659,6 +6694,11 @@ graphemer@^1.4.0: resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +guid-typescript@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc" + integrity sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ== + gzip-size@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" @@ -6967,6 +7007,11 @@ invariant@^2.2.2, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +iota-array@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/iota-array/-/iota-array-1.0.0.tgz#81ef57fe5d05814cd58c2483632a99c30a0e8087" + integrity sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA== + is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" @@ -7017,6 +7062,11 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-buffer@^1.0.2: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -7713,7 +7763,7 @@ long@^4.0.0: resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== -long@^5.0.0: +long@^5.0.0, long@^5.2.3: version "5.2.3" resolved "https://registry.npmjs.org/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== @@ -8213,6 +8263,14 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +ndarray@~1.0.0: + version "1.0.19" + resolved "https://registry.yarnpkg.com/ndarray/-/ndarray-1.0.19.tgz#6785b5f5dfa58b83e31ae5b2a058cfd1ab3f694e" + integrity sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ== + dependencies: + iota-array "^1.0.0" + is-buffer "^1.0.2" + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -8415,6 +8473,23 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +onnxruntime-common@1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.18.0.tgz#b904dc6ff134e7f21a3eab702fac17538f59e116" + integrity sha512-lufrSzX6QdKrktAELG5x5VkBpapbCeS3dQwrXbN0eD9rHvU0yAWl7Ztju9FvgAKWvwd/teEKJNj3OwM6eTZh3Q== + +onnxruntime-web@~1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/onnxruntime-web/-/onnxruntime-web-1.18.0.tgz#cd46268d9472f89697da0a3282f13129f0acbfa0" + integrity sha512-o1UKj4ABIj1gmG7ae0RKJ3/GT+3yoF0RRpfDfeoe0huzRW4FDRLfbkDETmdFAvnJEXuYDE0YT+hhkia0352StQ== + dependencies: + flatbuffers "^1.12.0" + guid-typescript "^1.0.9" + long "^5.2.3" + onnxruntime-common "1.18.0" + platform "^1.3.6" + protobufjs "^7.2.4" + open-color@1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/open-color/-/open-color-1.9.1.tgz#a6e6328f60eff7aa60e3e8fcfa50f53ff3eece35" @@ -8627,6 +8702,11 @@ pkg-types@^1.0.3: mlly "^1.2.0" pathe "^1.1.0" +platform@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" + integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== + png-chunk-text@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/png-chunk-text/-/png-chunk-text-1.0.0.tgz#1c6006d8e34ba471d38e1c9c54b3f53e1085e18f" @@ -11091,6 +11171,11 @@ yocto-queue@^1.0.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== +zod@^3.23.8: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== + zustand@^4.3.2: version "4.5.2" resolved "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz#fddbe7cac1e71d45413b3682cdb47b48034c3848"