What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To render a local image in a Puppeteer PDF, make the image reachable to the browser, wait until it has loaded, and then call page.pdf(). With page.setContent(), do not assume a relative image path resolves from your HTML file’s directory: the API sets markup but does not document a filesystem base URL. For a reliable workflow, use an absolute file:// URL only after verifying access in your runtime, or serve the image over HTTP or embed it as a data URL.

Why local images disappear from Puppeteer PDFs

Puppeteer’s Page.pdf() prints the current page to PDF. If you build the page with page.setContent(html), Puppeteer assigns that markup to the page; its documented API does not establish that relative URLs should resolve against the directory containing your source HTML. So an <img src="./logo.png"> may not point to the file you expect.

There are two separate conditions to satisfy: the browser must be able to access the intended image resource, and the image must finish loading before PDF generation. A successful call to setContent() proves only that markup was assigned. It does not prove that Chrome can read a local file or that an image loaded successfully.

Puppeteer’s PDF guide and API references reviewed on September 29, 2026, display versions 25.12.0 for PDF generation and PDF options, 25.11.0 for setContent(), and “Next” for the lifecycle-options page. Check the documentation for the version installed in your project because the reviewed pages do not establish universal local-file rules across operating systems, Chrome builds, or containers. Puppeteer’s setContent() reference

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose how the browser will reach the image

Method Useful when Trade-off
Absolute file:// URL The image is on the same machine and your controlled browser runtime permits access to that exact path. Access behavior depends on the browser and runtime configuration. Verify it in the environment that creates the PDF; there is no universally established launch flag or permission rule in the reviewed documentation.
Local HTTP route Your application already has a local server, or can serve files through a controlled route. The browser must be able to reach the route, and the server must expose the intended file.
Data URL You have a small image and want to include its bytes directly in the HTML. Encoding increases the HTML size and is less convenient for multiple or large images.

These are implementation choices, not guarantees of Puppeteer behavior. Pick the one that fits your deployment, then test resource access under the same operating-system user and browser setup used in production. The Puppeteer Files guide covers file upload and notes that Puppeteer does not provide programmatic file downloads; it does not define local images as page subresources.

Complete Node.js example using a local HTTP image

Serving the asset over HTTP avoids relying on an assumed relative base path. This example starts a small local server for one image, loads HTML that references that route, checks every image element, and writes a PDF. Install Puppeteer with npm install puppeteer, save the code as render-pdf.js, and run it with node render-pdf.js /absolute/path/to/logo.png.

const http = require('node:http');
const fs = require('node:fs');
const path = require('node:path');
const puppeteer = require('puppeteer');

async function main() {
  const imagePath = path.resolve(process.argv[2] || 'logo.png');
  if (!fs.existsSync(imagePath)) {
    throw new Error(`Image not found: ${imagePath}`);
  }

  const server = http.createServer((req, res) => {
    if (req.url !== '/logo') {
      res.writeHead(404).end('Not found');
      return;
    }
    res.writeHead(200, { 'Content-Type': 'image/png' });
    fs.createReadStream(imagePath).pipe(res);
  });
  await new Promise((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', resolve);
  });

  let browser;
  try {
    const address = server.address();
    const imageUrl = `http://127.0.0.1:${address.port}/logo`;
    browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.setContent(`
      <!doctype html>
      <html><head><meta charset="utf-8">
      <style>body { font: 16px sans-serif; } img { max-width: 300px; }</style>
      </head><body>
      <h1>PDF with a local image</h1>
      <img src="${imageUrl}" alt="Logo">
      </body></html>`,
      { waitUntil: 'load' }
    );

    const imageResults = await page.evaluate(async () => {
      const images = [...document.images];
      await Promise.all(images.map(image => {
        if (image.complete) return Promise.resolve();
        return new Promise(resolve => {
          image.addEventListener('load', resolve, { once: true });
          image.addEventListener('error', resolve, { once: true });
        });
      }));
      return images.map(image => ({
        src: image.currentSrc || image.src,
        loaded: image.complete && image.naturalWidth > 0,
      }));
    });

    const failures = imageResults.filter(image => !image.loaded);
    if (failures.length) {
      throw new Error(`Image failed to load: ${JSON.stringify(failures)}`);
    }

    await page.pdf({ path: 'output.pdf', printBackground: true });
    console.log('Wrote output.pdf');
  } finally {
    if (browser) await browser.close();
    await new Promise(resolve => server.close(resolve));
  }
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

The sample serves the supplied file with a PNG content type; change that header if the asset is another format. For multiple images, serve each from a controlled route or use another access method, then check all required image elements. If your page inserts images asynchronously, perform the check after that code has run. The readiness check waits until each image either loads or errors, then rejects the PDF step if any image has no positive naturalWidth.

Using a data URL for a small image

If you already have the image bytes in Node, a data URL removes the need for a filesystem URL or local server. This example reads a PNG and embeds it in the HTML; the same image-readiness check is still appropriate before printing.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const fs = require('node:fs');
const puppeteer = require('puppeteer');

(async () => {
  const bytes = fs.readFileSync('/absolute/path/to/logo.png');
  const imageData = `data:image/png;base64,${bytes.toString('base64')}`;
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.setContent(`<html><body><img src="${imageData}" alt="Logo"></body></html>`);
    const result = await page.evaluate(() => [...document.images].map(image => ({
      src: image.currentSrc || image.src,
      loaded: image.complete && image.naturalWidth > 0,
    })));
    if (result.some(image => !image.loaded)) throw new Error('Image did not load');
    await page.pdf({ path: 'output.pdf', printBackground: true });
  } finally {
    await browser.close();
  }
})().catch(error => { console.error(error); process.exitCode = 1; });

