Polotno-node
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!
Usage
npm install polotno-node
const fs = require('fs');
const { createInstance } = require('polotno-node');
async function run() {
const instance = await createInstance({
key: 'nFA5H9elEytDyPyvKL7T',
});
const json = JSON.parse(fs.readFileSync('polotno.json'));
const imageBase64 = await instance.jsonToImageBase64(json);
fs.writeFileSync('out.png', imageBase64, 'base64');
instance.close();
}
run();
API
createInstance(options)
Create working instance of Polotno Node.
const { createInstance } = require('polotno-node');
const instance = await createInstance({
key: 'nFA5H9elEytDyPyvKL7T',
useParallelPages: false,
url: 'https://yourappdomain.com/client',
browser: browser,
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();
}
},
});
instance.jsonToDataURL(json, attrs)
Export json into data URL.
const json = JSON.parse(fs.readFileSync('polotno.json'));
const url = await instance.jsonToDataURL(json);
res.json({ url });
for (const page of json.pages) {
const url = await instance.jsonToDataURL(
{ ...json, pages: [page] },
{ pageId: page.id }
);
}
instance.jsonToImageBase64(json, attrs)
Export json into base64 string of image.
const json = JSON.parse(fs.readFileSync('polotno.json'));
const imageBase64 = await instance.jsonToImageBase64(json, {
mimeType: 'image/png',
});
fs.writeFileSync('out.png', imageBase64, 'base64');
for (const page of json.pages) {
const imageBase64 = await instance.jsonToImageBase64(
{ ...json, pages: [page] },
{ pageId: page.id }
);
}
instance.jsonToPDFBase64(json, attrs)
Export json into base64 string of pdf file.
const json = JSON.parse(fs.readFileSync('polotno.json'));
const pdfBase64 = await instance.jsonToPDFBase64(json);
fs.writeFileSync('out.pdf', pdfBase64, 'base64');
instance.jsonToPDFDataURL(json, attrs)
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 });
instance.jsonToGIFDataURL(json, attrs)
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 });
instance.jsonToGIFBase64(json, attrs)
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');
attrs usage
NOTE: all export API will pass attrs object into relevant export function from store.
const url = await instance.jsonToDataURL(json, { pixelRatio: 0.2 });
attrs.assetLoadTimeout
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 });
attrs.fontLoadTimeout
Timeout for loading fonts. By default it is 6000ms.
const url = await instance.jsonToPDFDataURL(json, { fontLoadTimeout: 10000 });
attrs.htmlTextRenderEnabled
Enabled experimental HTML text rendering. By default it is false.
const url = await instance.jsonToPDFDataURL(json, {
htmlTextRenderEnabled: true,
});
attrs.textVerticalResizeEnabled
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,
});
attrs.textOverflow
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',
});
attrs.textSplitAllowed
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,
});
instance.run()
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.
const url = await instance.run(async (json) => {
window.config.addGlobalFont({
name: 'MyCustomFont',
url: 'https://example.com/font.otf',
});
store.loadJSON(json);
await store.waitLoading();
return store.toDataURL();
}, json);
window.config usage
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) => {
window.config.unstable_setTextVerticalResizeEnabled(true);
store.loadJSON(json);
return store.toDataURL();
}, json);
Your own client
By 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',
});
Usage on the cloud
AWS EC2
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 | bash
Got it from here: https://github.com/puppeteer/puppeteer/issues/765#issuecomment-353694116
Browserless usage
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.
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',
});
Minimal usage
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-min
const { createInstance } = require('polotno-node/instance');
const chromium = require('@sparticuz/chromium-min');
const puppeteer = require('puppeteer-core');
const makeInstance = async () => {
const browser = await puppeteer.launch({
args: [
...chromium.args,
'--no-sandbox',
'--hide-scrollbars',
'--disable-web-security',
'--allow-file-access-from-files',
'--disable-dev-shm-usage',
],
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();
AWS lambda with Layer configuration
In order to run the polotno-node in AWS Lambda, it's recommended to use a Lambda Layer configurations which will manage the dependencies, like chronium.
In addition, the Lambda's configuration needs to be changed as well. It's recommeded to change the timeout to 30 seconds instead of default value (3 seconds) and Memory limit increase to minimum 512 Mb instead of 128 mb ( default value ).
Dependencies:
- @sparticuz/chromium
- puppeteer-core
- polotno-node
Pre-requirements:
- The chronium and puppeteer versions need to be satisfied. Please check this document.
- The minumum Memory limit needs to be
512 Mb for AWS Lambda. Default is 128 Mb.
- The timeout should be increased from default 3 seconds to higher value, for example up to 30 seconds. It depends on the Memory limit. For instance, if the memory limit was set to 1024, the polotno process might take ~15 secs, if 512 mb, ~30sec.
==========================================
How to create a configuration layer with a dependency on chronium ?
- Create a
.zip file from a chronium project. For example:
mkdir chronium-112 && cd chronium-112
npm init -y
npm install @sparticuz/chromium@112.0
zip -r chronium.zip ./*
- Go to AWS console then open a Lambda section and click on
Layers.
- Following the documentation create a Layer with a chrnioum dependency by uploading a zip file. Keep in mind that environment like
nodejs18.x should match between layer and function.
Approximatelly, the size of the zip, will be around 73 Mb, so in order to upload, the S3 service will be needed. 4. Finally, open the Lambda function, select a Code section, at the bottom click on Add Layer and select a created layer.
==========================================
How to run the full example with aws-cli ?
1. Create a Lambda configuration layer
Create a Lambda configuration layer, and upload a .zip archive which stores a chronium npm or git package.
export CH_VERSION=112.0
export BUCKET=example-bucket
export AWS_ACC_ID=000000000000
# create a chronium project for Layer
mkdir chronium-layer
cd chronium-layer
npm init -y
npm install @sparticuz/chromium@${CH_VERSION}
zip -r chronium.zip ./*
# upload a Layer to S3
aws s3 cp chronium.zip s3://${BUCKET}/lambda-layers/chronium-${CH_VERSION}.zip
# publish a Layer so it can be used in Lambda
aws lambda publish-layer-version \
--layer-name chronium-polotno \
--content "S3Bucket=${BUCKET},S3Key=lambda-layers/chronium-{CH_VERSION}.zip" \
--description Chronium-dev-${CH_VERSION} \
--compatible-architectures x86_64 \
--compatible-runtimes nodejs18.x
2. Create a JS file for a Lambda's handler
Init a node project
# create a folder
mkdir handler && cd handler
# init a node project
npm init -y
# install dependencies
npm install --save polotno-node puppeteer-core@19.8.0
Create a handler
# create a file handler
touch index.mjs
# add code example
echo "
import chromium from "@sparticuz/chromium";
import puppeteer from "puppeteer-core";
import { createInstance } from "polotno-node";
export const handler = () => {
const browser = await puppeteer.launch({
args: [
...chromium.args,
"--no-sandbox",
"--hide-scrollbars",
"--disable-web-security",
"--allow-file-access-from-files",
"--disable-dev-shm-usage",
],
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: true,
ignoreHTTPSErrors: true,
});
const polotnoInstance = await createInstance({
key: process.env.POLOTNO_API_KEY,
browser,
});
const body = polotnoInstance.jsonToImageBase64(event.json)
return {
StatusCode: 200,
headers: {
'Content-Type': 'image/png'
},
body,
}
};
" >> index.mjs
Highlighted code example from the command above:
import chromium from "@sparticuz/chromium";
import puppeteer from "puppeteer-core";
import { createInstance } from "polotno-node";
export const handler = () => {
const browser = await puppeteer.launch({
args: [
...chromium.args,
"--no-sandbox",
"--hide-scrollbars",
"--disable-web-security",
"--allow-file-access-from-files",
"--disable-dev-shm-usage",
],
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: true,
ignoreHTTPSErrors: true,
});
const polotnoInstance = await createInstance({
key: process.env.POLOTNO_API_KEY,
browser,
});
const body = polotnoInstance.jsonToImageBase64(event.json)
return {
StatusCode: 200,
headers: {
'Content-Type': 'image/png'
},
body,
}
};
Create file with variables
echo '{
"Variables": {
"POLOTNO_API_KEY": "your-api-key"
}
}
' >> vars.json
# archive the folder
zip -r index.zip ./*
# copy to S3
aws s3 cp index.zip s3://${BUCKET}/polotno-handler.zip
Crete a trust policy
echo '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
' >> trustPolicy.json
Create roles and policies in AWS
aws iam create-role --role-name lambda-polotno-role --assume-role-policy-document file://trustPolicy.json
aws iam attach-role-policy --role-name lambda-polotno-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws lambda create-function --function-name lambda-polotno \
--runtime nodejs18.x \
--zip-file fileb://index.zip \
--layers "arn:aws:lambda:eu-central-1:${AWS_ACC_ID}:layer:chronium-polotno:1" \
--handler index.handler \
--environment file://vars.json \
--role "arn:aws:iam::${AWS_ACC_ID}:role/lambda-polotno-role" \
--timeout 60 \
--memory-size 512
// try it
aws lambda invoke --function-name lambda-polotno --cli-binary-format raw-in-base64-out --payload file://polotno-project-json.json image
AWS Lambda fonts issue
If you have faced an issue Timeout for loading font <font name>, please proceed to the next solution.
Lambda functions does not include any fonts by default, so we need to provide basic fonts (Arial and Times or their analogs) to make sure Polotno fonts functionality is working correctly.
- Create a
fonts folder in the root of your handler project.
mkdir fonts
-
Put the Arial.ttf and Times.ttf files into the fonts folder. 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.ttf and LiberationSans-Regular.ttf inside fonts folder.
- Create file
fonts.conf inside fonts folder. 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>
-
Upload your Lambda function as usual, fonts will be loaded automatically.
Troubleshooting
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": {
"includeFiles": "node_modules/polotno-node/**"
}
}
}