Utility Functions
@ydesign/react-editor accumulates a set of reusable utility functions over time. They're all exported from subpaths like @ydesign/react-editor/utils/* — import the ones you need.
These aren't here just to pad the surface area — every one of them is used internally by the built-in panels and toolbar components. When you build custom sections / toolbars, prefer these over hand-rolling the same thing: they match the editor's built-in behavior and stay in sync across future versions.
This page is organized by domain. Every signature comes from the real source at packages/react-editor/src/utils/.
💡 Global-config APIs like
setTranslations/addGlobalFont/setUploadFunc/setAPIlive in Editor Configuration. This page focuses on pure, side-effect-free utilities.
Unit conversion (@ydesign/react-editor/utils/unit)
The canvas stores everything in pixels. Business UI often needs to display or accept mm / cm / in / pt. This group converts between them.
pxToUnit({ px, unit, dpi })
Convert a pixel value to a target unit.
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
Supported unit values: 'pt' | 'mm' | 'cm' | 'in' | 'px' (pt and px are treated 1:1 today).
pxToUnitRounded({ px, unit, dpi, precious? })
Same as above but rounded to precious decimal places (default 2).
pxToUnitRounded({ px: 300, unit: 'mm', dpi: 72 }); // => 105.83
pxToUnitRounded({ px: 300, unit: 'mm', dpi: 72, precious: 0 }); // => 106
pxToUnitString(params)
Convert to a display string with unit suffix — drop straight into 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 })
Reverse — convert a value in a target unit back to pixels. Typical use: accepting user input.
import { unitToPx } from '@ydesign/react-editor/utils/unit';
// The user typed "210mm" for an A4 poster
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 })),
});
Image (@ydesign/react-editor/utils/image)
getImageSize(url): Promise<{ width, height }>
Async probe the natural dimensions of a remote / local image. Creates an Image with crossOrigin='anonymous' built in (so the result is safe for canvas export).
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,
});
Rejects on CORS / 404 / corrupted image.
getCrop(targetSize, imageSize)
Compute Cover-mode, center-cropped parameters so the image fills the target area with no letterboxing. Typical use: "generate cover / thumbnail".
import { getCrop } from '@ydesign/react-editor/utils/image';
const targetSize = { width: 1080, height: 1080 }; // square
const imageSize = { width: 3000, height: 2000 }; // wide original
const crop = getCrop(targetSize, imageSize);
// => { cropX, cropY, width, height } (pixels)
imageElement.set({
cropX: crop.cropX,
cropY: crop.cropY,
width: crop.width,
height: crop.height,
});
File / Blob conversion
localFileToURL(file: File): Promise<string> · @ydesign/react-editor/utils/file
Read a local File and resolve to a base64 data URL. Equivalent to "read this local image, stuff it into <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 });
⚠️ The built-in Upload panel uses this by default, which bloats the JSON. In production, override with setUploadFunc to upload to your own storage and store short URLs instead.
@ydesign/react-editor/utils/blob
import { dataURLtoBlob, blobToDataURL, blobToDataURLAsync } from '@ydesign/react-editor/utils/blob';
// base64 DataURL → Blob (for upload / download)
const blob = dataURLtoBlob('data:image/png;base64,iVBOR...');
// Blob → DataURL (callback flavor)
blobToDataURL(blob, dataUrl => console.log(dataUrl));
// Blob → DataURL (Promise, recommended)
const dataUrl = await blobToDataURLAsync(blob);
downloadFile(blob: Blob, fileName: string) · @ydesign/react-editor/utils/download
Trigger a browser download. Internally creates an <a> and cleans up on the next 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' })calls this under the hood. If you don't need extra customization, prefersaveAsImage.
svgToURL(svg: string): string · @ydesign/react-editor/utils/svg
Turn an SVG string into data:image/svg+xml;base64,…, usable directly in <img src> or as a Fabric image src.
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 });
Great for "generate SVG server-side or client-side, then drop onto canvas" use cases — QR codes, charts, dynamic badges.
Screen / device detection (@ydesign/react-editor/utils/screen)
The built-in mobile breakpoint is 768px. <SidePanel /> / <Toolbar /> all use this to flip layouts.
MOBILE_BREAKPOINT
import { MOBILE_BREAKPOINT } from '@ydesign/react-editor/utils/screen';
// => 768
isMobile() / isTouchDevice()
One-off checks (don't subscribe to resize):
import { isMobile, isTouchDevice } from '@ydesign/react-editor/utils/screen';
if (isMobile() || isTouchDevice()) {
// show compact gesture hints…
}
useMobile(): boolean (React hook)
Reactive version — the component re-renders when the window resizes:
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 helper)
Inside a styled-components template literal, emit a "media query + .polotno-mobile" double trigger. Lets your custom components track the built-in mobile switch:
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;
`)}
`;
Content helpers (@ydesign/react-editor/utils)
A handful of small utilities exported from src/utils/index.ts. Used internally across panels.
convertFillToPickerValue(fill)
Convert a Fabric fill property (string or gradient object) into a shape Antd ColorPicker can understand.
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 }]
Perfect glue when wiring a custom color picker panel.
getRandomColor(): string
Generate a random hex color — handy when "new elements should have a different default color each time":
import { getRandomColor } from '@ydesign/react-editor/utils';
store.addElement({
type: 'rect',
width: 200,
height: 200,
fill: getRandomColor(),
});
getJSONFontFamily(objects): FONT[]
Recursively walk a Fabric object array (including group descendants) and collect every fontFamily referenced. Dedup'd.
import { getJSONFontFamily } from '@ydesign/react-editor/utils';
const fontsUsed = getJSONFontFamily(json.objects);
// => [{ fontFamily: 'Inter' }, { fontFamily: 'Noto Sans SC' }]
store.loadJSON(...) uses this internally to populate store.fonts for on-demand loading. Reuse it for custom loaders.
isVectorShape(obj): boolean
Is a Fabric object a vector primitive (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);
}
i18n & fonts & loaders
These are global-config utilities documented elsewhere:
- i18n —
setTranslations/getTranslations/translate/t - Font management —
addGlobalFont/removeGlobalFont/replaceGlobalFonts/isFontLoaded/loadFont/injectCustomFont/injectGoogleFont/setGoogleFontsVariants/getGoogleFontsVariants/getGoogleFontsUrl - Asset load timeout —
setAssetLoadTimeout/setFontLoadTimeout/setFontLoadTimeoutCallback - Backend API —
setBaseURL/getBaseURL/setAPI/URLS
End-to-end example
The snippet below stitches most of these together, simulating "paste SVG / upload image / reset canvas to A4 + export 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) Reset canvas to 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) Pick a local image as a full-cover background
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) Stamp a programmatic SVG badge onto the canvas
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) Export as PNG and download
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}>Reset to A4</Button>
<input type="file" onChange={e => e.target.files?.[0] && addCoverImage(e.target.files[0])} />
<Button onClick={addBadge}>Stamp a badge</Button>
<Button type="primary" onClick={exportPng}>Export PNG</Button>
</div>
);
});
Why use these instead of rolling your own?
| Hand-rolled | Use utils |
|---|---|
Mess up the pt/mm/in conversion factors | Matches built-in rulers / size panel display |
Forget crossOrigin on getImageSize → tainted canvas on export | Always sets anonymous — no tripwire |
Hand-written FileReader in N places, N bugs | blobToDataURLAsync / localFileToURL — one line |
Hand-rolled resize listeners with 750 or other breakpoints | MOBILE_BREAKPOINT=768, aligned with built-in components |
When building custom Sections / Toolbars / Tooltips, check utils/ first — it usually saves an hour.