Skip to main content

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 / setAPI live 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, prefer saveAsImage.

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:

  • i18nsetTranslations / getTranslations / translate / t
  • Font managementaddGlobalFont / removeGlobalFont / replaceGlobalFonts / isFontLoaded / loadFont / injectCustomFont / injectGoogleFont / setGoogleFontsVariants / getGoogleFontsVariants / getGoogleFontsUrl
  • Asset load timeoutsetAssetLoadTimeout / setFontLoadTimeout / setFontLoadTimeoutCallback
  • Backend APIsetBaseURL / 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-rolledUse utils
Mess up the pt/mm/in conversion factorsMatches built-in rulers / size panel display
Forget crossOrigin on getImageSize → tainted canvas on exportAlways sets anonymous — no tripwire
Hand-written FileReader in N places, N bugsblobToDataURLAsync / localFileToURL — one line
Hand-rolled resize listeners with 750 or other breakpointsMOBILE_BREAKPOINT=768, aligned with built-in components

When building custom Sections / Toolbars / Tooltips, check utils/ first — it usually saves an hour.


Next