跳到主要内容

工具函数

@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'ptpx 当前按 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 里有没有现成的,通常能省掉一小时。


下一步