npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

polotno-node

v3.9.0

Published

Polotno workflow from NodeJS

Readme

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
import fs from 'fs';
import { createInstance } from '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();

API

createInstance(options)

Create working instance of Polotno Node.

import { createInstance } from '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'],

  // executablePath - launch this browser binary instead of the one we pick,
  // e.g. the Chrome installed in your container. Ignored if you pass `browser`.
  executablePath: '/usr/bin/google-chrome-stable',

  // headless - which headless mode to launch. By default, where we pick the
  // browser ourselves, that is puppeteer's `chrome-headless-shell`; set `true`
  // for full Chrome. Ignored if you pass `browser`.
  headless: true,

  // useFontCache - cache Google Fonts responses (in memory + on disk in the
  // OS temp folder, shared across processes) and serve them to every page.
  // Every page starts with an empty HTTP cache, so without it each render
  // re-downloads the same fonts and bursts of parallel renders may get
  // throttled by Google Fonts (fonts time out and text falls back to a
  // default font). Default is true.
  useFontCache: true,

  // 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();
    }
  },
});

createBrowser(options)

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.

import { createBrowser, createInstance } from '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.

instance.jsonToDataURL(json, attrs)

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
}

instance.jsonToImageBase64(json, attrs)

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
}

instance.jsonToPDFBase64(json, attrs)

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');

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');

instance.jsonToVideoDataURL(json, attrs)

Export json into data URL of MP4 video file with animations.

const json = JSON.parse(fs.readFileSync('polotno.json'));

const url = await instance.jsonToVideoDataURL(json, {
  fps: 30,
  pixelRatio: 1,
  onProgress: (progress, frameTime) => {
    console.log(`Progress: ${progress}%`);
  },
});
res.json({ url });

instance.jsonToVideoBase64(json, attrs)

Export json into base64 string of MP4 video file with animations.

const json = JSON.parse(fs.readFileSync('polotno.json'));

const base64 = await instance.jsonToVideoBase64(json, {
  fps: 30,
  pixelRatio: 1,
});
fs.writeFileSync('out.mp4', base64, 'base64');

Video status and inactivity detection

Video export methods report status through the third argument of onProgress(fraction, frameTime, status). The first argument remains a number between 0 and 1. Existing handlers can ignore the third argument. Monitoring runs only when you provide onProgress. It does not cancel an export, close a browser, or retry a job.

await instance.jsonToVideoFile(json, 'out.mp4', {
  onProgress: (fraction, frameTime, status) => {
    console.log('Progress:', fraction, status);
    if (status?.state === 'stalled') {
      console.warn('No recent work progress:', status.stage, status.idleMs);
    }
  },
  // Example advisory budgets. Tune these from healthy exports on your hosts.
  stallTimeoutMs: {
    frames: 60_000,
    'audio-render': 180_000,
    finalize: 180_000,
  },
});

The third argument contains:

| Field | Meaning | | -------------------- | ---------------------------------------------------------------------------- | | state | running, stalled, completed, or failed | | stage | The last reported operation, listed in the next table | | completed, total | Work counters within the stage, when available | | elapsedMs | Time since the export passed design validation | | idleMs | Time since the last stage change, counter increase, or fraction increase | | heartbeatAgeMs | Time since the last tab heartbeat. Absent until the first heartbeat arrives. |

Stages describe these operations:

| Stage | Operation and counter unit | | --------------- | ----------------------------------------------------------------------------------------------------- | | prepare-media | Download and prepare remote media. Counts prepared sources. | | load-client | Open and configure the renderer page. | | load-design | Load the design and wait for its pages. Counts pages. | | setup | Initialize the video exporter and codecs. | | frames | Render and encode video frames. Counts frames. | | audio-decode | Load and decode audio. Counts processed sources, including sources the exporter skips after an error. | | audio-render | Mix audio through the native audio renderer. | | audio-encode | Send the mixed audio to the encoder. | | finalize | Finish the video container. | | transfer | Transfer the video to Node. Counts bytes written to the output file. | | postprocess | Inspect the output file and convert its audio codec when necessary. | | read-output | Read the file for a base64 or data-URL result. |

stallTimeoutMs accepts one number for every stage, or an object with overrides per stage. The default is 60000 ms, including stages without an override. A value of 0 disables stall notifications for that budget. The threshold is advisory. It is not a measured limit for healthy renders.

The monitor emits one stalled notification per inactivity period. A new stage, counter increase, or fraction increase emits running and starts a new inactivity period. Repeated counters, repeated fractions, and tab heartbeats do not reset the inactivity clock. Stage changes and stall notifications can repeat the last fraction. Successful completion reports 1. The last notification is completed or failed. Monitoring then stops, including after temporary-file cleanup. Callback errors are logged. Callback promises do not delay the export.

