Recommended Free Tools
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Node.js PDFKit does not render arbitrary HTML and CSS directly. It is an imperative PDF-generation library: you create a PDFDocument, draw text and graphics, place images, add links, and finish the stream. To convert HTML, parse or template a deliberately supported subset of elements, then map those nodes to PDFKit calls. If you need browser-level CSS layout or client-side JavaScript, use a browser renderer or an HTML-to-PDF service instead.
What PDFKit can—and cannot—convert
The Node package named pdfkit is designed for programmatic PDF creation, not as an HTML engine. Its documented primitives cover text, images, links, vector drawing and SVG paths. There is no official function that accepts an arbitrary HTML document and reproduces the browser’s layout.
That distinction determines your implementation:
- Controlled templates: build a small HTML-like input format and map each supported node to PDFKit methods.
- Modern CSS: use a browser-based renderer when flexbox, grid, complex pagination, web fonts or CSS counters must match a browser.
- Client-side applications: choose a renderer that executes JavaScript when charts or components are created in the browser.
A separate Ruby project also called PDFKit wraps wkhtmltopdf and accepts HTML, URLs or files. Its PDFKit.new(...).to_pdf examples are not applicable to the Node pdfkit package.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchGenerate a PDF with PDFKit in Node.js
Install the package
npm install pdfkit
Create and save a document
const fs = require('node:fs');
const { PDFDocument } = require('pdfkit');
const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(fs.createWriteStream('output.pdf'));
doc.fontSize(18).text('Invoice');
doc.fontSize(11).text('Rendered from a supported HTML template.');
doc.end();
PDFDocument instances are readable Node streams. Piping the document to a file (or an HTTP response), adding content, and calling doc.end() finalizes the PDF. Omitting doc.end() leaves the output incomplete.
#1 Best Overall
Stream to an HTTP response
app.get('/invoice.pdf', (req, res) => {
res.setHeader('Content-Type', 'application/pdf');
const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(res);
doc.fontSize(18).text('Invoice');
doc.fontSize(11).text('Generated on demand.');
doc.end();
});
The same pattern works in any Node HTTP framework: set the PDF content type, pipe to the response, write content, then end the document.
Map a supported HTML subset to PDFKit
A practical converter separates parsing from layout. Parse trusted HTML with the parser of your choice, walk the resulting tree, and implement only the tags and attributes your documents require. Keep unsupported CSS visible in your documentation rather than silently promising browser fidelity.
1. Define the supported elements
A common first version supports headings, paragraphs, unordered lists, images, anchors and explicit page breaks. Convert inline formatting to font changes only where you can restore the previous state reliably. Treat styles such as grid, flexbox, floats, sticky positioning, media queries and CSS animations as unsupported unless you implement them yourself.
2. Track layout state
Maintain the current cursor, left and right margins, line height, active font and page number. PDFKit wraps text when you provide a width, but your renderer still needs to decide when a block starts, how much vertical space it consumes and when to call doc.addPage(). For long paragraphs, use PDFKit’s measured height or let its text flow while reserving space for headers and footers.
3. Render text and headings
function renderHeading(doc, text, level) {
const sizes = { 1: 22, 2: 16, 3: 13 };
doc.moveDown(0.6);
doc.fontSize(sizes[level] || 12).text(text, { width: 500 });
doc.moveDown(0.25);
}
function renderParagraph(doc, text) {
doc.fontSize(11).text(text, { width: 500, lineGap: 3 });
doc.moveDown(0.4);
}
This example deliberately handles plain text. A production renderer should decode entities, normalize whitespace, enforce a maximum block width and sanitize any user-controlled content before parsing it.
4. Resolve images
Resolve an image source to a local filename, a buffer or a data URL, then call doc.image. Decide whether remote URLs are allowed; downloading arbitrary URLs introduces latency and server-side request risks. Supply width or height constraints so a large source image cannot exceed the page.
doc.image(imageBuffer, {
fit: [500, 320],
align: 'center',
valign: 'center'
});
5. Turn links into clickable areas
Render the anchor’s visible text and calculate the rectangle occupied by that text before calling doc.link(x, y, width, height, href). Multi-line links require one rectangle per line or a more sophisticated text-layout pass. Do not expose untrusted schemes such as javascript:; allow only the URL schemes your application needs.
6. Fonts and page breaks
Register and embed fonts when the output must preserve a particular typeface or non-Latin characters. Add a page before a block that cannot fit, and provide an explicit page-break element in your input model for invoices, reports or chapters. Headers and footers should be rendered from a page event or a wrapper that knows the page dimensions.
Complete miniature HTML-to-PDF renderer
The following example accepts a small, predictable set of tags. It is intentionally not a general browser replacement; expand the switch only when you can test its pagination and security behavior.
const fs = require('node:fs');
const PDFDocument = require('pdfkit');
function textOf(node) {
if (node.type === 'text') return node.data;
return (node.children || []).map(textOf).join(' ');
}
function renderNode(doc, node) {
if (node.type === 'text') {
doc.fontSize(11).text(node.data, { width: 500, lineGap: 3 });
return;
}
const children = node.children || [];
switch (node.name) {
case 'h1':
doc.moveDown(0.6).fontSize(22).text(textOf(node), { width: 500 }).moveDown(0.3);
break;
case 'h2':
doc.moveDown(0.5).fontSize(16).text(textOf(node), { width: 500 }).moveDown(0.2);
break;
case 'p':
doc.fontSize(11).text(textOf(node), { width: 500, lineGap: 3 }).moveDown(0.4);
break;
case 'br':
doc.moveDown(1);
break;
case 'ul':
for (const li of children.filter(child => child.name === 'li')) {
doc.fontSize(11).text('• ' + textOf(li), { width: 480, indent: 20 });
}
doc.moveDown(0.4);
break;
default:
for (const child of children) renderNode(doc, child);
}
}
const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(fs.createWriteStream('output.pdf'));
// Feed renderNode() nodes produced by your HTML parser here.
renderNode(doc, { name: 'h1', children: [{ type: 'text', data: 'Report' }] });
renderNode(doc, { name: 'p', children: [{ type: 'text', data: 'A supported paragraph.' }] });
doc.end();
In a real application, parse the HTML before calling renderNode, reject dangerous elements and attributes, and add tests for nested formatting, long words, images, links and page boundaries.
SVG from HTML
Simple paths
For basic SVG path data, PDFKit’s built-in path() API can draw the geometry directly. This is suitable when you control the paths and do not need the complete SVG document model.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Complete SVG fragments
For shapes, text and tspan, styling, colors, transforms and viewBox behavior, use the complementary svg-to-pdfkit package.
const SVGtoPDF = require('svg-to-pdfkit');
const svgMarkup = '<svg xmlns="http://www.w3.org/2000/svg" width="300" height="80">'
+ '<rect width="300" height="80" fill="#eee"/>'
+ '<text x="20" y="45">Hello SVG</text>'
+ '</svg>';
SVGtoPDF(doc, svgMarkup, 50, 120, { width: 500 });
Validate or sanitize SVG supplied by users. External references, embedded scripts and unexpectedly large dimensions should be rejected according to your threat model.
When PDFKit is the wrong renderer
| Requirement | Best fit | Reason |
|---|---|---|
| Controlled templates and deterministic drawing | PDFKit | Direct text, image, vector and stream APIs |
| Arbitrary modern CSS layout | Browser or API renderer | Browser layout engines handle CSS more completely |
| Client-side JavaScript charts or components | JavaScript-capable renderer | PDFKit does not execute page JavaScript |
| Small server bundle and direct streaming | PDFKit | No browser process is required |
| Rich SVG diagrams | svg-to-pdfkit or browser renderer |
Choose based on SVG complexity and fidelity needs |
The hosted pdfkitt API is a separate service choice: its documented POST /v1/convert accepts exactly one html or url field, page-size and margin options, and an optional javascript flag. Its documented rendering cap is 30 seconds. Those options do not belong to the Node PDFKit library.
Or skip the browser setup
If your real goal is a clean screenshot or PDF of a live page rather than a hand-mapped PDFKit document, ScreenshotNeo provides a single-call API. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and the response reports the page verdict and billing status in headers. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.
Read the parameter reference in the ScreenshotNeo documentation. cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo includes full-page capture, element selectors, device presets, PDF page options, custom CSS and JavaScript, waits, request blocking, cookies and headers, geolocation, caching, signed links, asynchronous webhooks, bulk capture and a usage API on every plan. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshooting PDFKit conversions
The file is empty or corrupt
Confirm that the document is piped to a writable destination and that doc.end() is called exactly once after all content is added. Handle stream errors so a failed destination is not mistaken for a successful PDF.
Text overlaps or runs off the page
Give text a width, use measured heights for blocks, and track margins after every image or paragraph. Add page-break logic before content that cannot fit and test with long unbroken words.
Images do not appear
Check that the source resolves to a readable filename, buffer or data URL. Remote downloads must complete before doc.image runs; verify format support and constrain oversized images.
Rank #4
SVG styling is missing
Use path() only for simple paths. For complete fragments, pass valid SVG XML to svg-to-pdfkit and verify transforms, fonts and viewBox dimensions.
HTML looks different from the browser
That is expected when the input relies on unsupported CSS or JavaScript. Reduce the template to your documented subset, implement the missing layout deliberately, or switch to a browser-capable renderer.
The wrong PDFKit package was installed
Check whether your code is Node.js pdfkit or the Ruby wrapper around wkhtmltopdf. Their APIs and rendering models are unrelated.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minuteOperational checklist
- Pin and document the Node and package versions used by your build.
- Sanitize HTML, URLs, images and SVG when any input is user-controlled.
- Set download limits and timeouts for remote assets.
- Test page breaks, embedded fonts, links, images, RTL or non-Latin text, and large documents.
- Stream output where possible and monitor destination errors.
- Choose PDFKit for controlled drawing; choose a browser renderer for browser fidelity.
Frequently Asked Questions
Does PDFKit accept an HTML string directly?
The Node PDFKit package does not provide an official arbitrary-HTML input method. Parse a supported subset and map it to PDFKit calls, or use a browser-based renderer.
Can PDFKit run the JavaScript in my web page?
No. Client-rendered charts and components require a renderer that executes page JavaScript.
How do I preserve a custom font?
Register and embed the font in PDFKit, then test the required characters and fallback behavior.
Is Ruby PDFKit the same as Node pdfkit?
No. Ruby PDFKit wraps wkhtmltopdf; Node pdfkit is an imperative PDF-generation library.
Free tools Windows power users keep installed
One-click scans. No signup required.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

