Renderpaper
Log in Start free

Images not loading in a PDF rendered from HTML

Your template has <img src="https://example.com/logo.png">, the page looks right in a browser, and the PDF has a blank space where the logo should be.

The renderer does not fetch it. That is not a bug and it is not a timeout — the renderer makes no outbound requests at all.

Why it works that way

A rendering service that fetches whatever URL a document names is a request-forgery engine. Anyone who can submit a template can make the service fetch a URL from inside its own network: cloud metadata endpoints, internal admin panels, anything reachable from the renderer but not from the caller.

The defences people bolt onto that — allowlists, DNS pinning, blocking link-local addresses — are notoriously easy to get wrong, and a mistake is a serious breach rather than a broken image.

So the renderer fetches nothing, and a document must carry everything it needs. The whole class of vulnerability is absent rather than mitigated, and as a side effect renders are faster and cannot fail because someone else's CDN is having a bad afternoon.

Fix 1: data URIs

Embed the bytes in the document.

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." alt="Logo">

Build it in whatever language you already have. Python:

import base64
from pathlib import Path

def data_uri(path: Path, mime: str = "image/png") -> str:
    encoded = base64.b64encode(path.read_bytes()).decode("ascii")
    return f"data:{mime};base64,{encoded}"

Go:

func dataURI(path, mime string) (string, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	return "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(b), nil
}

Base64 costs about 33% in size, and the template ceiling is 512 KiB — so this suits logos, signatures and icons rather than photography.

Fix 2: upload the asset

For anything that does not fit, or any image reused across templates, upload it once through the editor. Uploaded assets are inlined into the document at render time, so the renderer still fetches nothing — the inlining happens before rendering starts, on our side, from storage we control.

This is the right choice for a company logo: one upload, referenced from every template, and changing it does not mean re-encoding a base64 blob in a dozen places.

Fix 3: draw it in CSS

A surprising share of "images" in documents are shapes. A coloured header band, a rounded avatar placeholder, a rule, a watermark — all cheaper as CSS, and immune to this problem entirely.

.header-band { height: 8mm; background: linear-gradient(90deg, #2b6cb0, #4299e1); }

SVG works, inline

Inline <svg> is part of the document, so it renders — and it stays sharp at print resolution, which a 72dpi PNG will not.

<svg viewBox="0 0 24 24" width="32" height="32" fill="#2b6cb0" aria-hidden="true">
  <path d="M12 2 2 22h20L12 2Z"/>
</svg>

<img src="logo.svg"> does not work, for the same reason as any other URL. The SVG has to be inline, or a data URI.

Checking before you render

If an image is missing, the fastest check is whether the src starts with data: or points at an uploaded asset. Anything beginning http:// or https:// will not load, and no amount of waiting will change that.

Last updated 2026-08-08.