Export Polotno JSON into images and pdf files. NodeJS package to work with Polotno SDK.
🚀 Optimize Your Workflow with Cloud Render API!
Instead of managing your own server infrastructure with
polotno-node, consider using our Cloud Render API. It provides all the powerful export capabilities of Polotno with none of the server maintenance. Seamlessly convert your designs into images, PDFs, and videos at scale, with the reliability and speed of cloud-based rendering.Get started now and focus on what truly matters—creating stunning designs!
npm install polotno-nodeconst fs = require('fs');
const { createInstance } = require('polotno-node');
async function run() {
// create working instance
const instance = await createInstance({
// this is a demo key just for that project
// (!) please don't use it in your projects
// to create your own API key please go here: https://polotno.dev/cabinet
key: 'nFA5H9elEytDyPyvKL7T',
});
// load sample json
const json = JSON.parse(fs.readFileSync('polotno.json'));
const imageBase64 = await instance.jsonToImageBase64(json);
fs.writeFileSync('out.png', imageBase64, 'base64');
// close instance
instance.close();
}
run();Create working instance of Polotno Node.
const { createInstance } = require('polotno-node');
const instance = await createInstance({
// this is a demo key just for that project
// (!) please don't use it in your projects
// to create your own API key please go here: https://polotno.dev/cabinet
key: 'nFA5H9elEytDyPyvKL7T',
// useParallelPages - use parallel pages to speed up rendering
// you can use false only for sequential calls
// it may break rendering if you call many parallel requests
// default is true
useParallelPages: false,
// url - url of the Polotno Client Editor
// client editor is just simple public html page that have `store` as global variable
// by default it will run local build
url: 'https://yourappdomain.com/client',
// browser - puppeteer browser instance
// by default it will use chrome-aws-lambda
// useful to set your own rendering props or use browserless
browser: browser,
// browserArgs - additional browser arguments to append to default args
// see "Custom Browser Arguments" section for more details
browserArgs: ['--custom-arg'],
// requestInterceptor - optional function to intercept and modify network requests
// Useful when you need to:
// - Modify headers like User-Agent to access protected image resources
// - Add authentication tokens or credentials to requests
// - Log or monitor network traffic
requestInterceptor: (request) => {
const targetUrl = request.url();
if (/\.(png|jpe?g)(\?|$)/i.test(targetUrl)) {
console.log(`Modifying User-Agent for image request: ${targetUrl}`);
request.continue({
headers: {
...request.headers(),
'User-Agent': 'MyCustomApprovedAgent/1.0',
},
});
} else {
request.continue();
}
},
});Create a Puppeteer browser instance with optimized settings for Polotno rendering. This is useful when you want to create a browser separately from the instance.
const { createBrowser, createInstance } = require('polotno-node');
// Create a browser
const browser = await createBrowser({
browserArgs: ['--custom-arg'], // optional: additional browser arguments
// ... any other puppeteer.launch options
});
// Create instance with the browser
const instance = await createInstance({
key: 'your-key',
browser: browser,
});Note: createBrowser() automatically uses the optimized args for rendering. You can add custom arguments via the browserArgs parameter.
Export json into data URL.
const json = JSON.parse(fs.readFileSync('polotno.json'));
// by default it will export first page only
const url = await instance.jsonToDataURL(json);
res.json({ url });
// export many pages:
for (const page of json.pages) {
const url = await instance.jsonToDataURL(
{ ...json, pages: [page] }, // for optimization, we can modify JSON to include only one page
{ pageId: page.id }
);
// do something with url
}Export json into base64 string of image.
const json = JSON.parse(fs.readFileSync('polotno.json'));
// by default it will export first page only
const imageBase64 = await instance.jsonToImageBase64(json, {
mimeType: 'image/png',
}); // also 'image/jpeg' is supported
fs.writeFileSync('out.png', imageBase64, 'base64');
// export many pages:
for (const page of json.pages) {
const imageBase64 = await instance.jsonToImageBase64(
{ ...json, pages: [page] }, // for optimization, we can modify JSON to include only one page
{ pageId: page.id }
);
// do something with base64
}Export json into base64 string of pdf file.
const json = JSON.parse(fs.readFileSync('polotno.json'));
// it will export all pages in the JSON
const pdfBase64 = await instance.jsonToPDFBase64(json);
fs.writeFileSync('out.pdf', pdfBase64, 'base64');Export json into data url of pdf file.
const json = JSON.parse(fs.readFileSync('polotno.json'));
const url = await instance.jsonToPDFDataURL(json);
res.json({ url });Export json into data url of GIF file with animations
const json = JSON.parse(fs.readFileSync('polotno.json'));
const url = await instance.jsonToGIFDataURL(json);
res.json({ url });Export json into data url of GIF file with animations
const json = JSON.parse(fs.readFileSync('polotno.json'));
const base64 = await instance.jsonToGIFBase64(json);
fs.writeFileSync('out.gif', base64, 'base64');NOTE: all export API will pass attrs object into relevant export function from store.
const url = await instance.jsonToDataURL(json, { pixelRatio: 0.2 });
// under the hood it will call:
// const url = await store.toDataURL({ pixelRatio: 0.2 });You can add assetLoadTimeout attribute to attrs object. It will be used to set timeout for loading assets. By default it is 30000ms.
const url = await instance.jsonToPDFDataURL(json, { assetLoadTimeout: 60000 });Timeout for loading fonts. By default it is 6000ms.
const url = await instance.jsonToPDFDataURL(json, { fontLoadTimeout: 10000 });Enabled experimental HTML text rendering. By default it is false.
const url = await instance.jsonToPDFDataURL(json, {
htmlTextRenderEnabled: true,
});Enabled vertical text resize and align. By default it is false.
const url = await instance.jsonToPDFDataURL(json, {
textVerticalResizeEnabled: true,
});If skipFontError is true, it will not throw error font is not loaded or not defined. By default it is false, so it will throw error.
const url = await instance.jsonToPDFDataURL(json, {
skipFontError: true,
});If skipImageError is true, it will not throw error an can't be loaded. By default it is false, so it will throw error.
const url = await instance.jsonToPDFDataURL(json, {
skipImageError: true,
});Control behavior of text on its overflow. Default is change-font-size. It means it will automatically reduce font size to fit text into the box. Other options are:
resize(change text element height to make text fit)ellipsis(add ellipsis to the end of the text)
const url = await instance.jsonToPDFDataURL(json, {
textOverflow: 'resize',
});Additinal options to overflow behaviour. Default is false. It means the render will make sure no words are rendered into several lines. If you set it to true, the render will split words into several lines if needed without reducing font size.
const url = await instance.jsonToPDFDataURL(json, {
textSplitAllowed: true,
});Run any Polotno store API directly inside web-page context.
Warning: by default every run and every export function will create a new page with its own editor and context. If you want to make and export after you use instance.run() you must do it inside the same run function.
// we can't directly use "json" variable inside the run function
// we MUST pass it as the second argument
const url = await instance.run(async (json) => {
// you can use global "config" object that has some functions from "polotno/config" module
window.config.addGlobalFont({
name: 'MyCustomFont',
url: 'https://example.com/font.otf',
});
// you can use global "store" object
store.loadJSON(json);
await store.waitLoading();
return store.toDataURL();
}, json);window.config is a global object that has some functions from polotno/config module. You can use it to add custom fonts and customize some settings.
Not all options are supported yet. If you see anything missing, please create an issue. You can see all available options in client.js file.
You should be able to change config before you call store.loadJSON function and do you export.
const url = await instance.run(async (json) => {
// you can use global "config" object that has some functions from "polotno/config" module
window.config.unstable_setTextVerticalResizeEnabled(true);
// you can use global "store" object
store.loadJSON(json);
return store.toDataURL();
}, json);polotno-node exports a carefully curated set of Chrome arguments (args) that are optimized for server-side rendering. These arguments are automatically used when you call createInstance() without providing your own browser.
const { args } = require('polotno-node');
console.log(args);
// Will show the default arguments like:
// ['--disable-web-security', '--allow-file-access-from-files', '--disable-gpu', ...]Platform compatibility: Using @sparticuz/chromium args works best on Linux and macOS. On Windows, these args may not work as expected, so the library automatically skips them on Windows. When manually combining args, be aware of platform differences.
When you want to use your own browser instance (e.g., with browserless.io or custom puppeteer configuration), you should combine chrome.args (the base defaults) with polotno-node's args (additional optimizations):
const { createInstance, args } = require('polotno-node');
const chromium = require('@sparticuz/chromium');
const puppeteer = require('puppeteer-core');
// Combine chrome.args (base defaults) with polotno-node's args (optimizations)
// This works best on Linux/macOS
const browser = await puppeteer.launch({
args: [...chromium.args, ...args],
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: true,
ignoreHTTPSErrors: true,
});
const instance = await createInstance({
key: 'your-key',
browser,
});Note: polotno-node's args are designed to work on top of @sparticuz/chromium's args for optimal server-side rendering on Linux/macOS environments.
If you need to add, remove, or replace specific arguments (Linux/macOS):
const { createInstance, args } = require('polotno-node');
const chromium = require('@sparticuz/chromium');
const puppeteer = require('puppeteer-core');
// Add custom arguments on top of chrome.args and polotno args
const browser = await puppeteer.launch({
args: [...chromium.args, ...args, '--custom-arg', '--another-custom-arg'],
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
});
// Remove specific arguments from polotno args
const filteredArgs = args.filter((arg) => arg !== '--disable-gpu');
const browser2 = await puppeteer.launch({
args: [...chromium.args, ...filteredArgs],
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
});
// Replace specific arguments in polotno args
const customArgs = args.map((arg) =>
arg === '--disable-web-security' ? '--enable-web-security' : arg
);
const browser3 = await puppeteer.launch({
args: [...chromium.args, ...customArgs],
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
});Note: These examples assume Linux/macOS environment where chromium.args work properly.
When using createInstance() or createBrowser(), you can provide additional arguments via browserArgs option. Internally, this automatically combines chrome.args + args + your custom browserArgs:
const { createInstance } = require('polotno-node');
const instance = await createInstance({
key: 'your-key',
browserArgs: ['--custom-arg', '--another-arg'],
});
// Internally merges: chrome.args + polotno args + browserArgsBy default polotno-node ships with the default Polotno Editor with its (hopefully) last version. If you use experimental API such as unstable_registerShapeModel and unstable_registerShapeComponent, the rendering may fail if you use unknown elements types.
In that case you can use your own client editor. You need to create a public html page with store as global variable and mount just <Workspace /> component from polotno/canvas module. Take a look into client.html file and client.js file in this repo as a demo. In your own version of the Editor you can use experimental API to define custom components.
Pass url option to createInstance function with public url of your client editor.
**Note: you will have to maintain the last version of your client editor by yourself. Better to keep using the last **
const { createInstance } = require('polotno-node');
const instance = await createInstance({
key: 'KEY',
url: 'https://yourappdomain.com/client',
});polotno-node works with AWS Lambda out of the box. Here's a simple example:
const { createInstance } = require('polotno-node');
export const handler = async (event) => {
const instance = await createInstance({
key: process.env.POLOTNO_API_KEY,
});
const base64 = await instance.jsonToImageBase64(event.json);
await instance.close();
return {
statusCode: 200,
headers: {
'Content-Type': 'image/png',
},
body: base64,
};
};Important: For reliable performance, you may need to increase AWS Lambda limits:
- Memory: Increase from the default. For complex designs, you may need to set it to maximum.
- Timeout: Increase from the default. For large files, you may need to set it to maximum.
- Ephemeral Storage: May need to increase from the default for complex designs.
Without these increases, polotno-node may work on smaller files but will fail or timeout on larger files.
Full working example: See polotno-node-aws-lambda for a complete demo.
For advanced usage, you can use Lambda Layers to manage dependencies like chromium separately. This can help with deployment size and organization.
Dependencies:
- @sparticuz/chromium
- puppeteer-core
- polotno-node
Requirements:
- The chromium and puppeteer versions need to be compatible. Please check this document.
- The Memory limit needs to be increased from the default. You may need to set it to maximum for complex designs.
- The timeout should be increased from the default. You may need to set it to maximum for large files.
Creating a Lambda Layer with chromium:
- Create a
.zipfile from a chromium project:
mkdir chromium-112 && cd chromium-112
npm init -y
npm install @sparticuz/[email protected]
zip -r chromium.zip ./*- Go to AWS console then open Lambda section and click on
Layers. - Following the documentation create a Layer with a chromium dependency by uploading a zip file. Keep in mind that environment like
nodejs18.xshould match between layer and function.
The size of the zip will be large, so you may need to use S3 to upload it.
- Finally, open the Lambda function, select a
Codesection, at the bottom click onAdd Layerand select a created layer.
Handler code with custom chromium:
Create index.mjs:
import chromium from '@sparticuz/chromium';
import puppeteer from 'puppeteer-core';
import { createInstance, args } from 'polotno-node';
export const handler = async (event) => {
const browser = await puppeteer.launch({
// Combine chromium args with polotno-node's optimized args
// Works well on AWS Lambda (Linux environment)
args: [...chromium.args, ...args],
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: true,
ignoreHTTPSErrors: true,
});
const polotnoInstance = await createInstance({
key: process.env.POLOTNO_API_KEY,
browser,
});
const body = await polotnoInstance.jsonToImageBase64(event.json);
await polotnoInstance.close();
return {
statusCode: 200,
headers: {
'Content-Type': 'image/png',
},
body,
};
};Lambda functions do not include any fonts by default. If you encounter Timeout for loading font <font name> errors, you need to provide basic fonts (Arial and Times or their analogs).
Solution: Add fonts to your Lambda function
- Create a
fontsfolder in the root of your handler project.
mkdir fonts-
Put the
Arial.ttfandTimes.ttffiles into thefontsfolder. You can get them from your system fonts folder. -
Usage of fonts analogues is also possible:
- If you don't want to use system Arial and Times fonts, you can use Liberation Fonts as free alternative. Download fonts from repository. Put
LiberationMono-Regular.ttfandLiberationSans-Regular.ttfinsidefontsfolder. - Create file
fonts.confinsidefontsfolder. It should contain the following lines:
<?xml version="1.0"?> <!DOCTYPE fontconfig SYSTEM "fonts.dtd"> <fontconfig> <alias> <family>Arial</family> <prefer> <family>Liberation Sans</family> </prefer> </alias> <alias> <family>Times New Roman</family> <prefer> <family>Liberation Serif</family> </prefer> </alias> <dir>/var/task/fonts</dir> </fontconfig>
- If you don't want to use system Arial and Times fonts, you can use Liberation Fonts as free alternative. Download fonts from repository. Put
-
Upload your Lambda function as usual, fonts will be loaded automatically.
EC2 has some troubles with loading fonts. To fix the issue install Google Chrome, it will load all required libraries.
curl https://intoli.com/install-google-chrome.sh | bashGot it from here: puppeteer/puppeteer#765 (comment)
You can speed up your function execution a lot, if instead of using full browser you will use browserless.io service. It is a paid service not affiliated with Polotno.
Using browserless.io you can also make your function much smaller in size, so it will be possible to deploy to cloud provider with smaller limits, like Vercel.
// (!) loading from polotno-node/instance will not import puppeteer and chromium-min dependencies
const { createInstance } = require('polotno-node/instance');
const puppeteer = require('puppeteer');
const instance = await createInstance({
key: 'nFA5H9elEytDyPyvKL7T',
browser: await puppeteer.connect({
browserWSEndpoint: 'wss://chrome.browserless.io?token=API_KEY',
}),
url: 'https://yourappdomain.com/client', // see "Your own client" section
});Also you can use @sparticuz/chromium-min to reduce function size. Make sure it is caching chromium binary in your cloud provider. Looks like Vercel is NOT doing that!
npm install @sparticuz/chromium-minconst { createInstance } = require('polotno-node/instance');
// Import args from main entry point for optimal browser configuration
const { args } = require('polotno-node');
const chromium = require('@sparticuz/chromium-min');
const puppeteer = require('puppeteer-core');
const makeInstance = async () => {
const browser = await puppeteer.launch({
// Combine chromium args with polotno-node's optimized args
// Best for Linux/macOS environments
args: [...chromium.args, ...args],
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(
'https://github.com/Sparticuz/chromium/releases/download/v110.0.1/chromium-v110.0.1-pack.tar'
),
headless: chromium.headless,
ignoreHTTPSErrors: true,
});
return await createInstance({
key: 'your-key',
browser,
});
};
const instance = await makeInstance();If you have an error like this
Unhandled Promise Rejection {"errorType":"Runtime.UnhandledPromiseRejection","errorMessage":"Error: Evaluation failed: ReferenceError: store is not defined\n at **puppeteer_evaluation_script**:3:9"
It may mean that Polotno Client Editor was not loaded in puppeteer instance. It is possible that you are missing required files in node_modules folder. I got this error when I was trying to run polotno-node on Vercel. To fix the issue you need to add this config into vercel.json:
{
"functions": {
"api/render.js": {
// remember to replace this line with your function name
"includeFiles": "node_modules/polotno-node/**"
}
}
}