@polotno/pdf-import
v0.6.0
Published
Convert PDFs into editable Polotno JSON designs with text, images, tables, and vector graphics in browsers and Node.js
Readme
@polotno/pdf-import — PDF to editable Polotno JSON
Turn a PDF back into an editable design.
Works without the editor. The converter returns plain Polotno design JSON.
Runtime: Browser and Node.js 20 or newer. Conversion runs locally.
Documentation · Try PDF to JSON in your browser
License: Non-production use is free, with no time limit. Production use requires a valid Polotno subscription. Read the license.
Most PDF libraries give you a wall of text. This one gives you the design back: text elements with their fonts, real tables with rows and cells, vector shapes, images, and page order. The output is Polotno JSON. You can edit it, and you can render it again.
No OCR. No flattened page image. It reads the PDF content stream directly.
| A text extractor returns | @polotno/pdf-import returns |
| ------------------------------ | --------------------------------------------------------------- |
| One string per page | Text elements with font, size, color, alignment and line height |
| The cell text in reading order | One table element with rows, columns and cells |
| Nothing, or a page screenshot | SVG paths, gradients, patterns and native rectangle shapes |
| A list of embedded image blobs | Images with position, rotation, crop, flip and border |
| Flat lines of text | Paragraphs, columns, bullet lists and rotated text |
Features
- Structure, not a text dump — It groups glyphs into lines, paragraphs, columns and blocks. Alignment (left, center, right, justified) and line height come from the geometry, not from a guess.
- Native tables — A ruled grid becomes one Polotno
tableelement with real rows and cells. Cell text and vertical alignment come from the source. A grid that is not a clean data table falls back to lines and text. - Bullet lists — A marker column beside a body column becomes one list element. Continuation lines stay with their own item.
- Rich text runs — Bold, italic, color and size changes inside one paragraph survive as styled runs. Paragraph indents survive as leading spaces.
- Vector art — Paths, linear and radial gradients, tiling patterns and clip paths become SVG elements. A single axis-aligned rectangle becomes a native figure with an editable fill and stroke.
- Fonts — It extracts the embedded font program and re-embeds it, or matches the font to the closest Google family. It handles small caps, letter spacing and CJK encodings. The Adobe CMap tables ship in the package.
- Images — It recovers position, rotation, mirror and crop from the transform matrix, and transcodes CMYK JPEGs. A stroked frame around an image folds into the border of that image.
- Effects that stay editable — A rasterized drop shadow becomes
shadowEnabledon the text element, so the shadow follows your edits. It also reads soft masks, blend modes and text used as a clipping path. - Curved text — It finds text set along a circular path and emits one curved text element.
- Invisible content stays invisible — It drops hidden optional-content (OCG) layers and OCR text layers, so the JSON matches what a PDF reader shows.
- Form fields — Widget annotation text imports with its own font, size and rotation.
- Browser and Node — One API, one package, no extra setup.
Install
npm install @polotno/pdf-importQuick start
Node.js:
import fs from 'fs';
import { pdfToJson } from '@polotno/pdf-import';
const pdfBuffer = fs.readFileSync('document.pdf');
const json = await pdfToJson({ pdf: pdfBuffer });Browser:
import { pdfToJson } from '@polotno/pdf-import';
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
const json = await pdfToJson({ pdf: await file.arrayBuffer() });
// Optional: load the design into a Polotno store.
store.loadJSON(json);
});What comes out
A Polotno design. Each page holds typed elements in paint order:
{
"width": 816, // US Letter in CSS pixels at 96 dpi
"height": 1056,
"unit": "px",
"dpi": 96,
"fonts": [{ "fontFamily": "Lexend", "url": "data:font/opentype;base64,..." }],
"pages": [
{
"id": "id_1",
"background": "#FFFFFF",
"children": [
{ "type": "image", "x": 315.8, "y": 143.8, "width": 183.3, "rotation": 0 },
{ "type": "text", "text": "Quarterly report", "fontSize": 32, "align": "center" },
{ "type": "figure", "subType": "rect", "fill": "#0B5FFF", "strokeWidth": 2 },
{ "type": "svg", "src": "data:image/svg+xml;...", "keepRatio": false },
{ "type": "table", "rows": 6, "cols": 4, "colWidths": [], "cells": [] }
]
}
]
}Every field is a normal Polotno property. Change the text, swap a color, move an element, then write the file out again.
Round trip
The same JSON goes back out through the Polotno export packages:
@polotno/pdf-export— back to PDF@polotno/svg-export— to SVG@polotno/html-export— to HTML@polotno/pptx-export— to PowerPoint
To give your users a canvas, load the JSON into the
Polotno editor with store.loadJSON(json).
Options
const json = await pdfToJson({
pdf: pdfBuffer,
// 'auto' (default): a font that resolves to a real Google family keeps its
// name and loads from Google Fonts. Custom fonts are embedded as data URIs.
// 'embed': every font is embedded. Google families are renamed "Name (PDF)".
// Highest fidelity, and it works offline.
// 'googleFontsMatch': nothing is embedded. An unknown font maps to the
// metrically closest Google family.
fontStrategy: 'auto',
// Multiplier for all geometry. The default 96/72 converts PDF points to CSS
// pixels, so a 1080x1350 Canva design comes back as 1080x1350, not
// 810x1012.5. The dpi scales with it, so the physical print size does not
// change. Pass 1 to keep raw PDF points.
scale: 96 / 72,
// Only for a PDF that asks for a password to open it.
password: 'secret',
// Called for each non-fatal problem. See "Warnings" below.
onWarning: (warning) => console.warn(warning.code, warning.details),
});Warnings
A PDF can be valid and still hold content that cannot convert. Without
onWarning these cases only reach console.warn. You then cannot tell a clean
result from one that dropped an image.
await pdfToJson({
pdf: pdfBuffer,
onWarning: (warning) => {
// warning.code is stable. Branch on it, not on warning.message.
if (warning.code === 'IMAGE_SKIPPED') {
// warning.details.reason: 'extract-failed' | 'no-source' | 'clipped-out'
// warning.details.pageIndex: zero-based page index
report(warning.details);
}
},
});Password-protected PDFs
A PDF that only restricts printing or copying imports normally. One that needs a
password to open takes password. Without it, or with the wrong one,
pdfToJson throws IMPORT_FAILED with a reason. You can then ask the user and
try again:
try {
return await pdfToJson({ pdf: pdfBuffer, password });
} catch (e) {
if (e.details?.reason === 'password-required') {
// ask for a password
}
if (e.details?.reason === 'password-incorrect') {
// ask again
}
throw e; // no reason: the bytes are not a readable PDF
}Documentation
Full guide and a live demo: PDF Import Guide.
License
See LICENSE.md.
