Skip to content
CastPDF
Guides

Images and fonts

Get logos, images and fonts into your PDFs. Public and data URLs, the network guard, blocked resources, your brand logo and fonts, and fallbacks.

On this page

Images and fonts are where an HTML to PDF render most often looks different from your browser: a logo that loads on your laptop may be on a network the renderer cannot reach, and a font installed on your computer is not installed on ours. This page explains how the renderer loads both, and how to make sure every PDF looks the way you designed it.

How images reach the renderer

CastPDF renders your HTML in Chromium, on our servers. Every image, stylesheet, font or script your HTML refers to is fetched by the renderer itself, during the render, under these rules:

  • Public https and http URLs on ports 443 and 80 are fetched. Use https URLs that anyone could open without logging in, or signed URLs that carry their own token.
  • data: URLs always work and are not counted against any network limit: <img src="data:image/png;base64,iVBORw0KGgo...">. They suit small images such as a logo or a signature; remember that they count toward the size of the HTML.
  • Use absolute URLs. Your HTML is not served from a web address, so a relative URL such as images/logo.png has nothing to resolve against. Write https://cdn.example.com/images/logo.png, or add a <base href="https://cdn.example.com/"> to the document's head.

The network guard

The renderer refuses requests that could reach a private network, so a document can never be used to read our internal services or yours:

  • Private, loopback and link-local addresses (such as 10.x.x.x, 192.168.x.x, 127.0.0.1, localhost or cloud metadata addresses) are refused, and so are the servers' own addresses.
  • The check is made on the address that is actually connected to: a public host name that resolves to a private address is refused, and every redirect is checked again.
  • Only http, https, data: and blob: URLs are allowed. file: URLs and other schemes are refused.
  • Cookies are never stored or sent, WebSockets and WebRTC are not available, and the page cannot navigate away from your document.

So http://localhost:3000/logo.png or an image on your company intranet works in your browser and is missing from the PDF. Put the file somewhere public (a CDN or object storage), or embed it as a data: URL.

Limits

LimitValue
Requests per document (every redirect counts)300
Downloaded data per document20 MB
Time per request10 seconds, and never beyond the render's own limit
Redirects per request5
The whole render, including loading resources30 seconds (more is render_timeout)

The renderer waits until the network is idle and all fonts are loaded before it prints, so an image that arrives in time is always in the PDF.

Blocked resources in the response

A request that is refused, or that fails at the network level (DNS, connection, timeout, size or request limit), does not fail the document: the PDF is made without that resource, and the response tells you about it.

  • A binary response has the header x-blocked-resources with the number reported, 0 when nothing was blocked.
  • A "response": "url" answer has blocked_resources: up to 10 URLs, each with its reason in brackets.
JSON
"blocked_resources": [
  "http://10.0.0.5/logo.png (address 10.0.0.5:80)",
  "https://cdn.example.invalid/font.woff2 (upstream error: ENOTFOUND)",
  "https://slow.example.com/photo.jpg (timeout)"
]

Other reasons you may see are size limit, too many requests, too many redirects and scheme file:. An HTTP error answered by the other server, such as a 404 for a mistyped image URL, is not listed: the page receives that answer like a browser would, and the image is simply missing. A request repeated with the same Idempotency-Key returns the stored PDF and reports 0 and [], and background PDFs and batch items do not report blocked resources at all. So check a new template with a direct request and "response": "url", and open the PDF, before you rely on it.

Upload your logo once in the dashboard (Simple mode, Design panel): a PNG, JPG or SVG of up to 1 MB. Templates print its URL as brand.logo_url:

HTML
{% if brand.logo_url %}<img src="{{ brand.logo_url }}" alt="" style="height: 14mm">{% endif %}
  • An SVG logo is turned into a PNG when you upload it (up to 1,200 pixels on its longest side, smaller if needed to stay under the size limit), so what is stored and printed is a picture.
  • When CastPDF renders your PDF it hands the logo file to the renderer directly: it is never fetched over the network and does not count against the limits above.
  • The URL is stable and needs no API key. When you replace the logo, it gets a new URL, and templates that print brand.logo_url pick it up automatically.
  • brand is Liquid data, so it is there for templates and for html sent with data. Without a logo, brand.logo_url is empty and the {% if %} above prints nothing.

The other brand values (colours, font, text direction and company details) are described under your brand.

SVG

SVG stays sharp at any size, which makes it the best format for logos, icons and charts in a PDF. Three ways to use it:

HTML
<!-- inline: can use the document's fonts and CSS -->
<svg viewBox="0 0 120 40" width="120" height="40"><rect width="120" height="40" rx="6" fill="#0f766e"/></svg>

<!-- as an image from a public URL -->
<img src="https://cdn.example.com/logo.svg" alt="" style="height: 12mm">

<!-- as a data URL: no network request -->
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4=" alt="">

Give an SVG a size (CSS width and height, or a viewBox plus one dimension), or it may take the full width of the page. In print mode, an SVG taller than a page is scaled down to fit on one page.

Sizing images for print

A PDF is measured in physical units, so size images with CSS lengths such as mm or in, not by their pixel count. CSS uses 96 px per inch, so a 960-pixel-wide image with no size set prints 10 inches (254 mm) wide, wider than an A4 page.

CSS
.logo { height: 14mm; width: auto; }
.photo { width: 100%; max-width: 120mm; }
img { max-width: 100%; }  /* never wider than the page */

