├── .gitignore ├── README.md ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── code.ts ├── ignore.d.ts ├── ui.html ├── ui.scss └── ui.tsx ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Figma Remove.bg Plugin 2 | 3 | Remove background of images with just 1-click (Using https://www.remove.bg/). 4 | 5 | ![Preview](https://aaroniker.me/removebg.gif) 6 | 7 | ## Usage 8 | 9 | Download it on the Figma plugin library [figma.com/c/plugin/738992712906748191/Remove-BG](https://www.figma.com/c/plugin/738992712906748191/Remove-BG) 10 | 11 | ## Development 12 | 13 | First clone this repository 14 | ```shell 15 | git clone https://github.com/aaroniker/figma-remove-bg.git 16 | cd figma-remove-bg 17 | ``` 18 | 19 | Install dependencies & build files 20 | ```shell 21 | npm install 22 | npm run build 23 | # Or watch: npm run dev 24 | ``` 25 | 26 | After that open a project in Figma Desktop, select _Plugins -> Development -> New Plugin_. Click `Choose a manifest.json` and find the `manifest.json` file in this plugin directory. 27 | 28 | Done! Now _Plugins -> Development -> Remove BG -> Run/Set API Key_ 29 | 30 | ## ToDo 31 | 32 | - [ ] Show statistics about available/used credits 33 | - [ ] More options, e.x. size 34 | - [ ] Support selecting multiple nodes 35 | 36 | ## Author 37 | 38 | - Aaron Iker ([Twitter](https://twitter.com/aaroniker_me)) 39 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Remove BG", 3 | "id": "738992712906748191", 4 | "api": "1.0.0", 5 | "main": "dist/code.js", 6 | "ui": "dist/ui.html", 7 | "editorType": ["figma"], 8 | "menu": [ 9 | { 10 | "name": "Run", 11 | "command": "removebgfunc" 12 | }, 13 | { 14 | "separator": true 15 | }, 16 | { 17 | "name": "Set API Key", 18 | "command": "removebgkey" 19 | } 20 | ], 21 | "networkAccess": { 22 | "allowedDomains": [ 23 | "https://remove.bg", 24 | "https://api.remove.bg", 25 | "https://api.remove.bg/v1.0/removebg" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-remove-bg", 3 | "version": "1.0.0", 4 | "description": "Remove the background of images automatically", 5 | "main": "code.js", 6 | "scripts": { 7 | "dev": "webpack --mode=development --watch", 8 | "build": "webpack" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/aaroniker/figma-remove-bg.git" 13 | }, 14 | "author": "Aaron Iker", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@figma/plugin-typings": "^1.39.0", 18 | "@types/node": "^16.11.8", 19 | "@types/react": "^17.0.35", 20 | "@types/react-dom": "^17.0.11", 21 | "css-loader": "^6.5.1", 22 | "html-webpack-plugin": "^5.5.0", 23 | "node-sass": "^6.0.1", 24 | "sass": "^1.43.4", 25 | "sass-loader": "^12.3.0", 26 | "style-loader": "^3.3.1", 27 | "ts-loader": "^9.2.6", 28 | "typescript": "^4.5.2", 29 | "url-loader": "^4.1.1", 30 | "webpack": "^5.64.1", 31 | "webpack-cli": "^4.9.1" 32 | }, 33 | "dependencies": { 34 | "react": "^17.0.2", 35 | "react-dev-utils": "^11.0.4", 36 | "react-dom": "^17.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/code.ts: -------------------------------------------------------------------------------- 1 | if (figma.command == "removebgfunc") { 2 | async function checkFill(fill, apiKey) { 3 | if (fill.type === "IMAGE") { 4 | figma.showUI(__html__, { visible: false }); 5 | 6 | const image = figma.getImageByHash(fill.imageHash); 7 | const bytes = await image.getBytesAsync(); 8 | 9 | figma.ui.postMessage({ 10 | type: "run", 11 | buffer: bytes.buffer, 12 | apikey: apiKey, 13 | }); 14 | 15 | const response: { uint8Array: Uint8Array; credits: string } = 16 | await new Promise((resolve, reject) => { 17 | figma.ui.onmessage = (res) => { 18 | if ( 19 | typeof res["errors"] !== "undefined" && 20 | Array.isArray(res["errors"]) && 21 | res["errors"].length > 0 22 | ) { 23 | figma.closePlugin(res["errors"][0].title); 24 | } else { 25 | resolve(res); 26 | } 27 | }; 28 | }); 29 | 30 | const newImageFill = JSON.parse(JSON.stringify(fill)); 31 | newImageFill.imageHash = figma.createImage(response.uint8Array).hash; 32 | 33 | return { 34 | fill: newImageFill, 35 | credits: response.credits, 36 | updated: true, 37 | }; 38 | } 39 | return { 40 | fill: fill, 41 | updated: false, 42 | }; 43 | } 44 | 45 | async function removeBG(node, apiKey) { 46 | let types = ["RECTANGLE", "ELLIPSE", "POLYGON", "STAR", "VECTOR", "TEXT"]; 47 | if (types.indexOf(node.type) > -1) { 48 | let newFills = [], 49 | updated = false, 50 | check; 51 | for (const fill of node.fills) { 52 | check = await checkFill(fill, apiKey); 53 | updated = check.updated || updated; 54 | newFills.push(check.fill); 55 | } 56 | node.fills = newFills; 57 | figma.closePlugin( 58 | updated 59 | ? `Image background removed${ 60 | typeof check.credits !== "undefined" && 61 | check.credits.toString().length > 0 62 | ? ` (${check.credits} ${ 63 | Number(check.credits) === 1 ? "credit" : "credits" 64 | } charged)` 65 | : "" 66 | }.` 67 | : "Nothing changed (No credits charged)." 68 | ); 69 | } else { 70 | figma.closePlugin("Select a node with image fill."); 71 | } 72 | } 73 | 74 | if (figma.currentPage.selection.length !== 1) { 75 | figma.closePlugin("Select a single node."); 76 | } 77 | 78 | figma.clientStorage.getAsync("removeBgApiKey").then((apiKey) => { 79 | if (apiKey) { 80 | removeBG(figma.currentPage.selection[0], apiKey); 81 | } else { 82 | figma.closePlugin("Set API Key first."); 83 | } 84 | }); 85 | } else if (figma.command == "removebgkey") { 86 | figma.clientStorage.getAsync("removeBgApiKey").then((apiKey) => { 87 | figma.showUI(__html__, { 88 | height: 220, 89 | width: 348, 90 | visible: true, 91 | themeColors: true, 92 | }); 93 | figma.ui.postMessage({ 94 | type: "key", 95 | apikey: apiKey, 96 | }); 97 | figma.ui.onmessage = (response) => { 98 | figma.clientStorage.setAsync("removeBgApiKey", response).then(() => { 99 | figma.closePlugin("API Key set."); 100 | }); 101 | }; 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /src/ignore.d.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | type ShowUIOptions = any; 3 | -------------------------------------------------------------------------------- /src/ui.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/ui.scss: -------------------------------------------------------------------------------- 1 | #setApiKey { 2 | font-family: Inter, sans-serif; 3 | font-weight: 400; 4 | font-size: 12px; 5 | line-height: 18px; 6 | padding: 8px; 7 | color: var(--figma-color-text); 8 | 9 | a { 10 | color: var(--figma-color-bg-brand); 11 | text-decoration: none; 12 | &:hover { 13 | text-decoration: underline; 14 | } 15 | } 16 | 17 | p { 18 | color: var(--figma-color-text-secondary); 19 | margin: 12px 0 0 0; 20 | } 21 | 22 | ol { 23 | margin: 0 0 12px 0; 24 | padding: 0 0 0 16px; 25 | 26 | li:not(:last-child) { 27 | margin-bottom: 8px; 28 | } 29 | } 30 | 31 | .row { 32 | display: grid; 33 | grid-gap: 8px; 34 | grid-template-columns: auto 76px; 35 | } 36 | 37 | input, 38 | button { 39 | -webkit-appearance: none; 40 | font-family: Inter, sans-serif; 41 | outline: none; 42 | border: none; 43 | font-size: 12px; 44 | line-height: 16px; 45 | padding: 7px 8px; 46 | margin: 0; 47 | border-radius: 5px; 48 | display: block; 49 | } 50 | 51 | input { 52 | position: relative; 53 | align-items: center; 54 | width: 100%; 55 | color: var(--figma-color-text); 56 | box-shadow: inset 0 0 0 1px var(--figma-color-border); 57 | transition: box-shadow 0.2s; 58 | background: none; 59 | 60 | &:hover { 61 | box-shadow: inset 0 0 0 1px var(--figma-color-border-strong); 62 | } 63 | 64 | &:focus { 65 | border-color: var(--figma-color-border-selected); 66 | } 67 | } 68 | 69 | button { 70 | color: #fff; 71 | cursor: pointer; 72 | background-color: var(--figma-color-bg-brand); 73 | font-weight: 500; 74 | text-align: center; 75 | transition: background-color 0.2s; 76 | 77 | &:hover { 78 | background-color: var(--figma-color-bg-brand-pressed); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/ui.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, useEffect, ChangeEvent, useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./ui.scss"; 4 | 5 | const getImageBinary = (bytes: ArrayBuffer): Promise => { 6 | return new Promise((resolve, reject) => { 7 | try { 8 | const blob = new Blob([new Uint8Array(bytes)], { type: "image/jpeg" }); 9 | const url = URL.createObjectURL(blob); 10 | 11 | const img = new Image(); 12 | img.onload = () => { 13 | URL.revokeObjectURL(url); 14 | resolve(blob); 15 | }; 16 | img.onerror = () => { 17 | URL.revokeObjectURL(url); 18 | reject(new Error("Invalid image")); 19 | }; 20 | 21 | img.src = url; 22 | } catch (e) { 23 | reject(e); 24 | } 25 | }); 26 | }; 27 | 28 | export const App: React.FC = () => { 29 | const [apiKey, setApiKey] = useState(""); 30 | 31 | const handleSubmit = (event: FormEvent) => { 32 | event.preventDefault(); 33 | 34 | parent.postMessage( 35 | { 36 | pluginMessage: apiKey, 37 | }, 38 | "*" 39 | ); 40 | }; 41 | 42 | useEffect(() => { 43 | window.onmessage = async (event) => { 44 | if (event.data.pluginMessage.type == "key") { 45 | setApiKey(event.data.pluginMessage.apikey || ""); 46 | } 47 | if (event.data.pluginMessage.type == "run") { 48 | try { 49 | const imageBinary = await getImageBinary( 50 | event.data.pluginMessage.buffer 51 | ); 52 | 53 | const formData = new FormData(); 54 | formData.append("size", "auto"); 55 | formData.append("image_file", imageBinary); 56 | 57 | let credits: string = ""; 58 | 59 | fetch("https://api.remove.bg/v1.0/removebg", { 60 | method: "POST", 61 | headers: { 62 | "X-Api-Key": event.data.pluginMessage.apikey, 63 | }, 64 | body: formData, 65 | }) 66 | .then((response) => { 67 | if (!response.ok) { 68 | throw response; 69 | } 70 | credits = response.headers.get("X-Credits-Charged"); 71 | return response.blob(); 72 | }) 73 | .then((blob) => { 74 | return blob.arrayBuffer(); 75 | }) 76 | .then((arrayBuffer) => { 77 | const uint8Array = new Uint8Array(arrayBuffer); 78 | parent.postMessage( 79 | { 80 | pluginMessage: { 81 | uint8Array, 82 | credits, 83 | }, 84 | }, 85 | "*" 86 | ); 87 | }) 88 | .catch((response) => { 89 | try { 90 | response.json().then((res) => { 91 | parent.postMessage( 92 | { 93 | pluginMessage: res, 94 | }, 95 | "*" 96 | ); 97 | }); 98 | } catch (e) { 99 | parent.postMessage( 100 | { 101 | pluginMessage: { 102 | type: "error", 103 | message: "Error, please DM me on 𝕏 @aaroniker_me", 104 | }, 105 | }, 106 | "*" 107 | ); 108 | } 109 | }); 110 | } catch (e) { 111 | console.log("Error", e); 112 | 113 | parent.postMessage( 114 | { 115 | pluginMessage: { 116 | type: "error", 117 | message: 118 | "Error in dev console, please DM me on 𝕏 @aaroniker_me", 119 | }, 120 | }, 121 | "*" 122 | ); 123 | } 124 | } 125 | }; 126 | }, []); 127 | 128 | return ( 129 |
130 |
    131 |
  1. 132 | Go to the{" "} 133 | 137 | remove.bg website 138 | {" "} 139 | and create a free account (you will need to confirm your email). 140 |
  2. 141 |
  3. 142 | After that you can find your API key here{" "} 143 | 144 | https://www.remove.bg/dashboard#api-key 145 | 146 | . 147 |
  4. 148 |
149 |
150 | ) => 154 | setApiKey(event.target.value) 155 | } 156 | value={apiKey} 157 | /> 158 | 159 |
160 |

