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
httpsandhttpURLs on ports 443 and 80 are fetched. UsehttpsURLs 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.pnghas nothing to resolve against. Writehttps://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,localhostor 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:andblob: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
| Limit | Value |
|---|---|
| Requests per document (every redirect counts) | 300 |
| Downloaded data per document | 20 MB |
| Time per request | 10 seconds, and never beyond the render's own limit |
| Redirects per request | 5 |
| The whole render, including loading resources | 30 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-resourceswith the number reported,0when nothing was blocked. - A
"response": "url"answer hasblocked_resources: up to 10 URLs, each with its reason in brackets.
"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.
Your brand logo
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:
{% 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_urlpick it up automatically. brandis Liquid data, so it is there for templates and forhtmlsent withdata. Without a logo,brand.logo_urlis 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:
<!-- 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.
.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:
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 Serifand the Noto families for other scripts), the Noto CJK families (such asNoto Sans CJK SCandNoto 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.
<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>@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:
@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:
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:
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
- Render with
"response": "url"and look atblocked_resources. A listed URL tells you the reason: a private address, a DNS error, a timeout or a limit. - 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.
- Check for a
404. A mistyped URL is not listed inblocked_resources; the image is simply missing. - Check relative URLs. Make every URL absolute, or add a
<base href>. - Check the font name. The
font-familymust 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. - 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.
- Embed it. When nothing else works, a
data:URL takes the network out of the equation.
Next steps
- Print CSS guide: page size, page breaks, headers and page numbers.
- Templates: your brand's colours, font and company details.
- Troubleshooting: other ways a PDF can differ from your browser.