首页
/ 使用 tldraw 的 `editor.toImage` 导出整张画布为 PNG 图片:从零实现"导出画布"按钮

使用 tldraw 的 `editor.toImage` 导出整张画布为 PNG 图片:从零实现"导出画布"按钮

2026-09-07 22:35:02作者:幸俭卉

本指南基于 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.toImage and 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(图片二进制)、widthheight(导出图片的像素尺寸)的对象。
  • 调用内部会先等待字体加载:在 Editor.tsgetSvgElement 中可以看到,导出前会执行 await this.fonts.loadRequiredFontsForCurrentPage(...)。这是因为文本图形的尺寸依赖已加载字体做测量,若在编辑器刚挂载、字体尚未加载完成时导出,结果会按回退字体度量计算尺寸与排版。

导出主流程解析

toImage 的实现分三步走:

  1. 默认参数补齐Editor.ts):format 默认 'png'scale 默认 1pixelRatio 在非 SVG 格式下默认 2(即位图导出默认按 2 倍像素密度生成,保证高分屏清晰度)。
  2. 先生成 SVG 字符串:调用 getSvgString(shapes, withDefaults),其中 getSvgStringgetSvgElementexportToSvg(this, ids, opts),把图形集合序列化为 SVG 元素再经 XMLSerializer 转成字符串,同时返回 widthheighttrimPadding
  3. 按格式分流产出 Blob
    • format: 'svg':直接以 image/svg+xml 类型包裹 SVG 字符串生成 Blob;若存在 trimPadding,会先调用 trimSvgToContent 把空白裁掉。
    • format: 'png' | 'jpeg' | 'webp':调用 getSvgAsImageWithOptions(来自 packages/editor/src/lib/exports/getSvgAsImage.ts)做 SVG→位图的栅格化,可传入 typequalitypixelRatio、目标宽高与裁切信息。
    • 其他值走 exhaustiveSwitchError 兜底报错。

同族 API 还有 getSvgString(拿 SVG 字符串)与 toImageDataUrlEditor.ts 返回 blobToDataUrl 转换后的 data URL 及宽高),适合图片预览等无需下载的场景。

完整导出选项 TLImageExportOptions

toImage 的第二个参数类型定义在 packages/editor/src/lib/editor/types/misc-types.tsTLImageExportOptions 继承自 TLSvgExportOptions,并追加了两个位图相关字段。各字段如下:

选项 类型 默认值 作用
format 'svg' | 'png' | 'jpeg' | 'webp' 'png' 导出格式(见 TLExportTypemisc-types.ts
quality number 有损位图格式(如 jpeg)的压缩质量,取值 01
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,
})

这个变体回答了几个常见的定制问题:

  • 如何导出透明背景 PNGbackground: 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 取整)。

从示例到产品化的几个工程要点

  1. 放在哪:示例选择挂在 SharePanel 插槽,通过 <Tldraw components={components} /> 覆盖默认分享按钮,因而按钮天然继承 tldraw 工具栏布局与样式体系(tlui-menu 容器、TldrawUiButton)。也可以放到任意自定义 React 工具栏里——前提只是能在组件树中通过 useEditor() 拿到编辑器实例(即组件位于 <Tldraw> 内部)。
  2. 空画布防护editor.getCurrentPageShapeIds() 返回一个集合,尺寸为 0 时先 alert 提示并中止导出,避免无意义的空图片下载。
  3. 内存管理URL.createObjectURL 产生的对象 URL 应在下载触发后立即 URL.revokeObjectURL 回收,防止长期持有 Blob 内存。若在同一页面频繁导出,也可以自行缓存并复用 object URL。
  4. 画布 vs 选区导出[...shapeIds] 传整页全部图形即"导出画布";把入参换成 editor.getSelectedShapeIds() 即可做出"导出选中内容"功能——同一个 API、只需换一组 ID。
  5. 相关能力:想要图片之外的形式,可改用 getSvgString 获取可嵌入的 SVG 文本;想直接在页面预览,可用 toImageDataUrl 拿 data URL;图片资源在导出时的按分辨率引用问题则由 pixelRatio/dpr 交给 TLAssetStore.resolve 处理。

延伸阅读(仓库内相关文件)

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391