figma-capture-extension

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Figma Capture Extension

Figma Capture 扩展

Skill by ara.so — Design Skills collection
This Chrome extension captures any webpage into Figma's clipboard format, adding post-processing to fix fonts (especially CJK), clean up DOM structure, and remove empty elements before pasting into Figma.
来自ara.so的技能——设计技能合集
这款Chrome扩展可将任意网页捕获为Figma的剪贴板格式,在粘贴到Figma之前进行后期处理,修复字体(尤其是CJK字体)、清理DOM结构并移除空元素。

What It Does

功能介绍

  • CJK Font Correction: Detects Chinese/Japanese/Korean text and remaps to
    PingFang SC
    /
    Noto Serif SC
  • Font Mapping: Remaps unavailable fonts via configurable
    font-map.json
  • Default Font Fallback: Assigns
    Noto Sans SC
    to elements without explicit fonts
  • DOM Flattening: Removes wrapper elements that don't contribute visually
  • Empty Frame Cleanup: Strips zero-size elements and childless containers
  • Event Isolation: Prevents toolbar clicks from affecting host page behavior
  • CJK字体修正:检测中文/日文/韩文文本,重新映射为
    PingFang SC
    /
    Noto Serif SC
  • 字体映射:通过可配置的
    font-map.json
    重新映射不可用字体
  • 默认字体回退:为未指定字体的元素分配
    Noto Sans SC
  • DOM扁平化:移除对视觉无贡献的包装元素
  • 空框架清理:剥离零尺寸元素和无子元素的容器
  • 事件隔离:防止工具栏点击影响宿主页面行为

Installation

安装步骤

Step 1: Download Figma's Capture Script

步骤1:下载Figma的Capture脚本

bash
make
This downloads
capture.js
from Figma's public endpoint. The Makefile does:
makefile
capture.js:
	curl -o capture.js https://www.figma.com/community/plugin/1159123024924461424/capture.js
bash
make
此命令会从Figma的公开端点下载
capture.js
。Makefile执行的操作如下:
makefile
capture.js:
	curl -o capture.js https://www.figma.com/community/plugin/1159123024924461424/capture.js

Step 2: Configure Font Mapping

步骤2:配置字体映射

bash
cp font-map.example.json font-map.json
Example
font-map.json
structure:
json
{
  "Arial": "Inter",
  "Helvetica": "Inter",
  "SF Pro Display": "Google Sans Flex",
  "Roboto": "Noto Sans SC",
  "system-ui": "Inter"
}
bash
cp font-map.example.json font-map.json
font-map.json
的示例结构:
json
{
  "Arial": "Inter",
  "Helvetica": "Inter",
  "SF Pro Display": "Google Sans Flex",
  "Roboto": "Noto Sans SC",
  "system-ui": "Inter"
}

Step 3: Load Extension in Chrome

步骤3:在Chrome中加载扩展

  1. Navigate to
    chrome://extensions
  2. Enable Developer mode (top right toggle)
  3. Click Load unpacked
  4. Select the
    figma-capture
    directory
  1. 访问
    chrome://extensions
  2. 启用开发者模式(右上角开关)
  3. 点击加载已解压的扩展程序
  4. 选择
    figma-capture
    目录

Usage

使用方法

Basic Capture Workflow

基础捕获流程

  1. Navigate to the webpage you want to capture
  2. Click the extension icon or press
    Alt+Shift+F
  3. Use the toolbar to:
    • Capture entire page
    • Select specific element by clicking
  4. Switch to Figma
  5. Press
    Ctrl+V
    (Windows) or
    Cmd+V
    (Mac)
  1. 导航到需要捕获的网页
  2. 点击扩展图标 按下快捷键
    Alt+Shift+F
  3. 使用工具栏:
    • 捕获整个页面
    • 点击选择特定元素
  4. 切换到Figma
  5. 按下
    Ctrl+V
    (Windows)或
    Cmd+V
    (Mac)粘贴

Keyboard Shortcut

键盘快捷键

Default:
Alt+Shift+F
To customize, go to
chrome://extensions/shortcuts
默认快捷键:
Alt+Shift+F
如需自定义,前往
chrome://extensions/shortcuts

Configuration Files

配置文件

manifest.json

manifest.json

