-
Notifications
You must be signed in to change notification settings - Fork 131
refig - render images from .fig or cached figma rest-api response
#539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
b7f102c
Update README.md to clarify input sources for Figma rendering
softmarshmallow 82f42cf
repo: ts, tsx, turbo
softmarshmallow e9780bd
Implement headless Figma rendering with @grida/refig package
softmarshmallow c9ce1a1
Add Figma archiving tool and update .gitignore
softmarshmallow 51e9bc3
doc: refig images input model
softmarshmallow cbba6a3
Implement image registration with custom RID in Renderer API
softmarshmallow cb17860
wasm 0.90.0-canary.6
softmarshmallow 06fa6eb
Refactor image handling in Editor and Playground components
softmarshmallow 18c9765
Enhance image handling in Figma rendering
softmarshmallow e320d5a
fallback COMPONENT_SET to FRAME
softmarshmallow efdf495
add figma rest api community file fixture
softmarshmallow 3e46928
update with fixture tests
softmarshmallow 3af0e12
shebang config
softmarshmallow 735c839
Update dependencies and add smoke tests for refig CLI
softmarshmallow 4bbd9b3
Update fixture fig (784448220678228461) file with export settings inc…
softmarshmallow 6f2a429
Enhance Figma rendering capabilities
softmarshmallow c1d7c46
Add prepack and postpack scripts for npm publishing
softmarshmallow e230602
Enhance Figma archiving and documentation
softmarshmallow 4b25a59
update fixtures
softmarshmallow a9b2f2a
wasm 0.90.0-canary.7
softmarshmallow ba0e8a8
Enhance Figma font handling and update dependencies
softmarshmallow c88728d
optimize test
softmarshmallow faa985a
refig cli: --skip-default-fonts
softmarshmallow ece6188
bump @figma/rest-api-spec
softmarshmallow b3294b9
Add support for volatile vectorNetwork in Figma REST API
softmarshmallow 8b0a291
docs: refig
softmarshmallow a2d304f
refig 0.0.1
softmarshmallow 3719cd6
chore
softmarshmallow 4b1804e
chore
softmarshmallow cffdac8
wasm 0.90.0-canary.8
softmarshmallow 69762bd
add path node support and have refig prefer it.
softmarshmallow 29ea801
docs: add title
softmarshmallow a13257f
fix crates build
softmarshmallow a53566c
refig 0.0.2
softmarshmallow 11f17a3
Update refig to version 0.0.3 with enhanced output handling
softmarshmallow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| #!/usr/bin/env python3 | ||
| # https://gist.github.com/softmarshmallow/27ad65dfa5babc2c67b41740f1f05791 | ||
| """ | ||
| Archive a Figma file via REST API: document.json (with geometry) and images/*. | ||
|
|
||
| Stdlib only. Usage: | ||
|
|
||
| python .tools/figma_archive.py --filekey <key> --archive-dir <dir> [--x-figma-token <token>] | ||
| # or set FIGMA_TOKEN in the environment | ||
|
|
||
| Output layout: | ||
| <archive-dir>/document.json — GET /v1/files/:key?geometry=paths | ||
| <archive-dir>/images/<ref>.<ext> — from GET /v1/files/:key/images, then download each URL | ||
| """ | ||
|
|
||
| import argparse | ||
| import json | ||
| import os | ||
| import sys | ||
| from pathlib import Path | ||
| from urllib.error import HTTPError, URLError | ||
| from urllib.request import Request, urlopen | ||
|
|
||
| FIGMA_BASE = "https://api.figma.com" | ||
| ENV_TOKEN = "FIGMA_TOKEN" | ||
|
|
||
| # Content-Type -> file extension for image fills | ||
| MIME_EXT = { | ||
| "image/png": "png", | ||
| "image/jpeg": "jpeg", | ||
| "image/jpg": "jpg", | ||
| "image/webp": "webp", | ||
| "image/gif": "gif", | ||
| } | ||
|
|
||
|
|
||
| def get_token(flag_value: str | None) -> str: | ||
| token = flag_value or os.environ.get(ENV_TOKEN) | ||
| if not token or not token.strip(): | ||
| print(f"error: provide --x-figma-token or set {ENV_TOKEN}", file=sys.stderr) | ||
| sys.exit(1) | ||
| return token.strip() | ||
|
|
||
|
|
||
| def api_get(url: str, token: str) -> bytes: | ||
| req = Request(url, method="GET", headers={"X-Figma-Token": token}) | ||
| try: | ||
| with urlopen(req, timeout=60) as resp: | ||
| return resp.read() | ||
| except HTTPError as e: | ||
| print(f"error: HTTP {e.code} {e.reason}: {url}", file=sys.stderr) | ||
| if e.fp: | ||
| body = e.fp.read().decode("utf-8", errors="replace")[:500] | ||
| print(body, file=sys.stderr) | ||
| sys.exit(1) | ||
| except URLError as e: | ||
| print(f"error: request failed: {e.reason}", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| def fetch_document(file_key: str, token: str) -> dict: | ||
| url = f"{FIGMA_BASE}/v1/files/{file_key}?geometry=paths" | ||
| raw = api_get(url, token) | ||
| return json.loads(raw.decode("utf-8")) | ||
|
|
||
|
|
||
| def fetch_image_fills(file_key: str, token: str) -> dict[str, str]: | ||
| url = f"{FIGMA_BASE}/v1/files/{file_key}/images" | ||
| raw = api_get(url, token) | ||
| data = json.loads(raw.decode("utf-8")) | ||
| meta = data.get("meta") or {} | ||
| images = meta.get("images") or {} | ||
| return {k: v for k, v in images.items() if v} | ||
|
|
||
|
|
||
| def download_image(url: str) -> tuple[bytes, str]: | ||
| # Image URLs from the Images API are pre-signed; no token needed. | ||
| req = Request(url, method="GET") | ||
| with urlopen(req, timeout=120) as resp: | ||
| data = resp.read() | ||
| content_type = ( | ||
| (resp.headers.get("Content-Type") or "").split(";")[0].strip().lower() | ||
| ) | ||
| ext = MIME_EXT.get(content_type) or "png" | ||
| return data, ext | ||
|
|
||
|
|
||
| def main() -> None: | ||
| parser = argparse.ArgumentParser( | ||
| description="Archive a Figma file (document.json + images/) via REST API." | ||
| ) | ||
| parser.add_argument( | ||
| "--x-figma-token", | ||
| metavar="TOKEN", | ||
| default=None, | ||
| help=f"Figma personal access token (or set {ENV_TOKEN})", | ||
| ) | ||
| parser.add_argument("--filekey", required=True, help="Figma file key") | ||
| parser.add_argument( | ||
| "--archive-dir", | ||
| required=True, | ||
| type=Path, | ||
| help="Output directory (document.json and images/ will be written here)", | ||
| ) | ||
| args = parser.parse_args() | ||
|
|
||
| token = get_token(args.x_figma_token) | ||
| archive_dir = args.archive_dir.resolve() | ||
| file_key = args.filekey.strip() | ||
|
|
||
| archive_dir.mkdir(parents=True, exist_ok=True) | ||
| images_dir = archive_dir / "images" | ||
| images_dir.mkdir(exist_ok=True) | ||
|
|
||
| print("Fetching document (geometry=paths)...", flush=True) | ||
| doc = fetch_document(file_key, token) | ||
| document_path = archive_dir / "document.json" | ||
| with open(document_path, "w", encoding="utf-8") as f: | ||
| json.dump(doc, f, indent=2, ensure_ascii=False) | ||
| doc_size = document_path.stat().st_size | ||
| print(f"Wrote document.json ({doc_size:,} bytes)", flush=True) | ||
|
|
||
| print("Fetching image fills list...", flush=True) | ||
| ref_to_url = fetch_image_fills(file_key, token) | ||
| n_images = len(ref_to_url) | ||
| if not ref_to_url: | ||
| print("No image fills.", flush=True) | ||
| return | ||
|
|
||
| print(f"Downloading {n_images} image(s)...", flush=True) | ||
| total_bytes = 0 | ||
| ok = 0 | ||
| for i, (ref, url) in enumerate(ref_to_url.items(), 1): | ||
| # Ref can contain characters unsafe for filenames; sanitize to alphanumeric + underscore | ||
| safe_ref = "".join(c if c.isalnum() or c in "._-" else "_" for c in ref) | ||
| if not safe_ref: | ||
| safe_ref = f"ref_{i}" | ||
| try: | ||
| data, ext = download_image(url) | ||
| out_path = images_dir / f"{safe_ref}.{ext}" | ||
| out_path.write_bytes(data) | ||
| size_k = len(data) / 1024 | ||
| total_bytes += len(data) | ||
| ok += 1 | ||
| print(f" [{i}/{n_images}] {safe_ref}.{ext} ({size_k:.1f} KB)", flush=True) | ||
| except Exception as e: | ||
| print(f" [{i}/{n_images}] {safe_ref} — failed: {e}", file=sys.stderr) | ||
|
|
||
| total_mb = total_bytes / (1024 * 1024) | ||
| print( | ||
| f"Done. document.json ({doc_size:,} B) + {ok}/{n_images} images ({total_mb:.2f} MB) in {archive_dir}.", | ||
| flush=True, | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| --- | ||
| title: Introducing refig — a headless Figma renderer for real-world pipelines | ||
| description: Export Figma designs to PNG/SVG/PDF without a browser. Deterministic, CI-friendly rendering from .fig or REST JSON—built for tooling, previews, and automation. | ||
| slug: refig | ||
| authors: universe | ||
| date: 2026-02-17 | ||
| tags: [figma, rendering, ci, wasm, skia, tooling] | ||
| hide_table_of_contents: false | ||
| --- | ||
|
|
||
| Figma exports are easy—until exporting becomes **infrastructure**. | ||
|
|
||
| Teams start with “click Export” and end up needing **thumbnails**, **asset pipelines**, **visual diffs**, and **repeatable builds**. That’s where the usual options start to hurt: | ||
|
|
||
| - A headless browser is slow and flaky in CI. | ||
| - The Figma Images API is great, but it’s still **a network dependency** (tokens, rate limits, availability). | ||
| - Signed image URLs expire, which makes “render later” workflows fragile. | ||
| - Offline or air‑gapped environments simply can’t rely on API calls. | ||
|
|
||
| **refig** is built for that gap. | ||
|
|
||
| <!-- truncate --> | ||
|
|
||
| ## What is refig? | ||
|
|
||
| **`@grida/refig`** (“render figma”) is a **headless Figma renderer**. It turns a Figma document + node id into **PNG, JPEG, WebP, PDF, or SVG**—without opening Figma and without driving a browser UI. | ||
|
|
||
| It works with: | ||
|
|
||
| - **`.fig` files** (offline, reproducible) | ||
| - **Figma REST API file JSON** (`GET /v1/files/:key`) when you already have your own ingestion layer | ||
|
|
||
| You can use it as a **CLI** (`refig`) or as a **library** in Node.js and the browser. | ||
|
|
||
| - Technical reference and usage: [`@grida/refig` on npm](https://www.npmjs.com/package/@grida/refig) | ||
|
|
||
| ## The idea: deterministic rendering you can build on | ||
|
|
||
| When rendering is deterministic and programmable, a bunch of workflows become straightforward: | ||
|
|
||
| - **Preview services**: generate thumbnails for files, pages, frames, components. | ||
| - **Asset pipelines**: treat “Export presets” as build artifacts (commit, cache, publish). | ||
| - **Visual regression tests**: render the same node on every PR and diff outputs. | ||
| - **Offline archives**: store `.fig` snapshots and reproduce outputs later—no tokens, no network. | ||
| - **In-app previews**: render inside your product (browser entrypoint) to show design snapshots. | ||
|
|
||
| In other words: refig is less about “export an image” and more about making Figma rendering a reliable building block. | ||
|
|
||
| ## What refig intentionally doesn’t do | ||
|
|
||
| refig is opinionated about scope so it stays composable: | ||
|
|
||
| - **No auth / fetching**: you bring your own token storage, HTTP client, and caching. | ||
| - **No design-to-code**: this is rendering (pixels / SVG / PDF), not HTML/CSS/Flutter generation. | ||
| - **No editor**: read + render only. | ||
|
|
||
| ## A tiny taste (CLI) | ||
|
|
||
| If you just want to feel what it’s like: | ||
|
|
||
| ```sh | ||
| # Render a node from a .fig export | ||
| npx @grida/refig ./design.fig --node "1:23" --out ./out.png | ||
|
|
||
| # Or render from REST API JSON you fetched elsewhere | ||
| npx @grida/refig ./document.json --node "1:23" --out ./out.svg | ||
| ``` | ||
|
|
||
| From there, most teams graduate quickly to: “render N nodes”, “render on every commit”, or “render on demand”. | ||
|
|
||
| ## Where this is headed | ||
|
|
||
| We’re treating refig as a foundation for “design → build” automation: | ||
|
|
||
| - render outputs that are **repeatable** | ||
| - workflows that can run **in CI** | ||
| - tooling that can work **offline** | ||
|
|
||
| If that sounds like your problem, start with the docs (high-level) and the npm page (full API/CLI reference), then wire it into the pipeline you already have: | ||
|
|
||
| - [`@grida/refig` docs](https://grida.co/docs/packages/@grida/refig) | ||
| - [`@grida/refig` on npm](https://www.npmjs.com/package/@grida/refig) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Git LFS file not shown
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
str | Noneunion syntax requires Python ≥ 3.10.If this script needs to run on older Python 3 versions (e.g., 3.8/3.9), use
Optional[str]fromtypinginstead or addfrom __future__ import annotations.🤖 Prompt for AI Agents