A lightweight, high-performance rendering API built on Puppeteer. This is the open-source version of the software powering html2pdfapi.com. The SaaS version includes additional features out of the box:
- Async support with a job queue for background processing
- Webhook notifications when jobs complete
- Direct S3 upload to your own bucket
- 📸 PNG, JPEG, WebP, GIF, AVIF screenshots from any URL or HTML
- 📄 PDF generation with full page layout control
- 🎬 MP4 video recording with smooth scroll animation
- 🌐 Full HTML capture with all resources embedded inline
- ⚡ Browser pool with configurable recycling and warmup
- 🔧 Structured JSON logging (Pino)
- 🐳 Docker-ready with CI pipeline
- Node.js 24+
- FFmpeg (for video recording)
npm install
npm run devThe server starts on http://localhost:3000. API docs are available at /docs.
For video support on macOS (Homebrew):
FFMPEG_PATH=/opt/homebrew/bin/ffmpeg npm run devPull from the GitHub Container Registry:
docker pull ghcr.io/carbogninalberto/fast-html-to-pdf-api:latest
docker run -p 3000:3000 ghcr.io/carbogninalberto/fast-html-to-pdf-api:latestOr build locally:
docker build . -t bakney/fastrender
docker run -p 3000:3000 bakney/fastrenderFor ARM-based machines (Apple Silicon):
docker build --platform linux/amd64 . -t render
docker run --platform linux/amd64 -p 3000:3000 render| Endpoint | Method | Description |
|---|---|---|
/render |
POST | Render URL or HTML to PDF/Image/Video/HTML |
/render?config=... |
GET | Same as above, with JSON config as query param |
/health |
GET | Health check with browser pool stats |
/ping |
GET | Simple connectivity check (returns "pong") |
/docs |
GET | Swagger API documentation |
| Parameter | Type | Required | Description |
|---|---|---|---|
url |
string | One of url or html |
URL to render |
html |
string | One of url or html |
Raw HTML content to render |
type |
string | Yes | image, pdf, video, or html |
headers |
object | No | Custom HTTP headers to send with the request |
device |
object | No | Viewport settings (width, height, scale, userAgent, locale, timezone) |
render |
object | No | Render options (waitTime, fullPage, scroll, block) |
image |
object | No | Image settings (type, quality, compression, crop, resize, rotation) |
pdf |
object | No | PDF settings (format, width, height, margins, orientation, header/footer) |
video |
object | No | Video settings (fps, duration, codec, bitrate, crf, preset) |
Configure the browser viewport and device emulation.
| Parameter | Type | Default | Description |
|---|---|---|---|
width |
number | 1920 |
Viewport width in pixels |
height |
number | 1080 |
Viewport height in pixels |
scale |
number | 1 |
Device scale factor (DPR) |
userAgent |
string | Chrome default | Custom user agent string |
locale |
string | en-US |
Browser locale (e.g., fr-FR, de-DE) |
timezone |
string | UTC |
Timezone (e.g., Europe/London, America/New_York) |
javascriptEnabled |
boolean | true |
Enable/disable JavaScript execution |
Control page loading and rendering behavior.
| Parameter | Type | Default | Description |
|---|---|---|---|
waitTime |
number | 0 |
Additional wait time after page load (ms) |
timeout |
number | 30000 |
Page load timeout (ms) |
fullPage |
boolean | false |
Capture full scrollable page (images only) |
triggerLazyAnimations |
boolean | false |
Scroll page to trigger lazy-loaded content |
waitUntil |
string | networkidle0 |
Wait condition: load, domcontentloaded, networkidle0, networkidle2 |
scroll.position |
number | 0 |
Scroll to position before capture (px) |
scroll.animate |
boolean | false |
Enable scroll animation (video) |
scroll.duration |
number | 5000 |
Scroll animation duration (ms) |
scroll.animation |
string | smooth |
Scroll animation style |
block.cookies |
boolean | false |
Block cookie consent banners |
block.ads |
boolean | false |
Block advertisements |
block.trackers |
boolean | false |
Block tracking scripts |
block.banners |
boolean | false |
Block promotional banners |
Full control over PDF generation. Supports custom page dimensions with units.
| Parameter | Type | Default | Description |
|---|---|---|---|
format |
string | A4 |
Paper format: A0-A6, Letter, Legal, Tabloid, Ledger |
width |
string | - | Custom width with units (e.g., 210mm, 8.5in, 600px) |
height |
string | - | Custom height with units (e.g., 297mm, 11in, 800px) |
landscape |
boolean | false |
Landscape orientation |
scale |
number | 1 |
Scale of the webpage rendering (0.1 to 2) |
printBackground |
boolean | true |
Print background graphics |
displayHeaderFooter |
boolean | false |
Display header and footer |
headerTemplate |
string | "" |
HTML template for header |
footerTemplate |
string | "" |
HTML template for footer |
pageRanges |
string | "" |
Page ranges to print (e.g., 1-5, 1,3,5) |
preferCSSPageSize |
boolean | false |
Use CSS-defined page size |
omitBackground |
boolean | false |
Hide default white background |
margin.top |
string | 0px |
Top margin with units |
margin.right |
string | 0px |
Right margin with units |
margin.bottom |
string | 0px |
Bottom margin with units |
margin.left |
string | 0px |
Left margin with units |
Supported units: px, in, cm, mm
Note: When using custom
widthorheight, theformatoption is automatically ignored.
Configure screenshot output format and post-processing.
| Parameter | Type | Default | Description |
|---|---|---|---|
type |
string | png |
Output format: png, jpeg, webp, gif, avif |
quality |
number | 100 |
Image quality (0-100, for jpeg/webp) |
compression |
number | 0 |
PNG compression level (0-9, 0=fastest) |
smooth |
boolean | true |
Enable adaptive filtering for PNG |
resize |
number | 1 |
Resize factor (0.1 to 3) |
rotate |
number | 0 |
Rotation angle (0-360 degrees) |
roundedBorders |
number/boolean | false |
Border radius in pixels |
padding |
number | 0 |
Padding in pixels |
crop.left |
number | - | Crop area left offset |
crop.top |
number | - | Crop area top offset |
crop.width |
number | - | Crop area width |
crop.height |
number | - | Crop area height |
Configure MP4 video recording with scroll animation.
| Parameter | Type | Default | Description |
|---|---|---|---|
fps |
number | 60 |
Frames per second |
followNewTab |
boolean | true |
Follow navigation to new tabs |
videoCrf |
number | 23 |
Constant Rate Factor (0-51, lower=better quality) |
videoCodec |
string | libx264 |
Video codec |
videoPreset |
string | ultrafast |
Encoding preset: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow |
videoBitrate |
number | 3000 |
Bitrate in kbps (500-10000) |
recordDurationLimit |
number | 30 |
Maximum recording duration in seconds |
videoFrame.width |
number | 1920 |
Video frame width |
videoFrame.height |
number | 1080 |
Video frame height |
Screenshot from URL:
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "type": "image"}' \
--output screenshot.pngPDF from HTML:
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{
"html": "<!DOCTYPE html><html><body><h1>Hello</h1></body></html>",
"type": "pdf",
"pdf": {"format": "A4", "margin": {"top": "20px", "bottom": "20px"}}
}' \
--output output.pdfPDF with custom dimensions (mm):
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"type": "pdf",
"pdf": {
"width": "210mm",
"height": "400mm",
"margin": {"top": "10mm", "bottom": "10mm", "left": "10mm", "right": "10mm"}
}
}' \
--output custom-size.pdfPDF with custom dimensions (inches - US Letter):
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"type": "pdf",
"pdf": {
"width": "8.5in",
"height": "11in",
"margin": {"top": "0.5in", "bottom": "0.5in", "left": "0.5in", "right": "0.5in"}
}
}' \
--output letter.pdfReceipt/ticket PDF (narrow format):
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{
"html": "<html><body style=\"font-family:monospace\"><h2>RECEIPT</h2><hr><p>Item 1...$10.00</p><p>Total: $10.00</p></body></html>",
"type": "pdf",
"pdf": {"width": "80mm", "height": "200mm", "margin": {"top": "5mm", "bottom": "5mm", "left": "5mm", "right": "5mm"}}
}' \
--output receipt.pdfPDF with header and footer:
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"type": "pdf",
"pdf": {
"format": "A4",
"displayHeaderFooter": true,
"headerTemplate": "<div style=\"font-size:10px;text-align:center;width:100%\">Company Name</div>",
"footerTemplate": "<div style=\"font-size:10px;text-align:center;width:100%\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>",
"margin": {"top": "20mm", "bottom": "20mm", "left": "10mm", "right": "10mm"}
}
}' \
--output with-header-footer.pdfWebP image with custom quality:
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "type": "image", "image": {"type": "webp", "quality": 80}}' \
--output output.webpVideo with scroll animation:
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"type": "video",
"video": {"fps": 24},
"render": {"scroll": {"animate": true, "duration": 3000}}
}' \
--output output.mp4Full HTML capture (resources embedded as data URIs):
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "type": "html"}' \
--output page.htmlMobile viewport screenshot:
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"type": "image",
"device": {
"width": 375,
"height": 812,
"scale": 2,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15"
}
}' \
--output mobile.pngFull page screenshot with lazy loading:
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"type": "image",
"render": {
"fullPage": true,
"triggerLazyAnimations": true,
"waitTime": 1000
}
}' \
--output fullpage.pngCropped and resized screenshot:
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"type": "image",
"image": {
"type": "png",
"crop": {"left": 0, "top": 0, "width": 800, "height": 600},
"resize": 0.5
}
}' \
--output cropped.pngScreenshot with custom headers and locale:
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"type": "image",
"headers": {"Authorization": "Bearer token123"},
"device": {"locale": "fr-FR", "timezone": "Europe/Paris"}
}' \
--output french.pngGET request with config parameter:
curl "http://localhost:3000/render?config=%7B%22url%22%3A%22https%3A%2F%2Fexample.com%22%2C%22type%22%3A%22image%22%7D" \
--output screenshot.pngA comprehensive test script is included to demonstrate PDF custom dimensions:
./pdf.demo.examples.shThis generates 26 PDFs with various sizes (A3, A5, Letter, Legal, custom dimensions in mm/in/cm/px, receipts, posters, etc.) in the pdf-demo-examples/ directory.
All settings are driven by environment variables with sensible defaults.
| Variable | Default | Description |
|---|---|---|
SERVER_PORT |
3000 |
HTTP server port |
BODY_LIMIT_BYTES |
52428800 |
Max request body size (50 MB) |
SHUTDOWN_TIMEOUT_MS |
30000 |
Graceful shutdown timeout |
LOG_LEVEL |
info |
Log level (trace, debug, info, warn, error, fatal) |
| Variable | Default | Description |
|---|---|---|
POOL_MIN |
2 |
Minimum browser instances |
POOL_MAX |
10 |
Maximum browser instances |
POOL_ACQUIRE_TIMEOUT_MS |
30000 |
Max wait to acquire a browser |
POOL_IDLE_TIMEOUT_MS |
30000 |
Idle time before eviction |
BROWSER_MAX_AGE_MS |
600000 |
Max browser lifetime (10 min) |
BROWSER_MAX_REQUESTS |
100 |
Max requests per browser before recycling |
BROWSER_MAX_MEMORY_MB |
500 |
Memory limit per browser |
| Variable | Default | Description |
|---|---|---|
FFMPEG_PATH |
/usr/bin/ffmpeg |
Path to FFmpeg binary |
VIDEO_MAX_SCROLL_DURATION_MS |
20000 |
Max scroll animation duration |
| Variable | Default | Description |
|---|---|---|
DEFAULT_VIEWPORT_WIDTH |
1920 |
Default viewport width |
DEFAULT_VIEWPORT_HEIGHT |
1080 |
Default viewport height |
npm testWith coverage report (97% line coverage required):
npm run coverageRequires a running server on port 3000:
# Terminal 1: start the server
FFMPEG_PATH=/opt/homebrew/bin/ffmpeg node app/server.js
# Terminal 2: run E2E tests
npm run test:e2eE2E tests validate output format correctness using magic bytes (PNG, WebP, PDF headers, MP4 ftyp box, HTML structure).
The GitHub Actions workflow runs on every push:
- Unit tests with 97% coverage gate
- Docker build
- E2E tests against the running container
GET /health returns pool status and memory usage:
{
"status": "healthy",
"timestamp": "2026-01-22T12:00:00.000Z",
"browserPool": {
"available": 2,
"borrowed": 1,
"pending": 0,
"min": 2,
"max": 10
},
"uptime": 3600,
"memory": {
"used": "256 MB",
"total": "512 MB"
}
}Returns 200 when healthy, 503 when degraded (no browsers available).
For usage in commercial services, please refer to the license.txt file in this repository.
Note: License is not enforced, but we are a small team, and any support to further develop this product would be greatly appreciated! 🙏