Key configuration sections:
json
{
  "manifest_version": 3,
  "name": "Figma Capture",
  "version": "1.0.0",
  "permissions": [
    "activeTab",
    "clipboardWrite"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_start"
    }
  ],
  "action": {
    "default_icon": "icon.png"
  },
  "commands": {
    "_execute_action": {
      "suggested_key": {
        "default": "Alt+Shift+F"
      }
    }
  }
}
关键配置部分:
json
{
  "manifest_version": 3,
  "name": "Figma Capture",
  "version": "1.0.0",
  "permissions": [
    "activeTab",
    "clipboardWrite"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_start"
    }
  ],
  "action": {
    "default_icon": "icon.png"
  },
  "commands": {
    "_execute_action": {
      "suggested_key": {
        "default": "Alt+Shift+F"
      }
    }
  }
}

Font Mapping Configuration

字体映射配置

Edit
font-map.json
to add custom font substitutions:
json
{
  "SegoeUI": "Inter",
  "San Francisco": "Google Sans Flex",
  "PingFang": "PingFang SC",
  "Microsoft YaHei": "Noto Sans SC",
  "Hiragino Sans": "Noto Sans JP"
}
Font mapping logic (applied in order):
  1. CJK detection →
    PingFang SC
    /
    Noto Serif SC
  2. Custom
    font-map.json
    mappings
  3. Icon font detection → preserve original
  4. No font specified →
    Noto Sans SC
编辑
font-map.json
添加自定义字体替换规则:
json
{
  "SegoeUI": "Inter",
  "San Francisco": "Google Sans Flex",
  "PingFang": "PingFang SC",
  "Microsoft YaHei": "Noto Sans SC",
  "Hiragino Sans": "Noto Sans JP"
}
字体映射逻辑(按顺序执行):
  1. CJK文本检测 → 应用
    PingFang SC
    /
    Noto Serif SC
  2. 自定义
    font-map.json
    映射规则
  3. 图标字体检测 → 保留原字体
  4. 未指定字体 → 应用
    Noto Sans SC

Architecture

架构设计

Component Flow

组件流程

User clicks extension icon
background.js activates content script
content.js injects capture.js
capture.js serializes DOM → clipboard payload
Clipboard interceptor transforms payload:
  - Font correction
  - DOM cleanup
  - Wrapper flattening
Modified payload written to clipboard
User pastes into Figma
用户点击扩展图标
background.js 激活内容脚本
content.js 注入capture.js
capture.js 序列化DOM → 剪贴板负载
剪贴板拦截器转换负载:
  - 字体修正
  - DOM清理
  - 包装元素扁平化
修改后的负载写入剪贴板
用户粘贴到Figma

Key Files

核心文件

  • background.js: Service worker, patches
    attachShadow
    for event isolation
  • content.js: Injected script, intercepts clipboard API
  • capture.js: Figma's official serializer (downloaded, not in repo)
  • font-map.json: User-configurable font substitutions
  • background.js:服务工作线程,修补
    attachShadow
    实现事件隔离
  • content.js:注入脚本,拦截剪贴板API
  • capture.js:Figma官方序列化脚本(需下载,不在仓库中)
  • font-map.json:用户可配置的字体替换规则

Code Examples

代码示例

Clipboard Interception Pattern

剪贴板拦截模式

javascript
// Override clipboard write to transform Figma payload
const originalWrite = navigator.clipboard.write;
navigator.clipboard.write = async function(data) {
  const items = await Promise.all(data.map(async item => {
    if (item.types.includes('text/html')) {
      const blob = await item.getType('text/html');
      const html = await blob.text();
      
      // Transform Figma's clipboard payload
      const transformed = transformFigmaPayload(html);
      
      return new ClipboardItem({
        'text/html': new Blob([transformed], { type: 'text/html' })
      });
    }
    return item;
  }));
  
  return originalWrite.call(this, items);
};
javascript
// Override clipboard write to transform Figma payload
const originalWrite = navigator.clipboard.write;
navigator.clipboard.write = async function(data) {
  const items = await Promise.all(data.map(async item => {
    if (item.types.includes('text/html')) {
      const blob = await item.getType('text/html');
      const html = await blob.text();
      
      // Transform Figma's clipboard payload
      const transformed = transformFigmaPayload(html);
      
      return new ClipboardItem({
        'text/html': new Blob([transformed], { type: 'text/html' })
      });
    }
    return item;
  }));
  
  return originalWrite.call(this, items);
};

Font Detection and Remapping

字体检测与重映射

