DocRaptor Alternative: Prince vs Chrome, Feature by Feature

We printed one paged-media test file with Prince 17 and Chrome 148. See which print features Chrome lacks, where Prince wins, and DocRaptor migration code.

By Simon Chiu · Updated

Disclosure: RenderScreenshot is our product and renders with Chrome. DocRaptor is not ours. The engine tests below ran on 2026-10-18 on our machine; DocRaptor's facts and prices come from its API reference and plans page, read the same day.

DocRaptor is a hosted front end for Prince, a typesetting engine built for print. That is its strength and the reason people look for an alternative: Prince is excellent at books, footnotes and page references, but your web app is built for Chrome, and every CSS feature you use has to be checked against a second engine. A Chrome-based API prints what your users already see.

The honest question is which print features you would lose. We wrote one test file that uses ten paged-media features, printed it with Prince 17 and with Chrome 148, and checked every page. Here is what we found, followed by a parameter map, prices at three volumes and migration code. For the wider market, see our HTML to PDF API roundup.

The short answer

Choose DocRaptor (Prince) if Choose a Chrome-based API if
Your PDFs need footnotes, running chapter titles in the header, or "see page 12" cross-references Your PDFs are invoices, receipts, statements, dashboards or exported app pages
You need PDF/A or PDF/UA output on request (prince_options.profile) Your templates use the CSS and JavaScript your web app uses
You also need XLS or XLSX files from the same API You also need PNG or WebP screenshots from the same key
Each document may take minutes to lay out (async jobs up to 600 seconds) Documents render within one minute
You price per document at any size and accept a higher entry price You want the lower price per document at 1,000 and 10,000 a month

The paged-media test

Method. One HTML file (paged.html, five pages) with ten paged-media features. Each feature prints a marker string, so pdftotext and pdfinfo can show whether it worked. Prince 17 ran from the prince-pdf Python package (the free version, which adds a small logo to page 1). Chrome 148.0.7778.97 ran through Puppeteer 24.43.1 with page.pdf({ printBackground: true, preferCSSPageSize: true }). Both on macOS, Apple M1 Pro, 2026-10-18.

Paged-media feature CSS used Prince 17 Chrome 148
Page counter in a margin box @bottom-center { content: counter(page) " of " counter(pages) } Yes Yes
Different first page @page :first { ... } Yes Yes
Static text in another margin box @bottom-left { content: "..." } Yes Yes
Named page with its own size page: wide + @page wide { size: A4 landscape } Yes, page 4 landscape Yes, page 4 landscape
Page break before each chapter break-before: page Yes Yes
Running header with the chapter title string-set: chapter content() + string(chapter) Yes No, header empty
Table of contents with page numbers target-counter(attr(href), page) Yes, "2", "3", "4" No, numbers missing
Dot leaders in the contents leader('.') Yes No
Cross-reference "(page 4)" target-counter() in ::after Yes No, the whole ::after text was dropped
Footnote at the foot of the page float: footnote Yes, moved to the page bottom No, stayed inline in the paragraph

Two more differences showed up in the file metadata:

  • Bookmarks. Prince wrote a PDF outline (bookmarks from the headings) by default. Chrome wrote none.
  • Tagging. Chrome's PDF was tagged (pdfinfo reports Tagged: yes). Prince's was not, by default; Prince tags when you ask for it, for example with the PDF/UA-1 profile.

The result is a clear line. Chrome now handles the page furniture most business documents need: page X of Y, a different cover page, landscape pages inside a portrait document. Prince still owns the book features: running headers that follow the content, generated page references and footnotes.

A caution about versions

We tested Prince 17 because it is the current release. DocRaptor's API reference lists pipeline 10.1, which is Prince 15.1, as its default. Versions matter: in our DocRaptor vs PDFShift test three days earlier, DocRaptor's default pipeline stacked a three-column CSS grid into one column. Prince 17 on our machine laid the same kind of grid out correctly, along with flexbox gap, :has() and CSS nesting. It still ignored aspect-ratio, object-fit and color-mix(). Check which pipeline your account uses before you blame the CSS.

If you move to Chrome: replacing the missing features

Prince feature Chrome replacement
Running chapter header (string-set) A fixed pdf.header template with the document title class, or render each chapter as its own request and merge
Contents page numbers (target-counter) Drop the numbers and rely on the PDF being read on screen, or render twice: print once, find each heading's page with pdftotext -f N -l N, write the numbers into the HTML, print again
Footnotes (float: footnote) Endnotes at the end of each section, linked with anchors
Bookmarks None in the API; add them afterwards with a PDF library if you need them
PDF/A, PDF/UA profiles None; stay on a print engine if a regulator asks for these

Parameter map

DocRaptor names are from its API reference; ours are from the PDF parameters and content input docs.

