The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Use one wkhtmltopdf process and add each document in the order it should appear. In PHP, install the wkhtmltopdf executable separately, install a Composer wrapper, create a Pdf object with global defaults, call addPage() for every URL, file, or HTML string, and save the resulting PDF. Page-specific options can override the global defaults.
This approach creates one PDF containing several independently rendered pages. If you instead have one long HTML document, use print CSS page-break rules. The two techniques can be combined for reports with covers, table of contents pages, and content sections.
What you need before writing PHP
- A trusted installation of the
wkhtmltopdfexecutable on the machine that runs PHP. - Composer and the
mikehaertl/phpwkhtmltopdfpackage. - Permission for the PHP process to execute the binary and write the output directory.
- Network access when pages, stylesheets, images, fonts, or scripts are remote.
The PHP package is a wrapper, not a replacement for the executable. Verify the binary first:
wkhtmltopdf --version
If the command is not on the web server’s PATH, use its absolute path in the wrapper configuration. Then install the wrapper:
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
composer require mikehaertl/phpwkhtmltopdf
Package and binary versions, operating-system libraries, and distribution builds differ. Test the exact combination on the deployment host rather than assuming a development machine’s installation will behave identically.
Generate several URLs or files in a defined order
Each call to addPage() appends one input. The first call becomes the first document in the PDF, the second call becomes the next, and so on. Inputs may be URLs, local filenames, or HTML strings.
<?php
require __DIR__ . '/vendor/autoload.php';
use mikehaertlwkhtmltoPdf;
$pdf = new Pdf([
'binary' => '/usr/local/bin/wkhtmltopdf',
'page-size' => 'A4',
'margin-top' => '15mm',
'margin-right' => '15mm',
'margin-bottom' => '15mm',
'margin-left' => '15mm',
]);
$pdf->addPage('https://example.com/page-1');
$pdf->addPage('https://example.com/page-2');
$pdf->addPage(__DIR__ . '/page-3.html', [
'javascript-delay' => 500,
'enable-local-file-access' => true,
]);
if (!$pdf->saveAs(__DIR__ . '/output.pdf')) {
throw new RuntimeException($pdf->getError());
}
Global options establish defaults for every page. The array passed to addPage() changes settings only for that page. In the example, the third input gets a JavaScript delay and local-file access while the first two use the global settings.
Appending an HTML string
For generated content, pass an HTML string instead of a filename. Keep relative assets resolvable, or use absolute URLs and an appropriate base URL.
$html = '<!doctype html>
<html><head><meta charset="utf-8"></head>
<body><h1>Invoice 1007</h1><p>Generated by PHP.</p></body></html>';
$pdf->addPage($html, [
'page-size' => 'A4',
'encoding' => 'UTF-8',
]);
Adding a cover or table of contents
wkhtmltopdf supports page, cover, and table-of-contents objects. Use the wrapper’s corresponding object methods when you need a cover or generated contents section, then append normal pages in their intended position. Keep the sequence explicit so a cover does not accidentally follow the report body.
Use CSS when one document must break across sheets
Multiple addPage() calls are best when each input is a separate document. For a single long HTML document, mark the elements where a new sheet should begin and protect blocks that should stay together:
<style>
.chapter { break-before: page; page-break-before: always; }
.keep-together { break-inside: avoid; page-break-inside: avoid; }
</style>
<section>Introduction</section>
<section class="chapter">Chapter 1</section>
<div class="keep-together">A table and its caption</div>
break-before and break-inside are modern print properties. The older page-break-* declarations provide a useful fallback for wkhtmltopdf’s older WebKit engine. They are requests, not guarantees: oversized elements, floats, tables, and WebKit layout rules can still produce unexpected splits. Always inspect the rendered PDF.
Rank #2
Choosing separate pages or CSS breaks
| Requirement | Recommended method | Reason |
|---|---|---|
| Several independent URLs | One addPage() call per URL |
Order is explicit and each page can have its own options. |
| Local HTML files with shared styling | One call per file, or combine into one document | Separate calls isolate failures; one document gives CSS control across sections. |
| One long report | One HTML document plus print CSS | Sections, tables, and headings can be laid out together. |
| Cover or contents section | wkhtmltopdf cover/TOC object followed by page objects | Special objects occupy a defined position in the PDF. |
Control headers, footers, and paper layout
Set paper size and margins globally, then override unusual pages. Header and footer strings can include substitutions such as [page], [topage], [webpage], [date], and [isodate]. For example:
$pdf = new Pdf([
'page-size' => 'A4',
'margin-top' => '22mm',
'header-right' => 'Page [page] of [topage]',
'footer-center' => '[isodate]',
]);
Reserve enough top and bottom margin for the header or footer. If you need logos, multiple styles, or richer markup, use an HTML header/footer file and reference it with the relevant wkhtmltopdf option. A header that overlaps body content is usually a margin problem, not a CSS page-break problem.
Make JavaScript and local assets render reliably
JavaScript-generated content
wkhtmltopdf can capture a page before a client-side application finishes. Give it time with javascript-delay, or configure the documented window-status mechanism when your page can signal that rendering is complete. A delay is simple but adds the same wait to every run; a status signal can finish sooner when content is ready.
$pdf->addPage('https://example.com/dashboard', [
'javascript-delay' => 1500,
]);
Do not treat a successful process exit as proof that asynchronous content appeared. Check the PDF for charts, images, and data populated after the initial HTML response.
Local CSS, images, and fonts
Local-file access is restricted in many builds. Enable it for a trusted directory, or allow only the directory that contains the assets. Limiting access is safer than exposing the entire filesystem.
$pdf->addPage(__DIR__ . '/report.html', [
'enable-local-file-access' => true,
// Prefer an explicit allow-list when your build supports it:
// 'allow' => __DIR__ . '/public-assets',
]);
Use predictable, readable paths and verify that the PHP user can read every referenced file. A missing font or image may appear as a blank area while the command still returns a PDF.
Remote authentication and resources
Authenticated pages may require cookies, custom headers, or an application-specific session. Configure those options deliberately and avoid placing secrets in URLs or logs. If a page depends on third-party resources, a blocked request, certificate problem, or rate limit can change the output without changing your PHP code.
Handle failures instead of returning a corrupt PDF
Production code should check the wrapper result and retain the executable’s diagnostic output. The wrapper exposes an error message through getError(); execution failures should be caught and logged with the input identifier and command context.
try {
$pdf = new Pdf([
'binary' => '/usr/local/bin/wkhtmltopdf',
'page-size' => 'A4',
]);
foreach ($urls as $url) {
$pdf->addPage($url);
}
if (!$pdf->saveAs($destination)) {
throw new RuntimeException($pdf->getError());
}
} catch (Throwable $e) {
error_log('PDF generation failed: ' . $e->getMessage());
throw $e;
}
Write to a temporary filename and move it into place only after a successful save. This prevents readers from downloading a partially written file when a worker times out.
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 minuteTroubleshooting common problems
“The binary could not be found”
Cause: PHP’s process environment has a different PATH from your shell, or wkhtmltopdf is not installed.
Fix: Run wkhtmltopdf --version as the deployment user and set 'binary' to the full executable path.
Local images or styles are missing
Cause: local-file access is disabled, paths are relative to a different working directory, or the PHP user lacks read permission.
Fix: use an absolute HTML path, enable local access for trusted content, or add a narrow allow-list; then verify permissions and asset paths.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Charts or data are blank
Cause: JavaScript had not finished when rendering began.
Rank #4
Fix: add a measured javascript-delay or use a completion status signal. Confirm that the data endpoint is reachable from the server.
Pages break in the wrong place
Cause: an element is taller than the remaining space, table layout differs from a modern browser, or only modern break properties were supplied.
Fix: add both modern and legacy break rules, use break-inside: avoid on suitable blocks, simplify oversized elements, and inspect the actual PDF.
Recommended Free Tools
The PDF is empty or a URL shows an error page
Cause: DNS, TLS, authentication, redirects, bot protection, or a server-side timeout.
Fix: fetch the URL from the same host, test authentication separately, review stderr, and increase waits only after fixing connectivity or access problems.
It works locally but fails in production
Cause: different binary builds, missing shared libraries, sandbox permissions, fonts, or environment variables.
Fix: record the binary version, install required system dependencies, deploy fonts and assets, and run a smoke-test PDF during release.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Performance, reliability, and cost considerations
Every input page is rendered by the executable, so total time grows with page count, page complexity, network latency, and JavaScript waits. Reuse a stable configuration, avoid unnecessary delays, and keep assets close to the rendering host. For large batches, queue jobs and limit concurrency so CPU, memory, and file descriptors remain available.
Cache or pre-render static pages when freshness allows. Do not cache personalized pages without a clear isolation policy. Set application-level timeouts longer than the expected render time, but still enforce an upper bound so one unavailable URL cannot occupy a worker indefinitely.
Measure your own documents; the available implementation guidance does not establish a universal benchmark. Log input order, binary version, elapsed time, output size, and stderr so a changed package or page can be diagnosed.
Or skip the browser setup
If you need screenshots rather than a multi-document PDF, ScreenshotNeo provides a single website screenshot API call. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. It also offers an MCP server for Claude, Cursor, and other MCP clients, with take_screenshot, get_page_info, and capture_pdf tools.
For the full parameter list, see the ScreenshotNeo documentation. A direct call looks like this:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo has a free tier of 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. If that fits your use case, sign up for the free plan.
Equivalent calls from Python and Node.js
These examples use the same ScreenshotNeo endpoint and return the image bytes. Change the target URL and output filename as needed.
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Frequently Asked Questions
Can I mix URLs, local files, and HTML strings in one PDF?
Yes. Call addPage() for each input in the required sequence; the wrapper accepts those input types and allows per-page options.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated 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 matchWhy does a successful command still produce the wrong layout?
wkhtmltopdf uses an older WebKit rendering engine. CSS support and pagination can differ from a current browser, so inspect the PDF and provide legacy page-break fallbacks.
Should I use one huge HTML document or many addPage() calls?
Use separate calls for independent documents or per-page settings; use one document when shared CSS and cross-section layout control matter most.
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.