javascript
function detectAndFixFont(element, computedStyle) {
  const text = element.textContent || '';
  const fontFamily = computedStyle.fontFamily;
  
  // CJK detection
  const cjkRegex = /[\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/;
  if (cjkRegex.test(text)) {
    // Check if serif
    const isSerif = /serif/i.test(fontFamily);
    return isSerif ? 'Noto Serif SC' : 'PingFang SC';
  }
  
  // Load font map
  const fontMap = loadFontMap(); // from font-map.json
  
  // Check for mapped font
  for (const [original, replacement] of Object.entries(fontMap)) {
    if (fontFamily.includes(original)) {
      return replacement;
    }
  }
  
  // Icon font detection (preserve)
  if (/icon|symbol|awesome|material/i.test(fontFamily)) {
    return fontFamily;
  }
  
  // Default fallback
  return fontFamily || 'Noto Sans SC';
}
javascript
function detectAndFixFont(element, computedStyle) {
  const text = element.textContent || '';
  const fontFamily = computedStyle.fontFamily;
  
  // CJK detection
  const cjkRegex = /[\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/;
  if (cjkRegex.test(text)) {
    // Check if serif
    const isSerif = /serif/i.test(fontFamily);
    return isSerif ? 'Noto Serif SC' : 'PingFang SC';
  }
  
  // Load font map
  const fontMap = loadFontMap(); // from font-map.json
  
  // Check for mapped font
  for (const [original, replacement] of Object.entries(fontMap)) {
    if (fontFamily.includes(original)) {
      return replacement;
    }
  }
  
  // Icon font detection (preserve)
  if (/icon|symbol|awesome|material/i.test(fontFamily)) {
    return fontFamily;
  }
  
  // Default fallback
  return fontFamily || 'Noto Sans SC';
}

DOM Cleanup: Wrapper Flattening

DOM清理:包装元素扁平化

javascript
function flattenWrappers(node) {
  // Skip if not a wrapper candidate
  if (node.children.length !== 1) return false;
  
  const parent = node;
  const child = node.children[0];
  
  // Check if parent is just a pass-through container
  const parentStyle = getComputedStyle(parent);
  const childStyle = getComputedStyle(child);
  
  const isPassThrough = 
    parentStyle.backgroundColor === 'transparent' &&
    parentStyle.border === 'none' &&
    parent.getBoundingClientRect().width === child.getBoundingClientRect().width &&
    parent.getBoundingClientRect().height === child.getBoundingClientRect().height;
  
  if (isPassThrough) {
    // Promote child to parent's position
    parent.replaceWith(child);
    return true;
  }
  
  return false;
}
javascript
function flattenWrappers(node) {
  // Skip if not a wrapper candidate
  if (node.children.length !== 1) return false;
  
  const parent = node;
  const child = node.children[0];
  
  // Check if parent is just a pass-through container
  const parentStyle = getComputedStyle(parent);
  const childStyle = getComputedStyle(child);
  
  const isPassThrough = 
    parentStyle.backgroundColor === 'transparent' &&
    parentStyle.border === 'none' &&
    parent.getBoundingClientRect().width === child.getBoundingClientRect().width &&
    parent.getBoundingClientRect().height === child.getBoundingClientRect().height;
  
  if (isPassThrough) {
    // Promote child to parent's position
    parent.replaceWith(child);
    return true;
  }
  
  return false;
}

Event Isolation for Toolbar

工具栏事件隔离

javascript
// In background.js - save closed shadow roots
const shadowRoots = new WeakMap();

const originalAttachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function(init) {
  const shadowRoot = originalAttachShadow.call(this, init);
  if (init.mode === 'closed') {
    shadowRoots.set(this, shadowRoot);
  }
  return shadowRoot;
};

// In content.js - intercept toolbar clicks
window.addEventListener('click', (event) => {
  const toolbar = document.querySelector('.figma-capture-toolbar');
  if (!toolbar) return;
  
  const toolbarHost = toolbar.host;
  const shadowRoot = shadowRoots.get(toolbarHost);
  
  if (shadowRoot) {
    event.stopPropagation();
    event.preventDefault();
    
    // Re-dispatch inside shadow DOM
    const point = shadowRoot.elementFromPoint(event.clientX, event.clientY);
    if (point) {
      point.dispatchEvent(new MouseEvent('click', event));
    }
  }
}, true); // Capture phase
javascript
// In background.js - save closed shadow roots
const shadowRoots = new WeakMap();

const originalAttachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function(init) {
  const shadowRoot = originalAttachShadow.call(this, init);
  if (init.mode === 'closed') {
    shadowRoots.set(this, shadowRoot);
  }
  return shadowRoot;
};

// In content.js - intercept toolbar clicks
window.addEventListener('click', (event) => {
  const toolbar = document.querySelector('.figma-capture-toolbar');
  if (!toolbar) return;
  
  const toolbarHost = toolbar.host;
  const shadowRoot = shadowRoots.get(toolbarHost);
  
  if (shadowRoot) {
    event.stopPropagation();
    event.preventDefault();
    
    // Re-dispatch inside shadow DOM
    const point = shadowRoot.elementFromPoint(event.clientX, event.clientY);
    if (point) {
      point.dispatchEvent(new MouseEvent('click', event));
    }
  }
}, true); // Capture phase

Common Patterns

通用模式

Handling Special Elements

特殊元素处理

javascript
// Skip script/style/svg from processing
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'LINK', 'META'];

function shouldProcessElement(element) {
  if (SKIP_TAGS.includes(element.tagName)) {
    return false;
  }
  
  // Skip hidden elements
  const style = getComputedStyle(element);
  if (style.display === 'none' || style.visibility === 'hidden') {
    return false;
  }
  
  // Skip zero-size elements
  const rect = element.getBoundingClientRect();
  if (rect.width === 0 && rect.height === 0) {
    return false;
  }
  
  return true;
}
javascript
// Skip script/style/svg from processing
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'LINK', 'META'];