The tab sends a heartbeat approximately once per second during the browser export. A recent heartbeat means that the tab could run JavaScript, even if the export made no visible progress. A missing heartbeat can indicate blocked JavaScript or host contention. It does not prove a permanent stall. Native audio processing and finalization can also take time without intermediate work counters. Custom clients with older video exporters can report fewer stages.

The inactivity timer runs in Node, so it can report a blocked renderer without another CDP request. It cannot fire while Node itself is blocked, including during synchronous media conversion or base64 conversion. An independent parent-process monitor is necessary to detect that condition during the block. Existing protocolTimeout and navigation timeouts still apply.

A confirmed renderer crash rejects the active render without waiting for its CDP timeout. Protocol failures skip further renderer diagnostics, which could otherwise consume another full timeout. Failed-page cleanup does not delay rejection or replace the original error. It does not terminate the browser. With the default useParallelPages: true, a later export opens a new page if the browser remains connected. Retries remain the caller's decision; inactivity notifications alone do not trigger them.

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 });
// under the hood it will call:
// const url = await store.toDataURL({ pixelRatio: 0.2 });

Downscaling quality

Raster images displayed below their source size are automatically resampled with Magic Kernel Sharp during export — the resampler Facebook and Instagram use — including pixelRatio < 1 exports. For the best output, pass full-resolution image sources in the design and let the export perform the one reduction.

The kernel runs for elements up to 16 megapixels of output. A larger element — a print-size export at a mild reduction — takes the browser's own high-quality scaler instead, which keeps a 50-megapixel page in seconds rather than tens of seconds.

attrs.skipDownloads (video export only)

By default, when exporting video and only when using the default local client (i.e. you did not pass createInstance({ url })), polotno-node will:

  • Download all remote video elements (type: 'video') and json.audios sources into a temporary folder
  • Deduplicate downloads (same URL is downloaded once)
  • Probe downloaded remote videos with ffprobe
  • Conditionally normalize incompatible downloaded videos into MP4 with H.264 video, yuv420p, and AAC audio for better Chromium render compatibility
  • Rewrite src to a URL that polotno-node answers from the temporary folder

For best compatibility, use MP4 with H.264 (yuv420p) video and AAC audio.

If you want to disable this behavior, set:

const url = await instance.jsonToVideoDataURL(json, {
  skipDownloads: true,
});

When skipDownloads is enabled, polotno-node will skip both remote-media downloading and the conditional video preprocessing described above.

Local files

A file: source anywhere in a design — an image, an SVG, a video, an audio track, a font — is read by polotno-node and handed to the renderer, in every export format. skipDownloads does not affect this; it controls remote media only.

Because the bytes come from Node, a browser running somewhere else (browser: await puppeteer.connect(...), browserless) can render a design that names files on this machine.

Restricted media hosts

Server-side downloads block cloud-metadata addresses (e.g. 169.254.169.254) by default. Normal URLs, localhost, and private hosts still work. To restrict further:

await instance.jsonToVideoFile(json, 'out.mp4', {
  blockPrivateNetwork: true, // also block loopback / private IPs
  fetchGuard: (url) => isAllowed(url), // custom allow rule (checked per redirect hop)
});

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 });

An asset that FAILS — a reset connection, a DNS blip, a 5xx from the asset host — is retried automatically (three attempts, with backoff) before the render is failed, so a transient error no longer costs you the whole job. Nothing to turn on.

The timeout above is a separate thing: it covers a request that HANGS, which gets no retry. A stalled download is indistinguishable from a large image arriving slowly, and abandoning the second kind would break renders that work today, so the timeout stays a single deadline for the whole load.

attrs.fontLoadTimeout

Timeout for loading fonts. By default it is 6000ms.

const url = await instance.jsonToPDFDataURL(json, { fontLoadTimeout: 10000 });

attrs.legacyRichTextEnabled

Use the legacy (pre polotno 4) rich text renderer. By default it is false — the new text rendering engine is always on.

const url = await instance.jsonToPDFDataURL(json, {
  legacyRichTextEnabled: true,
});

attrs.textVerticalResizeEnabled

Vertical text resize and align. On by default — pass false to disable it.

const url = await instance.jsonToPDFDataURL(json, {
  textVerticalResizeEnabled: false,
});

attrs.skipFontError

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,
});

attrs.skipImageError

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.