For a larger image set, embedding every asset can make the HTML cumbersome. A local server is often easier to operate in that situation, provided it is reachable from the browser process.

Using a file URL or an existing page

File URLs

A file:// URL can be used in a controlled environment if the browser is permitted to read the exact file. Resolve the path in Node rather than guessing the working directory, and construct a file URL with Node’s URL utilities. Then inspect the image’s actual currentSrc and loaded state before printing. Because file access depends on the specific browser launch and deployment environment, test this approach in the same OS, container, and process identity used in production. Do not add a launch flag merely because an example on another system uses one.

Pages loaded with goto()

If you navigate to an HTML file with page.goto(), relative references may resolve according to that page’s URL context, but the browser still needs access to the image. This differs from passing only markup to setContent(); do not conflate assigning HTML with navigating to a file. Confirm the actual resolved URL and access behavior in your own runtime.

Wait for page-specific work

setContent() supports lifecycle waiting options, but an event such as load should not be treated as proof that every required image succeeded. Puppeteer documents waiting for fonts in PDF options; it does not promise a general wait for every image or arbitrary asynchronous page task. Check the resources your PDF depends on explicitly. SetContentWaitForOptions reference

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Generate the PDF with the right print settings

Call page.pdf() only after resource checks pass. Puppeteer uses print media by default, so print CSS can make the PDF differ from a screen preview. It also omits background graphics unless printBackground is enabled. That option affects CSS backgrounds; it does not make an inaccessible or unloaded <img> appear.

await page.pdf({
  path: 'output.pdf',
  printBackground: true,
  format: 'A4',
  waitForFonts: true,
  timeout: 30000,
});

Relevant documented options include:

  • printBackground defaults to false; set it to true when the design needs CSS background graphics.
  • waitForFonts defaults to true and waits for document.fonts.ready. Puppeteer notes that bringing a background page to the front may be necessary.
  • format defaults to letter and takes priority over width and height when set.
  • preferCSSPageSize defaults to false; when enabled, CSS @page sizing takes priority over PDF width, height, or format.
  • scale defaults to 1 and accepts a documented range from 0.1 to 2.
  • timeout defaults to 30,000 milliseconds; setting it to zero disables the timeout.
  • Without a path, page.pdf() returns a Uint8Array. A relative output path is resolved from the current working directory.

PDF generation modifies colors for printing by default. If exact color rendering matters, Puppeteer documents CSS -webkit-print-color-adjust as a way to request it. Use page.emulateMediaType('screen') only when screen media, rather than print media, is explicitly what the PDF should reflect. See the PDF generation guide, PDFOptions reference, and Page reference.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshoot missing or changed images

  • Broken icon or blank image: log currentSrc and src, check the browser can read the file as the Puppeteer process identity, and inspect failed requests or the page before calling pdf(). A relative path in markup assigned with setContent() is not proof of a valid filesystem path.
  • The readiness check says the image failed: confirm the path exists, the local route returns the intended bytes, or the data URL has the right MIME type and complete base64 data. Fix the resource error before generating the PDF.
  • Image works in a development screenshot but not the PDF: inspect print CSS and media behavior. Puppeteer prints using print media by default; use screen media only if that is the desired output.
  • Background illustration is absent: enable printBackground: true. This does not repair a failed image element.
  • Colors differ: PDF printing alters colors by default; request exact CSS color rendering with -webkit-print-color-adjust where appropriate.
  • Fonts or layout timing differs: waitForFonts concerns document fonts, not image readiness or all asynchronous page logic. Await application work and verify image completion separately.
  • Works locally, fails in a container or production: test with the same OS, Chrome/Puppeteer setup, filesystem layout, permissions, and process identity. The reviewed API references do not settle file URL permissions across environments, and they do not identify a universal launch option.

Or skip the browser setup

If your goal is a screenshot or PDF of a publicly reachable webpage rather than a PDF containing a local filesystem image, ScreenshotNeo provides a website screenshot API and MCP server. It is not a way to attach an image that exists only on your machine; make that asset available to the page first if the rendered page needs it.

One GET request can capture a URL. This cURL example saves a WebP screenshot of Stripe:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Best Value
The SQL Programming Language: .
  • Used Book in Good Condition
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for options, including PDF output. Cookie banners, newsletter popups, and chat widgets are removed before capture; each cleanup step can be turned off. Bot checks/CAPTCHAs, blank pages, failed loads, timeouts, and cache hits are not billed, with response headers indicating the page verdict and billing status. Its MCP server lets AI agents use screenshot and PDF tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.

Sign up free for ScreenshotNeo to get 1,000 screenshots a month with no card.

References

Frequently Asked Questions

Does waitForFonts wait for local images too?

No. It waits for document.fonts.ready; check required image elements separately before calling page.pdf().

Can I use this method when the image is on another machine?

The browser needs a reachable resource. A local file path on the Node host is not automatically available to a browser on a different machine; serve the image through a route reachable by that browser.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.