工具函数
@ydesign/react-editor 在构建过程中积累了一批高复用的工具函数,它们都从子路径 @ydesign/react-editor/utils/* 单独导出,按需引入即可。
这些函数不是"为了凑数"而暴露的——编辑器内部的多个面板、工具栏组件都依赖它们。如果你在做自定义 Section / 自定义工具栏时遇到类似需求(比如单位换算、测量图片尺寸、转 Blob),优先用这里提供的版本:能保证与内置行为一致,未来升级也不会出差异。
本页按功能域组织。所有函数签名都来自真实源码 packages/react-editor/src/utils/。
💡
setTranslations/addGlobalFont/setUploadFunc/setAPI这类"全局配置"API 已集中在 编辑器配置 页。本页专注于无副作用的纯工具函数。
单位换算(@ydesign/react-editor/utils/unit)
画布内部使用像素,业务侧经常需要按 mm / cm / in / pt 显示和输入。这组函数做两侧的互转。
pxToUnit({ px, unit, dpi })
把像素值转换为目标单位的数值。
import { pxToUnit } from '@ydesign/react-editor/utils/unit';
pxToUnit({ px: 300, unit: 'mm', dpi: 72 }); // => 105.833...
pxToUnit({ px: 300, unit: 'in', dpi: 300 }); // => 1
支持的 unit:'pt' | 'mm' | 'cm' | 'in' | 'px'(pt 和 px 当前按 1:1 处理)。
pxToUnitRounded({ px, unit, dpi, precious? })
同上,但结果按 precious 位小数四舍五入(默认 2 位)。
pxToUnitRounded({ px: 300, unit: 'mm', dpi: 72 }); // => 105.83
pxToUnitRounded({ px: 300, unit: 'mm', dpi: 72, precious: 0 }); // => 106
pxToUnitString(params)
把像素值转换为带单位后缀的字符串,用于直接展示到 UI 上:
import { pxToUnitString } from '@ydesign/react-editor/utils/unit';
pxToUnitString({ px: 300, unit: 'mm', dpi: 72 }); // => '105.8mm'
pxToUnitString({ px: 300, unit: 'in', dpi: 300 }); // => '1in'
pxToUnitString({ px: 300, unit: 'px', dpi: 72 }); // => '300px'
unitToPx({ unitVal, unit, dpi })
从目标单位数值反向转回像素,常用于接收用户输入时:
import { unitToPx } from '@ydesign/react-editor/utils/unit';
// 用户填了 "210mm",打算做 A4 海报
const widthPx = unitToPx({ unitVal: 210, unit: 'mm', dpi: 300 });
// => 2480.31...
store.setSize({
width: Math.round(widthPx),
height: Math.round(unitToPx({ unitVal: 297, unit: 'mm', dpi: 300 })),
});
图片处理(@ydesign/react-editor/utils/image)
getImageSize(url): Promise<{ width, height }>
异步获取远程 / 本地图片的原始尺寸。内部创建 Image 对象,自动加了 crossOrigin='anonymous'(确保可用于 canvas 导出)。
import { getImageSize } from '@ydesign/react-editor/utils/image';
const src = 'https://cdn.example.com/photo.jpg';
const { width, height } = await getImageSize(src);
store.addElement({
type: 'image',
src,
left: (store.width - width) / 2,
top: (store.height - height) / 2,
width,
height,
});
失败(跨域 / 404 / 图片损坏)会 reject。
getCrop(targetSize, imageSize)
按 Cover 模式 + 居中 的方式算出裁剪参数,让原图铺满目标区域而不留白。常用于"生成封面 / 缩略图"。
import { getCrop } from '@ydesign/react-editor/utils/image';
const targetSize = { width: 1080, height: 1080 }; // 正方形
const imageSize = { width: 3000, height: 2000 }; // 横图
const crop = getCrop(targetSize, imageSize);
// => { cropX, cropY, width, height } 像素值
// 用于 Fabric image 对象
imageElement.set({
cropX: crop.cropX,
cropY: crop.cropY,
width: crop.width,
height: crop.height,
});
文件 / Blob 转换
localFileToURL(file: File): Promise<string> · @ydesign/react-editor/utils/file
读取一个本地 File,返回一个 base64 data URL。等价于"读本地图片后塞到 <img src>"。
import { localFileToURL } from '@ydesign/react-editor/utils/file';
const [file] = event.target.files;
const dataUrl = await localFileToURL(file);
store.addElement({ type: 'image', src: dataUrl });
⚠️ 内置的 Upload 面板 默认就是用这个函数,会让 JSON 体积膨胀。生产环境请用 setUploadFunc 把上传行为改造成"传到你自己的对象存储,只存短 URL"。
@ydesign/react-editor/utils/blob
import { dataURLtoBlob, blobToDataURL, blobToDataURLAsync } from '@ydesign/react-editor/utils/blob';
// base64 DataURL → Blob(用于上传 / 下载)
const blob = dataURLtoBlob('data:image/png;base64,iVBOR...');
// Blob → DataURL(回调版)
blobToDataURL(blob, dataUrl => console.log(dataUrl));
// Blob → DataURL(Promise 版,推荐)
const dataUrl = await blobToDataURLAsync(blob);
downloadFile(blob: Blob, fileName: string) · @ydesign/react-editor/utils/download
触发浏览器下载。内部会创建 <a> 标签并在下一个 tick 清理:
import { downloadFile } from '@ydesign/react-editor/utils/download';
const blob = await store.toBlob({ multiplier: 2, format: 'png' });
await downloadFile(blob, 'my-design.png');
💡
store.saveAsImage({ fileName: 'xxx.png' })内部也是调这个函数。如果你不需要额外定制,直接用saveAsImage即可。
svgToURL(svg: string): string · @ydesign/react-editor/utils/svg
把一段 SVG 字符串转成 data:image/svg+xml;base64,… URL,可直接扔进 <img src> 或 Fabric 的 image 元素。
import { svgToURL } from '@ydesign/react-editor/utils/svg';
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="#2253eb"/>
</svg>`;
store.addElement({ type: 'image', src: svgToURL(svg), left: 50, top: 50 });
适合"业务侧生成 SVG + 塞进画布"的场景,例如二维码、图表、动态徽章。
响应式 / 设备检测(@ydesign/react-editor/utils/screen)
内置移动端断点是 768px。<SidePanel /> / <Toolbar /> 等组件都基于它切换布局。
MOBILE_BREAKPOINT
import { MOBILE_BREAKPOINT } from '@ydesign/react-editor/utils/screen';
// => 768
isMobile() / isTouchDevice()
单次判断(不会订阅 resize):
import { isMobile, isTouchDevice } from '@ydesign/react-editor/utils/screen';
if (isMobile() || isTouchDevice()) {
// 简化手势提示…
}
useMobile(): boolean(React Hook)
响应式版本,组件会随 resize 自动重新渲染:
import { useMobile } from '@ydesign/react-editor/utils/screen';
function MyPanel() {
const mobile = useMobile();
return <div style={{ padding: mobile ? 8 : 24 }}>...</div>;
}
mobileStyle(cssRules: string)(styled-components 辅助)
在 styled-components 模板字符串里,生成"媒体查询 + .polotno-mobile 双触发"的样式片段。方便自定义组件也能跟随内置移动端切换:
import styled from 'styled-components';
import { mobileStyle } from '@ydesign/react-editor/utils/screen';
const Panel = styled.div`
padding: 24px;
${mobileStyle(`
padding: 8px;
font-size: 14px;
`)}
`;
内容辅助(@ydesign/react-editor/utils)
一组从 src/utils/index.ts 导出的小工具,内部面板会用到。
convertFillToPickerValue(fill)
把 Fabric 的 fill 属性(可能是字符串、也可能是渐变对象)转换为 Antd ColorPicker 能识别的格式。
import { convertFillToPickerValue } from '@ydesign/react-editor/utils';
convertFillToPickerValue('#ff6a00');
// => '#ff6a00'
convertFillToPickerValue({
type: 'linear',
coords: { x1: 0, y1: 0, x2: 1, y2: 0 },
colorStops: [
{ offset: 0, color: '#ff6a00' },
{ offset: 1, color: '#2253eb' },
],
});
// => [{ color: '#ff6a00', percent: 0 }, { color: '#2253eb', percent: 100 }]
自定义颜色选择器面板时用它做适配即可。
getRandomColor(): string
生成随机十六进制颜色,用于"新增元素时给个默认不同的颜色":
import { getRandomColor } from '@ydesign/react-editor/utils';
store.addElement({
type: 'rect',
width: 200,
height: 200,
fill: getRandomColor(),
});
getJSONFontFamily(objects): FONT[]
递归扫描 Fabric 对象数组(包含 group),提取所有用到的 fontFamily,返回去重后的列表。
import { getJSONFontFamily } from '@ydesign/react-editor/utils';
const fontsUsed = getJSONFontFamily(json.objects);
// => [{ fontFamily: 'Inter' }, { fontFamily: 'Noto Sans SC' }]
store.loadJSON(...) 内部正是用它把模板依赖的字体自动塞进 store.fonts,保证字体按需加载。自定义加载逻辑时可以复用。
isVectorShape(obj): boolean
判断一个 Fabric 对象是不是矢量图形(ellipse / triangle / rect / line / circle / polygon / polyline / path 其中之一)。
import { isVectorShape } from '@ydesign/react-editor/utils';
if (isVectorShape(el)) {
store.set({ stroke: '#000', strokeWidth: 2 }, el);
}
多语言 & 字体 & 加载器
这几个模块是"全局配置"性质的,完整文档请看:
- 多语言 ——
setTranslations/getTranslations/translate/t - 字体管理 ——
addGlobalFont/removeGlobalFont/replaceGlobalFonts/isFontLoaded/loadFont/injectCustomFont/injectGoogleFont/setGoogleFontsVariants/getGoogleFontsVariants/getGoogleFontsUrl - 资源加载超时 ——
setAssetLoadTimeout/setFontLoadTimeout/setFontLoadTimeoutCallback - 后端 API ——
setBaseURL/getBaseURL/setAPI/URLS
一个完整的综合示例
下面这段代码把 utils 里大多数函数串了一遍,模拟"用户粘贴一段 SVG / 上传图片 / 按 A4 尺寸重置画布 + 导出 PNG"的完整流程:
import { observer } from 'mobx-react-lite';
import { Button } from 'antd';
import { unitToPx } from '@ydesign/react-editor/utils/unit';
import { getImageSize, getCrop } from '@ydesign/react-editor/utils/image';
import { localFileToURL } from '@ydesign/react-editor/utils/file';
import { svgToURL } from '@ydesign/react-editor/utils/svg';
import { downloadFile } from '@ydesign/react-editor/utils/download';
import { useMobile } from '@ydesign/react-editor/utils/screen';
const DemoToolbar = observer(({ store }) => {
const mobile = useMobile();
// 1) 重置画布为 A4
const resetA4 = () => {
store.setUnit({ unit: 'mm', dpi: 300 });
store.setSize({
width: Math.round(unitToPx({ unitVal: 210, unit: 'mm', dpi: 300 })),
height: Math.round(unitToPx({ unitVal: 297, unit: 'mm', dpi: 300 })),
});
};
// 2) 选一张本地图片作为满铺背景
const addCoverImage = async (file: File) => {
const src = (await localFileToURL(file)) as string;
const { width, height } = await getImageSize(src);
const crop = getCrop({ width: store.width, height: store.height }, { width, height });
store.addElement({
type: 'image',
src,
left: 0,
top: 0,
width: store.width,
height: store.height,
cropX: crop.cropX,
cropY: crop.cropY,
});
};
// 3) 程序化生成一个角标 SVG 贴到画布
const addBadge = () => {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="#2253eb"/>
<text x="50" y="56" text-anchor="middle" fill="#fff" font-size="22">NEW</text>
</svg>`;
store.addElement({ type: 'image', src: svgToURL(svg), left: 40, top: 40, width: 100, height: 100 });
};
// 4) 导出 PNG 并下载
const exportPng = async () => {
const blob = await store.toBlob({ multiplier: 2, format: 'png' });
await downloadFile(blob, 'design.png');
};
return (
<div style={{ display: 'flex', gap: 8, padding: mobile ? 8 : 16 }}>
<Button onClick={resetA4}>重置为 A4</Button>
<input type="file" onChange={e => e.target.files?.[0] && addCoverImage(e.target.files[0])} />
<Button onClick={addBadge}>贴个角标</Button>
<Button type="primary" onClick={exportPng}>
导出 PNG
</Button>
</div>
);
});
为什么要用这些而不是自己写?
| 自己实现 | 用 utils |
|---|---|
pxToUnit 算错 pt/mm/in 的换算率 | 对齐内置标尺、尺寸面板的显示 |
getImageSize 忘了加 crossOrigin → 导出时 taint canvas | 默认带 anonymous,不踩坑 |
| 各种手写 FileReader → 多次写多次出错 | blobToDataURLAsync / localFileToURL 一行完事 |
自己监听 resize + 750 之类的断点 | MOBILE_BREAKPOINT=768,和框架内组件完全一致 |
在自定义 Section / Toolbar / Tooltip 时,先看看 utils 里有没有现成的,通常能省掉一小时。