function shouldProcessElement(element) {
  if (SKIP_TAGS.includes(element.tagName)) {
    return false;
  }
  
  // Skip hidden elements
  const style = getComputedStyle(element);
  if (style.display === 'none' || style.visibility === 'hidden') {
    return false;
  }
  
  // Skip zero-size elements
  const rect = element.getBoundingClientRect();
  if (rect.width === 0 && rect.height === 0) {
    return false;
  }
  
  return true;
}

Custom Font Mapping for Specific Websites

特定网站自定义字体映射

javascript
// Create site-specific font mappings
const siteSpecificMaps = {
  'github.com': {
    '-apple-system': 'Inter',
    'BlinkMacSystemFont': 'Inter',
    'Segoe UI': 'Inter'
  },
  'twitter.com': {
    'TwitterChirp': 'Google Sans Flex',
    'Helvetica Neue': 'Inter'
  }
};

function getFontMap() {
  const hostname = window.location.hostname;
  const baseMap = JSON.parse(localStorage.getItem('font-map.json') || '{}');
  const siteMap = siteSpecificMaps[hostname] || {};
  
  return { ...baseMap, ...siteMap };
}
javascript
// Create site-specific font mappings
const siteSpecificMaps = {
  'github.com': {
    '-apple-system': 'Inter',
    'BlinkMacSystemFont': 'Inter',
    'Segoe UI': 'Inter'
  },
  'twitter.com': {
    'TwitterChirp': 'Google Sans Flex',
    'Helvetica Neue': 'Inter'
  }
};

function getFontMap() {
  const hostname = window.location.hostname;
  const baseMap = JSON.parse(localStorage.getItem('font-map.json') || '{}');
  const siteMap = siteSpecificMaps[hostname] || {};
  
  return { ...baseMap, ...siteMap };
}

Troubleshooting

故障排除

Fonts Not Displaying Correctly in Figma

Figma中字体显示不正确

Problem: Pasted elements show default Times or incorrect fonts
Solution: Check if fonts are installed in Figma:
javascript
// Add debug logging to see what fonts are being applied
console.log('Applied font:', appliedFont);
console.log('Original font:', originalFont);
console.log('Font map:', fontMap);
Update
font-map.json
to map problematic fonts to fonts you have in Figma:
json
{
  "ProblematicFont": "Inter",
  "AnotherBadFont": "Roboto"
}
问题:粘贴的元素显示默认Times字体或错误字体
解决方案:检查字体是否已在Figma中安装:
javascript
// Add debug logging to see what fonts are being applied
console.log('Applied font:', appliedFont);
console.log('Original font:', originalFont);
console.log('Font map:', fontMap);
更新
font-map.json
,将有问题的字体映射到你在Figma中已有的字体:
json
{
  "ProblematicFont": "Inter",
  "AnotherBadFont": "Roboto"
}

Extension Not Activating

扩展无法激活

Problem: Clicking icon does nothing
Solution: Check console for errors:
  1. Right-click extension icon → Inspect popup
  2. Go to
    chrome://extensions
    → Find Figma Capture → Click "background page"
  3. Check for errors in console
Verify
capture.js
exists:
bash
ls -la capture.js
问题:点击图标无反应
解决方案:检查控制台错误:
  1. 右键点击扩展图标 → 检查弹出窗口
  2. 前往
    chrome://extensions
    → 找到Figma Capture → 点击“背景页”
  3. 检查控制台中的错误信息
确认
capture.js
存在:
bash
ls -la capture.js

If missing:

如果缺失:

make
undefined
make
undefined

Clipboard Payload Not Modified

剪贴板负载未被修改

Problem: Fonts/cleanup not being applied
Solution: Verify clipboard interceptor is loaded:
javascript
// In browser console on target page:
console.log(navigator.clipboard.write.toString());
// Should show wrapped function, not native code
Reload extension:
  1. chrome://extensions
  2. Click reload icon for Figma Capture
  3. Refresh target webpage