For sharp print, an image needs about 300 pixels per inch of printed size: a logo printed 50 mm (about 2 inches) wide looks sharp at around 600 pixels wide. Much larger files do not look better on paper, they only make the PDF bigger and slower to render. Resize and compress photos before you use them: a PDF may be at most 40 MB, and large images are the usual cause of document_too_large.

CastPDF keeps images on one page where it can (img { break-inside: avoid } is part of every document), and in print mode an image taller than a page is scaled down to fit one page. See the print CSS guide.

Fonts

Fonts built into every PDF

These fonts are part of the renderer itself: Plus Jakarta Sans, Inter, Noto Sans, Noto Sans Arabic, Noto Sans Hebrew and Noto Sans SC (all under the SIL Open Font License). Use one by naming it in a font-family (or font) declaration, and it looks the same in every PDF without loading anything from the network:

CSS
body { font-family: "Inter", "Noto Sans", sans-serif; }

Plus Jakarta Sans, Inter and Noto Sans cover Latin (including Vietnamese); Inter and Noto Sans also cover Cyrillic and Greek. Noto Sans Arabic and Noto Sans Hebrew are for right-to-left text, and Noto Sans SC for Chinese and Japanese kana. Only the styles and characters a document uses are loaded, so naming them costs nothing.

Fonts installed on the renderer

These families are installed on the renderer's servers and can be named too:

  • Noto (Noto Sans, Noto Serif and the Noto families for other scripts), the Noto CJK families (such as Noto Sans CJK SC and Noto Serif CJK JP) for Chinese, Japanese and Korean, and Noto Color Emoji for emoji;
  • Liberation Sans, Liberation Serif and Liberation Mono, which have the same metrics as Arial, Times New Roman and Courier New;
  • DejaVu Sans, DejaVu Serif and DejaVu Sans Mono.

Arial, Helvetica, Times New Roman, Segoe UI and other fonts from Windows or macOS are not installed. A document that asks for one of them gets whatever font the system substitutes, which may look different from your browser. Name Liberation Sans yourself if you need Arial's metrics, or load the font you want.

Web fonts

For any other font, load it like on a web page: link a stylesheet from a public font service, or declare an @font-face with a public https URL. The renderer waits for fonts to finish loading before it prints, so font-display makes no difference.

HTML
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lora:wght@400;700&display=swap">
<style>
  body { font-family: "Lora", "Noto Serif", serif; }
</style>
CSS
@font-face {
  font-family: "Brand Sans";
  src: url("https://cdn.example.com/fonts/brand-sans.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
}
@font-face {
  font-family: "Brand Sans";
  src: url("https://cdn.example.com/fonts/brand-sans-bold.woff2") format("woff2");
  font-weight: 700;
}

Or embed the file as a data: URL, which needs no network request at all:

CSS
@font-face {
  font-family: "Brand Sans";
  src: url("data:font/woff2;base64,d09GMgABAAAAA…") format("woff2");
}

Web fonts count against the network limits above, and a data: font against the size of the HTML: the HTML after Liquid, with the CSS, may be at most 5 MB. WOFF2 files are the smallest. Declare one @font-face per weight and style you use; otherwise Chromium imitates bold and italic from the regular face, which looks worse.

Uploaded brand fonts

In the dashboard (Simple mode, Design panel) you can upload up to 5 font files: .woff2, .ttf or .otf, up to 2 MB each. You give each file a family name (letters, digits, spaces, - and _; not the name of a built-in font), a weight and a style, so a family can have a regular and a bold file.

Then name the family in any template's CSS, or in the CSS of html you send:

CSS
h1, h2 { font-family: "Acme Display", "Noto Sans", sans-serif; }

CastPDF sees that your CSS names an uploaded family, adds the @font-face rules for it, and hands the files to the renderer with the render: they are never fetched over the network and do not count against its limits. Choose one as brand.font in the Design panel and the starter templates use it for their text. Only upload fonts whose licence allows embedding them in PDFs.

Fallbacks

Always end a font-family list with fonts that are sure to exist, so a font that fails to load never leaves you with the browser's default serif font:

CSS
body {
  font-family: "Brand Sans", "Noto Sans", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans SC", sans-serif;
}

Chromium falls back per character: text in a script your main font lacks (Arabic, Chinese, emoji) is drawn with the next font in the list that has it, and failing that with any installed font that has it. Listing the built-in Noto families makes that choice predictable. This is the same list the starter templates use behind brand.font.

When a font or image is missing

  1. Render with "response": "url" and look at blocked_resources. A listed URL tells you the reason: a private address, a DNS error, a timeout or a limit.
  2. Open the URL in a private browser window. If it asks you to log in, or only works on your company network, the renderer cannot load it either.
  3. Check for a 404. A mistyped URL is not listed in blocked_resources; the image is simply missing.
  4. Check relative URLs. Make every URL absolute, or add a <base href>.
  5. Check the font name. The font-family must match the name in your @font-face, or the uploaded family name, exactly. A font installed only on your computer (Arial, Helvetica, Segoe UI) is replaced by a fallback.
  6. Check the format. Use WOFF2, TTF or OTF fonts and PNG, JPEG, GIF, WebP or SVG images: what Chromium shows on a web page, it prints.
  7. Embed it. When nothing else works, a data: URL takes the network out of the equation.

Next steps

Updated Something unclear? [email protected], a real person replies.