161 | More information about free accounts & pricing{" "} 162 | 163 | here 164 | 165 | . 166 |

167 |

168 | Follow me on{" "} 169 | 170 | 𝕏 (@aaroniker_me) 171 | {" "} 172 | for updates & more. 173 |

174 |
175 | ); 176 | }; 177 | 178 | ReactDOM.render(, document.getElementById("react-page")); 179 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "jsx": "react", 5 | "noEmit": false, 6 | "allowSyntheticDefaultImports": true, 7 | "typeRoots": ["./node_modules/@types", "./node_modules/@figma"] 8 | }, 9 | "types": ["webpack-env"], 10 | "include": ["src/**/*.ts", "src/**/*.tsx"] 11 | } 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const InlineChunkHtmlPlugin = require("react-dev-utils/InlineChunkHtmlPlugin"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | const path = require("path"); 5 | const webpack = require("webpack"); 6 | 7 | module.exports = (__, argv) => ({ 8 | mode: argv.mode === "production" ? "production" : "development", 9 | 10 | devtool: argv.mode === "production" ? false : "inline-source-map", 11 | 12 | entry: { 13 | ui: "./src/ui.tsx", 14 | code: "./src/code.ts", 15 | }, 16 | 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | use: "ts-loader", 22 | exclude: /node_modules/, 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: ["style-loader", "css-loader"], 27 | }, 28 | { 29 | test: /\.svg/, 30 | type: "asset/inline", 31 | }, 32 | { 33 | test: /\.s[ac]ss$/i, 34 | use: ["style-loader", "css-loader", "sass-loader"], 35 | }, 36 | ], 37 | }, 38 | resolve: { extensions: [".tsx", ".ts", ".jsx", ".js", ".scss"] }, 39 | output: { 40 | filename: "[name].js", 41 | path: path.resolve(__dirname, "dist"), 42 | }, 43 | plugins: [ 44 | new webpack.DefinePlugin({ 45 | global: {}, 46 | }), 47 | new HtmlWebpackPlugin({ 48 | inject: "body", 49 | template: "./src/ui.html", 50 | filename: "ui.html", 51 | chunks: ["ui"], 52 | }), 53 | new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/ui/]), 54 | ], 55 | }); 56 | --------------------------------------------------------------------------------