问题:字体修复/清理未生效
解决方案:验证剪贴板拦截器已加载:
javascript
// In browser console on target page:
console.log(navigator.clipboard.write.toString());
// 应显示包装后的函数,而非原生代码
重新加载扩展:
  1. 打开
    chrome://extensions
  2. 点击Figma Capture的重新加载图标
  3. 刷新目标网页

Empty or Broken Capture

捕获内容为空或损坏

Problem: Pasting creates empty frame or errors
Solution: Some elements may be over-cleaned. Adjust cleanup logic:
javascript
// Disable wrapper flattening temporarily
const ENABLE_FLATTENING = false;

if (ENABLE_FLATTENING) {
  flattenWrappers(node);
}
Check for CSP violations in console — some sites block extension scripts.
问题:粘贴后生成空框架或出现错误
解决方案:部分元素可能被过度清理,调整清理逻辑:
javascript
// Disable wrapper flattening temporarily
const ENABLE_FLATTENING = false;

if (ENABLE_FLATTENING) {
  flattenWrappers(node);
}
检查控制台中的CSP违规信息——部分网站会阻止扩展脚本注入。

CJK Text Not Detected

CJK文本未被检测到

Problem: Chinese/Japanese/Korean text uses wrong font
Solution: Verify regex pattern matches your text:
javascript
const text = "你好世界";
const cjkRegex = /[\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/;
console.log(cjkRegex.test(text)); // Should be true
Manually set font in
font-map.json
:
json
{
  "SimSun": "Noto Sans SC",
  "Microsoft YaHei": "PingFang SC"
}
问题:中文/日文/韩文文本使用错误字体
解决方案:验证正则表达式是否匹配你的文本:
javascript
const text = "你好世界";
const cjkRegex = /[\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/;
console.log(cjkRegex.test(text)); // 应返回true
font-map.json
中手动设置字体:
json
{
  "SimSun": "Noto Sans SC",
  "Microsoft YaHei": "PingFang SC"
}

Advanced Configuration

高级配置

Modify Default Fallback Font

修改默认回退字体

Edit
content.js
:
javascript
const DEFAULT_FALLBACK_FONT = 'Inter'; // Change from 'Noto Sans SC'
编辑
content.js
javascript
const DEFAULT_FALLBACK_FONT = 'Inter'; // 从'Noto Sans SC'修改

Add Custom Cleanup Rules

添加自定义清理规则

javascript
function customCleanup(element) {
  // Remove all data attributes
  for (const attr of element.attributes) {
    if (attr.name.startsWith('data-')) {
      element.removeAttribute(attr.name);
    }
  }
  
  // Remove specific classes
  const REMOVE_CLASSES = ['ad', 'banner', 'cookie-notice'];
  element.classList.remove(...REMOVE_CLASSES);
}
javascript
function customCleanup(element) {
  // Remove all data attributes
  for (const attr of element.attributes) {
    if (attr.name.startsWith('data-')) {
      element.removeAttribute(attr.name);
    }
  }
  
  // Remove specific classes
  const REMOVE_CLASSES = ['ad', 'banner', 'cookie-notice'];
  element.classList.remove(...REMOVE_CLASSES);
}

Debug Mode

调试模式

Add to
content.js
:
javascript
const DEBUG = true;

function debug(...args) {
  if (DEBUG) {
    console.log('[Figma Capture]', ...args);
  }
}

debug('Font applied:', fontFamily);
debug('Element cleaned:', element.tagName);
content.js
中添加:
javascript
const DEBUG = true;

function debug(...args) {
  if (DEBUG) {
    console.log('[Figma Capture]', ...args);
  }
}

debug('Font applied:', fontFamily);
debug('Element cleaned:', element.tagName);

Limitations

局限性

  • Figma Dependency: Relies on Figma's undocumented clipboard format (may break)
  • Font Availability: Target fonts must be installed in Figma
  • CSP Restrictions: Some sites block extension script injection
  • Dynamic Content: May not capture lazy-loaded or JavaScript-rendered elements
  • capture.js
    Updates
    : Figma may change/remove the download endpoint
  • Figma依赖:依赖Figma未公开的剪贴板格式(可能随时失效)
  • 字体可用性:目标字体必须已在Figma中安装
  • CSP限制:部分网站会阻止扩展脚本注入
  • 动态内容:可能无法捕获懒加载或JavaScript渲染的元素
  • capture.js
    更新
    :Figma可能更改/移除下载端点

Resources

相关资源