// 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 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) => {
  // you can use global "config" object that has some functions from "polotno/config" module
  window.config.setTextVerticalResizeEnabled(true);
  // you can use global "store" object
  store.loadJSON(json);
  return store.toDataURL();
}, json);

Custom Browser Arguments

Using args export

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.

import { args } from 'polotno-node';

console.log(args);
// Will show the default arguments like:
// ['--disable-web-security', '--allow-file-access-from-files', '--disable-gpu', ...]

Platform compatibility: @sparticuz/chromium's args are tuned for the chromium binary that package ships, so polotno-node applies them only when it launches that binary — on Linux, when you did not pass an executablePath of your own. Any other browser (puppeteer's bundled Chromium on macOS and Windows, or one you supply) gets the polotno args alone. Keep the same pairing when you combine args by hand.

Using with custom browser

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):

import { createInstance, args } from 'polotno-node';
import chromium from '@sparticuz/chromium';
import puppeteer from 'puppeteer-core';

// Combine chrome.args (base defaults) with polotno-node's args (optimizations)
// The @sparticuz/chromium binary is Linux-only, so this pairing is too
const browser = await puppeteer.launch({
  args: [...chromium.args, ...args],
  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, where that binary runs.

Modifying default arguments

If you need to add, remove, or replace specific arguments (Linux):

import { createInstance, args } from 'polotno-node';
import chromium from '@sparticuz/chromium';
import puppeteer from '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'],
  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],
  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],
  executablePath: await chromium.executablePath(),
});

Note: These examples assume a Linux environment, where the @sparticuz/chromium binary and its args belong together.

Combining with browserArgs option

When using createInstance() or createBrowser(), you can provide additional arguments via browserArgs option. Internally, this combines the polotno args with your custom browserArgs, and adds chromium.args only when it launches the @sparticuz/chromium binary — that is, on Linux when you did not pass an executablePath of your own. Those flags are tuned for that binary and are not applied to a browser you supply:

import { createInstance } from 'polotno-node';

const instance = await createInstance({
  key: 'your-key',
  browserArgs: ['--custom-arg', '--another-arg'],
});
// Merges: polotno args + browserArgs (+ chromium.args on the bundled binary)

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 **

import { createInstance } from 'polotno-node';

const instance = await createInstance({
  key: 'KEY',
  url: 'https://yourappdomain.com/client',
});

Usage on the cloud

AWS Lambda

polotno-node works with AWS Lambda out of the box. Here's a simple example:

import { createInstance } from '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.

AWS Lambda with Layers (Optional)

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:

  1. Create a .zip file from a chromium project:
mkdir chromium-112 && cd chromium-112

npm init -y
npm install @sparticuz/[email protected]

zip -r chromium.zip ./*
  1. Go to AWS console then open Lambda section and click on Layers.
  2. Following the documentation create a Layer with a chromium dependency by uploading a zip file. Keep in mind that environment like nodejs18.x should match between layer and function.

The size of the zip will be large, so you may need to use S3 to upload it.

  1. Finally, open the Lambda function, select a Code section, at the bottom click on Add Layer and 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],
    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,
  };
};

AWS Lambda fonts issue

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).

  1. Create a fonts folder in the root of your handler project.
mkdir fonts
  1. Put the Arial.ttf and Times.ttf files into the fonts folder. You can get them from your system fonts folder.

  2. Usage of fonts analogues is also possible:

    1. 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.
    2. 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>
  3. Upload your Lambda function as usual, fonts will be loaded automatically.

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.

// (!) loading from polotno-node/instance will not import puppeteer and chromium-min dependencies
import { createInstance } from 'polotno-node/instance';
import puppeteer from '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
});

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
import { createInstance } from 'polotno-node/instance';
// Import args from main entry point for optimal browser configuration
import { args } from 'polotno-node';
import chromium from '@sparticuz/chromium-min';
import puppeteer from 'puppeteer-core';

const makeInstance = async () => {
  const browser = await puppeteer.launch({
    // Combine chromium args with polotno-node's optimized args
    args: [...chromium.args, ...args],
    executablePath: await chromium.executablePath(
      'https://github.com/Sparticuz/chromium/releases/download/v110.0.1/chromium-v110.0.1-pack.tar',
    ),
    headless: true,
    ignoreHTTPSErrors: true,
  });

  return await createInstance({
    key: 'your-key',
    browser,
  });
};

const instance = await makeInstance();

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": {
      // remember to replace this line with your function name
      "includeFiles": "node_modules/polotno-node/**"
    }
  }
}

License

See LICENSE.md.

This package contains a compiled copy of the polotno editor in dist/. No dependency entry shows this, so a dependency scan will not reveal it. The editor obeys the same terms as this package.