Task DocRaptor RenderScreenshot
Endpoint POST https://api.docraptor.com/docs POST https://api.renderscreenshot.com/v1/screenshot
Auth API key as the HTTP Basic user name Authorization: Bearer rs_live_...
HTML input document_content html
URL input document_url url
Base URL for relative assets prince_options.baseurl html_base_url
Output type type: "pdf" (also xls, xlsx) output.format: "pdf" (also png, jpeg, webp)
Print or screen CSS prince_options.media, default print browser.media, default screen
Paper size CSS @page { size } pdf.paper, or your @page size with pdf.prefer_css_page_size: true
Page numbers CSS margin boxes CSS margin boxes, or a pdf.footer template
JavaScript Off unless javascript: true Always on
Wait for async content Page calls docraptorJavaScriptFinished() wait.until: "networkidle" or wait.for_selector
Free test output test: true, watermarked Free plan credits each month
Long jobs async: true + callback_url, up to 600 s webhook.url, returns 202 with a job ID
Hosted file link hosted: true (paid add-on) X-Cache-URL header on every render
Passwords and permissions prince_options.user_password, disallow_print, ... None
Synchronous time limit 60 seconds wait.timeout up to 60 seconds

Two rows cause most migration bugs. Media: DocRaptor applies @media print rules by default, we apply screen rules, so send browser.media: "print". Paper: our default is Letter. If your templates set @page { size: A4 }, add prefer_css_page_size: true or set pdf.paper yourself. In our Chrome test, the @page margins in the template were applied either way.

Prices at three volumes

DocRaptor's monthly plans as listed on its plans page on 2026-10-18; ours from our pricing page the same day. Each cell is the cheapest plan that covers the volume.

PDFs per month DocRaptor RenderScreenshot
1,000 $75 (Premium, 1,250 documents) $10 (Starter)
10,000 $399 (Bronze, 15,000 documents) $40 (Growth)
100,000 Enterprise, contact sales (Silver is $1000 for 40,000) Above our largest plan (60,000 credits); not self-serve today

DocRaptor charges per document at any size and lets paid plans go over their limit at a per-document rate. We charge one credit per successful render at any size, and stop at the limit instead of billing overage. At 100,000 documents a month neither of us has a list price; talk to DocRaptor, or run Chrome yourself.

Migration code

The pattern is the same in every language: one render_pdf(html) function, a provider switch in an environment variable, and nothing else in your app knows which service printed the file. The Node version can also render with the second provider in the background and log both page counts, which is the cheapest way to find templates that break.

We checked the syntax of all three scripts and the page-count helper (against Chrome and Prince PDFs from our test) on 2026-10-18. They were not run against either live API for this page.

Node.js (22+)

// One renderPdf() for both providers. PDF_PROVIDER picks the primary; PDF_SHADOW renders with the other
// in the background and logs the page counts, so you can compare before you switch.
import { writeFile } from 'node:fs/promises';

const providers = {
  async docraptor(html) {
    const auth = Buffer.from(`${process.env.DOCRAPTOR_API_KEY}:`).toString('base64');
    const res = await fetch('https://api.docraptor.com/docs', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Basic ${auth}` },
      body: JSON.stringify({ type: 'pdf', document_content: html, test: process.env.NODE_ENV !== 'production' }),
      signal: AbortSignal.timeout(65_000),
    });
    if (!res.ok) throw new Error(`DocRaptor ${res.status}: ${await res.text()}`);
    return Buffer.from(await res.arrayBuffer());
  },

  async renderscreenshot(html) {
    const res = await fetch('https://api.renderscreenshot.com/v1/screenshot', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.RS_API_KEY}` },
      body: JSON.stringify({
        html,
        output: { format: 'pdf' },
        browser: { media: 'print' },              // DocRaptor applies print CSS by default
        pdf: { prefer_css_page_size: true },      // keep the @page size your templates already set
        wait: { until: 'networkidle' },
      }),
      signal: AbortSignal.timeout(65_000),
    });
    if (!res.ok) throw new Error(`RenderScreenshot ${res.status}: ${await res.text()}`);
    return Buffer.from(await res.arrayBuffer());
  },
};

const pageCount = (pdf) => (pdf.toString('latin1').match(/\/Type\s*\/Page[^s]/g) ?? []).length;

export async function renderPdf(html) {
  const primary = process.env.PDF_PROVIDER ?? 'docraptor';
  const pdf = await providers[primary](html);
  const shadow = process.env.PDF_SHADOW;
  if (shadow && shadow !== primary) {
    providers[shadow](html)
      .then((other) => console.log(JSON.stringify({ shadow, pages: [pageCount(pdf), pageCount(other)], bytes: [pdf.length, other.length] })))
      .catch((err) => console.error(`shadow ${shadow} failed:`, err.message));
  }
  return pdf;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const html = '<!doctype html><style>@page { size: A4; margin: 18mm }</style><h1>Statement</h1><p>Balance due: $120.00</p>';
  await writeFile('statement.pdf', await renderPdf(html));
  console.log('saved statement.pdf');
}

Run PDF_PROVIDER=docraptor PDF_SHADOW=renderscreenshot node render-pdf.mjs for a week. Every line where the two page counts differ points at a template to inspect. While DocRaptor stays in test mode outside production, its shadow renders are free and watermarked.

