Instrument x Seth Akkerman
Projects Downloading Original Images

Downloading Original Images

A Figma plugin that extracts the original full-resolution images embedded in frames — with 1x and 2x export options, inline renaming, and bulk download or direct Drive delivery

When designers work in Figma, images get embedded into frames — cropped, masked, layered over. The original files aren’t always easy to get back out. For motion designers, production teams, or anyone downstream who needs the actual source image at full resolution, that means digging through assets, finding the original, and hoping it’s the right version. This plugin skips all of that.

How it works

Select a frame or a set of frames, scan the selection, and the plugin surfaces every image it finds — including ones that aren’t immediately obvious. It handles the edge cases you run into in production work: a frame might have an image layer sitting on top of an image fill that’s technically still attached to the frame. The plugin detects both and exposes them separately, so nothing gets missed.

From there you have full control over what you do with them. Rename files inline before you export. Download at 1x (the original as-placed size) or 2x for retina. Grab individual files, select a subset, or export everything at once as a ZIP. You can also send directly to a Google Drive folder using the same Drive connection method as the other plugins in the suite — paste a folder URL and it goes straight there.

Why it exists

The ask came from motion designers who needed original images out of Figma files without quality loss or manual hunting. Figma’s native export gives you what the frame looks like — not necessarily the source image inside it. This plugin gives you the source. It doesn’t care how many layers are on top of it or how it’s been masked. It finds the image, gives you the options, and gets out of the way.

Applying this to your project

  1. What to copy vs. discard. Copy code.ts, code.js, ui.html, manifest.json, package.json, and tsconfig.json — that’s the whole plugin. Discard README.md as-is; it’s Figma’s generic plugin-scaffold boilerplate and doesn’t describe this project at all, so replace it with real instructions before sharing. PROJECT_CONTEXT.md is internal dev documentation, not something Figma loads — keep it only if you want a similar living design doc for your own fork.

  2. API keys / environment setup. This repo itself has no .env file and stores no secrets — all Google OAuth logic (client ID/secret, token exchange) lives in a separate serverless backend hosted at https://figma-to-drive.netlify.app (three functions: auth-start, token-refresh, drive-api). If you want the Google Drive upload feature, you must stand up your own equivalent backend (Netlify, Vercel, Cloudflare Workers, whatever) with your own Google Cloud OAuth client, then:

    • Update the NETLIFY_URL constant near the top of the <script> block in ui.html (around line 605) to point at your backend.
    • Update manifest.json’s networkAccess.allowedDomains to whitelist your backend’s domain (and drop figma-to-drive.netlify.app/the Google OAuth domains if you’re not using Drive at all).
    • If you don’t want Google Drive integration, you can strip it entirely: remove the .drive-section markup and related JS in ui.html, and remove the get-tokens / save-tokens / clear-tokens handlers in code.ts.
  3. What to swap to make it yours.

    • manifest.json: change name to your plugin’s name, and generate a new id (don’t reuse this one — Figma assigns these uniquely; copying it can cause collisions).
    • package.json: update name, description.
    • ui.html: swap the header text (“Select frames containing images, scan to find them…”) and any branding/copy in the status messages, plus the NETLIFY_URL constant as above.
    • The JSZip CDN version pin (3.10.1 in the <script src> tag) — bump it if you want a newer release, and update manifest.json’s allowed domain list if you switch CDNs.
  4. Critical integration points.

    • All communication between the plugin backend and UI goes through one router: figma.ui.onmessage in code.ts. New features are added as additional if (msg.type === '...') blocks there, paired with a matching parent.postMessage({ pluginMessage: { type: '...' } }, '*') call in ui.html. This is where you’d plug in new data or new export types.
    • findImageFills(node, images) in code.ts is the recursive walk that decides what counts as exportable content — right now it only matches nodes with an IMAGE fill. If you want to export something other than images (e.g. text content, component variants), this function is what you rewrite.
    • Byte data (thumbnails, original image bytes, scaled exports) is always transferred as base64 strings, never raw arrays — see uint8ArrayToBase64() in code.ts and base64ToUint8Array() in ui.html. Any new byte-transferring feature must follow this pattern.
  5. Non-obvious gotchas from the code.

    • manifest.json uses "documentAccess": "dynamic-page", which means you must use await figma.getNodeByIdAsync() — the synchronous figma.getNodeById() will throw. Easy to miss if you’re used to older Figma plugin examples.
    • Figma’s plugin UI runs in a sandboxed iframe that only allows one programmatic file download per user gesture. A loop that calls a.click() multiple times in a row will silently drop everything after the first. The workaround used here (see the “Download Individually” flow in ui.html) is to pre-fetch all files first, then show a button the user clicks once per file so each save has a fresh user gesture.
    • Do NOT convert image bytes to a number[] for postMessage transfer — it was tried here first and caused ~10x memory bloat and crashes on large images. Base64 strings are the fix; keep using them for any new byte payload.
    • The Figma sandbox has no btoa/atob — that’s why code.ts has a hand-rolled uint8ArrayToBase64() encoder, while ui.html (running in a real browser iframe) can use the native atob.
    • JSZip is loaded from a CDN <script> tag, not bundled via npm — if you add other CDN-hosted libraries, remember to add their domain to manifest.json’s networkAccess.allowedDomains or Figma will silently block the request.

Other ways to repurpose this

Product Design

Web Design

Marketing

Branding

Dependencies

jszip 3.10.1 Bundles all selected images into a single ZIP archive client-side for the "Download as ZIP" bulk-export button in ui.html; loaded from cdnjs rather than npm, so it must be whitelisted in manifest.json's networkAccess.allowedDomains.
typescript 5.9.3 Compiles code.ts (the plugin logic that runs in Figma's sandbox — image scanning, base64 encoding, export calls) into the code.js file that manifest.json actually loads.
@figma/plugin-typings 1.123.0 Supplies TypeScript type definitions for the Figma Plugin API (figma.currentPage, figma.getImageByHash, figma.getNodeByIdAsync, etc.) so code.ts type-checks against the real plugin sandbox surface.
Role
Creative Technologist
Tools
Figma Plugin API Google Drive API JavaScript
Metrics
Motion designers and others can instantly access original full-size images in bulk without manual extraction or quality loss
URL
https://www.figma.com/community/plugin/1601390922535711533/export-original-images
Video thumbnail for Downloading Original Images