使用 tldraw 的 `editor.toImage` 导出整张画布为 PNG 图片:从零实现"导出画布"按钮
本指南基于 tldraw 仓库 中官方示例 "Export canvas as image",讲解如何在 React 应用中通过 editor.toImage 把页面上的全部图形渲染成一张 PNG Blob 并触发浏览器下载。读完你将掌握 toImage 的完整调用链、TLImageExportOptions 全部配置项(background、padding、scale、darkMode、bounds 等)的含义与默认值,以及将其封装成 SharePanel 工具栏按钮的实战写法。
示例定位与核心思路
该示例位于仓库的 示例源码目录,配套的元数据描述文件 README.md 中声明了 keywords: [export, toimage, download, png, svg, blob, screenshot, canvas export, sharepanel],一句话概括它的做法:
"Render every shape on the page to a PNG with
editor.toImageand download it."(把页面上每个图形通过editor.toImage渲染成 PNG 并下载。)
它的实现路径非常清晰:通过 useEditor 拿到编辑器实例 → 用 editor.getCurrentPageShapeIds() 取得当前页面所有图形 ID → 调用 editor.toImage(shapeIds, { format: 'png', background: false }) 得到图片 Blob → 在浏览器中用带 download 属性的临时 <a> 链接把 Blob 下载为 every-shape-on-the-canvas.png。
最小可运行的"导出画布"按钮
示例的核心是一个挂在 SharePanel UI 插槽上的自定义组件。tldraw 允许通过 <Tldraw components={...}> 覆盖默认界面组件,这里把右上角分享面板位置替换成导出按钮:
import { Tldraw, TldrawUiButton, TldrawUiButtonLabel, TLUiComponents, useEditor } from 'tldraw'
import 'tldraw/tldraw.css'
function ExportCanvasButton() {
const editor = useEditor()
return (
<div className="tlui-menu" style={{ pointerEvents: 'all' }}>
<TldrawUiButton
type="normal"
onClick={async () => {
const shapeIds = editor.getCurrentPageShapeIds()
if (shapeIds.size === 0) return alert('No shapes on the canvas')
// ① 将全部图形渲染为 PNG Blob
const { blob } = await editor.toImage([...shapeIds], {
format: 'png',
background: false,
})
// ② 通过临时下载链接把 Blob 保存为本地文件
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = 'every-shape-on-the-canvas.png'
link.click()
URL.revokeObjectURL(link.href)
}}
>
<TldrawUiButtonLabel>Export canvas as image</TldrawUiButtonLabel>
</TldrawUiButton>
</div>
)
}
const components: TLUiComponents = {
SharePanel: ExportCanvasButton,
}
export default function ExportCanvasImageExample() {
return (
<div className="tldraw__editor">
<Tldraw components={components} />
</div>
)
}
整个示例只需一个自定义组件与一行 components 配置:把 SharePanel 插槽替换为 ExportCanvasButton。示例注释中特别标注了两段关键逻辑:
- [1]
editor.toImage把传入的图形渲染成Blob。传入当前页所有 shape id 即导出整张画布;若只想导出部分内容,可以传子集(如editor.getSelectedShapeIds())。format支持'png'、'jpeg'、'webp'与'svg'。 - [2] 浏览器中最简单的 Blob 下载方式是用一个带
download属性的临时<a>链接,下载结束后立即URL.revokeObjectURL释放对象 URL 内存。
运行时体验是:先在画布上随手画几个图形,点击 "Export canvas as image" 按钮,即可得到一张透明背景的 PNG。
核心 API:editor.toImage 签名与返回值
toImage 定义在编辑器核心实现 packages/editor/src/lib/editor/Editor.ts 中,是编辑器公开 API 的一部分:
async toImage(
shapes: TLShapeId[] | TLShape[],
opts: TLImageExportOptions = {}
): Promise<{ blob: Blob; width: number; height: number }>
- 第一个参数:
TLShapeId[](图形 ID 数组)或TLShape[](图形对象数组)。传入空数组时,底层getSvgElement会自动回退为导出当前页面全部图形(getCurrentPageShapeIdsSorted)。 - 返回值:包含
blob(图片二进制)、width与height(导出图片的像素尺寸)的对象。 - 调用内部会先等待字体加载:在 Editor.ts 的
getSvgElement中可以看到,导出前会执行await this.fonts.loadRequiredFontsForCurrentPage(...)。这是因为文本图形的尺寸依赖已加载字体做测量,若在编辑器刚挂载、字体尚未加载完成时导出,结果会按回退字体度量计算尺寸与排版。
导出主流程解析
toImage 的实现分三步走:
- 默认参数补齐(Editor.ts):
format默认'png'、scale默认1、pixelRatio在非 SVG 格式下默认2(即位图导出默认按 2 倍像素密度生成,保证高分屏清晰度)。 - 先生成 SVG 字符串:调用
getSvgString(shapes, withDefaults),其中getSvgString→getSvgElement→exportToSvg(this, ids, opts),把图形集合序列化为 SVG 元素再经XMLSerializer转成字符串,同时返回width、height与trimPadding。 - 按格式分流产出 Blob:
format: 'svg':直接以image/svg+xml类型包裹 SVG 字符串生成 Blob;若存在trimPadding,会先调用trimSvgToContent把空白裁掉。format: 'png' | 'jpeg' | 'webp':调用getSvgAsImageWithOptions(来自 packages/editor/src/lib/exports/getSvgAsImage.ts)做 SVG→位图的栅格化,可传入type、quality、pixelRatio、目标宽高与裁切信息。- 其他值走
exhaustiveSwitchError兜底报错。
同族 API 还有 getSvgString(拿 SVG 字符串)与 toImageDataUrl(Editor.ts 返回 blobToDataUrl 转换后的 data URL 及宽高),适合图片预览等无需下载的场景。
完整导出选项 TLImageExportOptions
toImage 的第二个参数类型定义在 packages/editor/src/lib/editor/types/misc-types.ts,TLImageExportOptions 继承自 TLSvgExportOptions,并追加了两个位图相关字段。各字段如下:
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
format |
'svg' | 'png' | 'jpeg' | 'webp' |
'png' |
导出格式(见 TLExportType,misc-types.ts) |
quality |
number |
无 | 有损位图格式(如 jpeg)的压缩质量,取值 0 到 1 |
background |
boolean |
无 | 是否包含背景色;为 false 且格式支持透明时导出透明背景 |
darkMode |
boolean |
编辑器当前深色模式 | 以深色模式(true)还是浅色模式(false)渲染导出 |
padding |
number | 'auto' |
'auto' |
导出四周留白:'auto' 自动裁到可视内容边界(捕捉粗描边、箭头等溢出部分而不留多余空白);数值(如 32)表示固定像素留白、不做裁切;0 表示无留白不裁切,溢出会被裁剪 |
bounds |
Box |
无 | 页面坐标系下的导出裁剪框;传了就用它裁剪而非自适应图形 |
scale |
number |
1 |
导出的逻辑缩放比例,同时放大生成 SVG/位图的尺寸 |
pixelRatio |
number |
SVG 无;位图 2 |
位图导出时最终尺寸乘以此数;SVG 导出时作为 dpr 传给 TLAssetStore.resolve 以便按分辨率请求资源 |
preserveAspectRatio |
string |
无 | 生成 SVG 元素的 preserveAspectRatio 属性 |
示例源码注释补充了重要经验:内置的"导出/复制"动作会用用户偏好(例如 editor.user.getIsDarkMode())填充这些选项,但当你自己调用 toImage 时完全由你决定每个值。而编辑器级的默认留白常量是 defaultSvgPadding: 32,定义在 packages/editor/src/lib/options.ts,这也是设置面板变体中 padding 选项的推荐初值来源。
进阶变体:带设置面板的导出(export-canvas-settings)
README 中提到的 "with settings" 变体在仓库中有完整实现:示例源码 与其 README.md。它保留同样的 SharePanel 挂载与 Blob 下载逻辑,额外加了一个控制面板,用 React 状态实时调整 TLImageExportOptions 的每个字段:
// ① 通过 useState 维护导出选项
const [opts, setOpts] = useState<TLImageExportOptions>({
scale: 1,
background: false,
padding: editor.options.defaultSvgPadding,
})
// ② bounds 初始为当前视口,保证首次导出所见即所得
const [box, setBox] = useState(() => {
const v = editor.getViewportPageBounds()
return { x: Math.round(v.x), y: Math.round(v.y), w: Math.round(v.w), h: Math.round(v.h) }
})
// ③ 导出时合并状态;宽或高为 0 的 Box 视为"无 bounds"
const { blob } = await editor.toImage([...shapeIds], {
format: 'png',
...opts,
bounds: box.w > 0 && box.h > 0 ? new Box(box.x, box.y, box.w, box.h) : undefined,
})
这个变体回答了几个常见的定制问题:
- 如何导出透明背景 PNG:
background: false。 - 如何导出一块自定义区域:把
bounds设为页面坐标系下的Box(x、y、宽、高),它会按此裁剪而不是自适应图形范围。面板里提供 x/y/w/h 四个输入框;当宽或高为 0 时该 Box 被当作"未设置",从而回到"适配全部图形"的默认行为。用editor.getViewportPageBounds()取当前视口作为起点,可实现首次导出与屏幕上看到的内容一致。 - 如何控制留白:
padding支持像素数或'auto';示例初值取editor.options.defaultSvgPadding(32px)。从类型注释(misc-types.ts)可知:'auto'会裁到可视内容边界并捕获粗描边、箭头之类的溢出;固定数值则不做裁切。 - 如何导出深色/浅色外观:
darkMode开关。它默认沿用编辑器当前实例的深色模式设置。 - 如何放大导出:
scale用正整数调节(示例里对数值取Math.ceil取整)。
从示例到产品化的几个工程要点
- 放在哪:示例选择挂在
SharePanel插槽,通过<Tldraw components={components} />覆盖默认分享按钮,因而按钮天然继承 tldraw 工具栏布局与样式体系(tlui-menu容器、TldrawUiButton)。也可以放到任意自定义 React 工具栏里——前提只是能在组件树中通过useEditor()拿到编辑器实例(即组件位于<Tldraw>内部)。 - 空画布防护:
editor.getCurrentPageShapeIds()返回一个集合,尺寸为 0 时先alert提示并中止导出,避免无意义的空图片下载。 - 内存管理:
URL.createObjectURL产生的对象 URL 应在下载触发后立即URL.revokeObjectURL回收,防止长期持有 Blob 内存。若在同一页面频繁导出,也可以自行缓存并复用 object URL。 - 画布 vs 选区导出:
[...shapeIds]传整页全部图形即"导出画布";把入参换成editor.getSelectedShapeIds()即可做出"导出选中内容"功能——同一个 API、只需换一组 ID。 - 相关能力:想要图片之外的形式,可改用
getSvgString获取可嵌入的 SVG 文本;想直接在页面预览,可用toImageDataUrl拿 data URL;图片资源在导出时的按分辨率引用问题则由pixelRatio/dpr交给TLAssetStore.resolve处理。
延伸阅读(仓库内相关文件)
- 本示例源码:ExportCanvasImageExample.tsx,配套描述:README.md
- 设置面板进阶变体源码:ExportCanvasImageSettingsExample.tsx,配套描述:README.md
toImage/getSvgElement/getSvgString/toImageDataUrl实现:packages/editor/src/lib/editor/Editor.tsTLImageExportOptions/TLSvgExportOptions/TLExportType类型定义:packages/editor/src/lib/editor/types/misc-types.ts- SVG→位图栅格化逻辑:packages/editor/src/lib/exports/getSvgAsImage.ts
- 编辑器默认导出留白
defaultSvgPadding: 32:packages/editor/src/lib/options.ts
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00