Why Images Disappear When Converting HTML to PDF and How to Fix It
Images vanish in HTML-to-PDF output? Diagnose print CSS, resource access, timing, and renderer settings with fixes for Puppeteer, wkhtmltopdf, and WeasyPrint.
Short answer: PDF rendering can use different CSS, a different URL base, and a different runtime than your browser. Identify whether the missing visual is an <img>, SVG, or CSS background; inspect print CSS; verify that the converter can fetch the final URL with required credentials; wait for dynamic content; and read renderer warnings. Enabling background printing fixes CSS backgrounds only, not failed image requests.
1. Identify the image and renderer
Record the converter and version. Classify the missing item:
| Type | Check first |
|---|---|
<img> |
Resolved URL, HTTP status, cookies or headers, complete, naturalWidth, and file permissions. |
| SVG | Inline versus external SVG, external references, response content type, and renderer support. |
| CSS background | Print CSS and the background-graphics setting. |
Save the exact HTML and PDF, and log each resource’s absolute URL. A browser view proves only that one browser could fetch it.
2. Inspect print CSS
Puppeteer’s Page.pdf documentation states that PDF generation uses print media. An @media print rule may hide an image or change its layout. If screen styling is intended, select it explicitly:
await page.emulateMediaType('screen');
await page.pdf({path: 'out.pdf', printBackground: true, format: 'A4'});
Otherwise inspect computed styles under print media for display:none, visibility, zero dimensions, clipping, and color changes.
3. Separate backgrounds from image elements
Puppeteer’s PDF options set printBackground to false by default. Enable it for CSS backgrounds:
await page.pdf({path: 'out.pdf', printBackground: true});
This does not repair a failed <img src> request. Test the image URL and response separately.
4. Verify URLs, permissions, and authentication
- Resolve relative paths against the document’s real base URL; add a valid
<base>or absolute URLs. - Compare the converter’s container, proxy, DNS, certificate trust, and outbound network with your workstation.
- Pass required cookies, headers, authorization, or a valid signed URL for private images.
- For local files, confirm the file exists inside the job environment and is readable by its process user. Do not grant unrestricted filesystem access to untrusted HTML.
WeasyPrint’s resource documentation describes URL fetchers, custom fetchers for application media, local files, and warnings when fetch errors are caught. wkhtmltopdf’s usage reference documents image loading, JavaScript, media-error, and local-file controls. Check your installed version before changing flags.
5. Wait for dynamic content
Puppeteer’s PDF guide uses waitUntil: 'networkidle2' and PDF generation waits for fonts by default. Network idle does not prove lazy images are ready; wait for your application’s completion signal and inspect every image.
await page.goto('https://example.test/report', {waitUntil: 'networkidle2'});
await page.waitForFunction(() => [...document.images]
.filter(img => img.currentSrc)
.every(img => img.complete && img.naturalWidth > 0));
await page.pdf({path: 'report.pdf', printBackground: true});
wkhtmltopdf exposes JavaScript enablement and a JavaScript delay. Use a delay to diagnose timing, never as proof that requests succeeded.
6. Complete Puppeteer example
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({headless: true});
try {
const page = await browser.newPage();
page.on('requestfailed', r => console.error('failed', r.url(), r.failure()?.errorText));
page.on('response', r => { if (r.request().resourceType() === 'image' && !r.ok()) console.error(r.status(), r.url()); });
await page.goto('https://example.test/report', {waitUntil: 'networkidle2'});
await page.emulateMediaType('print');
await page.waitForFunction(() => [...document.images].every(i => !i.currentSrc || (i.complete && i.naturalWidth > 0)));
await page.pdf({path: 'report.pdf', format: 'A4', printBackground: true, preferCSSPageSize: true, waitForFonts: true});
} finally { await browser.close(); }
7. wkhtmltopdf and WeasyPrint
wkhtmltopdf --enable-javascript --javascript-delay 1000 --print-media-type --load-error-handling abort --load-media-error-handling abort https://example.test/report report.pdf
Remove --print-media-type for screen styling. Confirm you did not use --no-images; allow only required local directories.
from weasyprint import HTML
HTML('report.html', base_url='https://example.test/').write_pdf('report.pdf')
Use a custom WeasyPrint URL fetcher for approved authenticated assets and capture its warnings.
8. Diagnostic checklist
- Record renderer/version and exact options.
- Classify image, SVG, or background.
- Dump final HTML and resolved URLs.
- Inspect print computed styles.
- Log request failures, status, redirects, content type, proxy, and certificate errors.
- Check cookies, headers, local permissions, and network access.
- Wait for an application readiness marker; validate
completeandnaturalWidth. - Read warnings and fail when required assets are absent.
- Reduce to a fixture with one local image, remote image, SVG, and background.
9. Troubleshooting
| Symptom | Cause to verify | Fix |
|---|---|---|
| Only PDF is missing images | Print rule hides or relocates them. | Adjust print CSS or emulate screen media. |
| Only backgrounds vanish | Background graphics disabled. | Enable printBackground or the renderer equivalent. |
| Works locally only | Different base URL, credentials, filesystem, network, proxy, or CA. | Log and fix access from the converter runtime. |
| Private image blank | Missing or expired cookie, header, token, or signed URL. | Supply credentials securely or refresh the URL. |
| Lazy image intermittent | Capture races JavaScript. | Wait for application readiness and validate each image. |
| PDF succeeds with warnings | Fetcher caught an exception. | Use warnings as errors for required assets. |
| Local file denied | Policy or OS permissions. | Allow only the needed directory or serve through an authenticated local endpoint. |
10. Performance, reliability, and cost
- Reuse a controlled browser for batches, but isolate pages and close them.
- Keep normal HTTP caching and set explicit navigation and readiness timeouts.
- Retry transient failures with bounded backoff and preserve the original error.
- Validate required images before publishing the PDF.
- Log URLs, statuses, and warnings, not sensitive document contents.
11. Or skip the browser setup
ScreenshotNeo returns PNG, JPEG, WebP, or PDF from one GET request. Before capture it accepts consent banners and removes 60+ known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; X-Page-Verdict and X-Billed identify the result. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo docs for options:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
It also supports full-page and element capture, waits, custom CSS and JavaScript, headers and cookies, request blocking, PDF page settings, caching, signed links, async webhooks, bulk capture, usage reporting, and HTML/CSS to image. Free includes 1,000 shots per month with no card; paid plans start at $5 for 3,000. Start free with ScreenshotNeo.
12. FAQ
Does a delay always fix missing images?
No. It cannot fix denied requests, wrong URLs, or print rules.
Should every PDF print backgrounds?
Only designs that rely on CSS backgrounds need that option.
Why do some images work?
Each URL can have different credentials, media rules, and timing.
Which renderer is best?
Choose by required CSS behavior, resource access controls, and diagnostics; defaults differ.