Python

"""render_pdf(html) for DocRaptor or RenderScreenshot, picked by PDF_PROVIDER."""
import os
import requests


def _docraptor(html):
    resp = requests.post(
        'https://api.docraptor.com/docs',
        auth=(os.environ['DOCRAPTOR_API_KEY'], ''),
        json={'type': 'pdf', 'document_content': html, 'test': os.environ.get('ENV') != 'production'},
        timeout=65,
    )
    resp.raise_for_status()
    return resp.content


def _renderscreenshot(html):
    resp = requests.post(
        'https://api.renderscreenshot.com/v1/screenshot',
        headers={'Authorization': f"Bearer {os.environ['RS_API_KEY']}"},
        json={
            'html': html,
            'output': {'format': 'pdf'},
            'browser': {'media': 'print'},
            'pdf': {'prefer_css_page_size': True},
            'wait': {'until': 'networkidle'},
        },
        timeout=65,
    )
    resp.raise_for_status()
    return resp.content


def render_pdf(html):
    provider = os.environ.get('PDF_PROVIDER', 'docraptor')
    return {'docraptor': _docraptor, 'renderscreenshot': _renderscreenshot}[provider](html)

Ruby

# render_pdf(html) for DocRaptor or RenderScreenshot, picked by PDF_PROVIDER.
require 'json'
require 'net/http'

module PdfRenderer
  module_function

  def render_pdf(html)
    case ENV.fetch('PDF_PROVIDER', 'docraptor')
    when 'docraptor' then docraptor(html)
    when 'renderscreenshot' then renderscreenshot(html)
    else raise ArgumentError, "unknown PDF_PROVIDER #{ENV['PDF_PROVIDER']}"
    end
  end

  def docraptor(html)
    req = Net::HTTP::Post.new(URI('https://api.docraptor.com/docs'), 'Content-Type' => 'application/json')
    req.basic_auth(ENV.fetch('DOCRAPTOR_API_KEY'), '')
    req.body = { type: 'pdf', document_content: html, test: ENV['RAILS_ENV'] != 'production' }.to_json
    send_request(req)
  end

  def renderscreenshot(html)
    req = Net::HTTP::Post.new(URI('https://api.renderscreenshot.com/v1/screenshot'),
                              'Content-Type' => 'application/json',
                              'Authorization' => "Bearer #{ENV.fetch('RS_API_KEY')}")
    req.body = { html: html, output: { format: 'pdf' }, browser: { media: 'print' },
                 pdf: { prefer_css_page_size: true }, wait: { until: 'networkidle' } }.to_json
    send_request(req)
  end

  def send_request(req)
    res = Net::HTTP.start(req.uri.host, req.uri.port, use_ssl: true, read_timeout: 65) { |http| http.request(req) }
    raise "#{req.uri.host} #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)

    res.body
  end
end

Templates that use prince-* properties, string-set or float: footnote will render without those effects in Chrome, as the test table shows. Search your templates for those names first; they are the work in a migration.

When to stay with DocRaptor

  • Your PDFs are books, manuals, contracts with numbered clauses and cross-references, or academic papers. Prince's generated content is the reason DocRaptor exists, and Chrome has no answer for footnotes or page references.
  • Compliance asks for PDF/A or PDF/UA. DocRaptor exposes Prince's profiles on the request.
  • You need Excel output or password-protected PDFs. Neither is in our API.
  • Some documents take several minutes. DocRaptor's async mode allows 600 seconds per job; our wait timeout is 60 seconds.

If your PDFs are business documents that already look right in Chrome, get a free API key and run the shadow script above on a week of real templates. The PDF API overview covers header and footer templates and the print CSS checklist.

Last tested 2026-10-18: Prince 17 (prince-pdf 17.0.0.post2) and Chrome 148.0.7778.97 via Puppeteer 24.43.1, Node 22.18.0, macOS on an Apple M1 Pro. DocRaptor parameters and plans read from its site on the same date.

Frequently asked questions

What engine does DocRaptor use?
Prince. DocRaptor's API reference lists pipeline 10.1 as the default, which runs Prince 15.1. You choose another version with the pipeline parameter.
Can Chrome replace Prince for PDF generation?
For most invoices, receipts and reports, yes. Chrome 148 printed page counters, first-page rules and landscape named pages in our test. It did not print running headers from string-set, table-of-contents page numbers, cross-reference page numbers or footnotes. If your documents need those, stay on Prince.
Is there a Prince XML alternative that supports CSS paged media?
Chrome supports a growing part of CSS paged media, including @page margin boxes and named pages. WeasyPrint is an open-source engine with strong paged-media support. For footnotes, cross-references and PDF/UA, Prince remains the most complete option we tested.
Why does my DocRaptor template look different in Chrome?
DocRaptor applies print CSS by default and Chrome-based APIs often start from screen CSS. Set browser.media to print. Also check the page size: set pdf.prefer_css_page_size to true so Chrome uses the @page size in your template.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.