How to Embed Images and Logos Reliably in Generated PDFs

Embedding images in PDFs sounds trivial until a logo renders as a broken icon on a customer invoice. Here's how to get it right across formats, sources, and rendering engines.

pdfimageslogosnode.jshtml-to-pdf

How to Embed Images and Logos Reliably in Generated PDFs

Embedding an image in a PDF sounds like a one-liner — until the logo renders as a broken icon on a client's invoice, a signature image gets rasterized into mush, or your CI pipeline silently ships PDFs without the company brand because an S3 URL timed out. Image embedding is one of those areas where small decisions compound into reliability problems.

This post covers the practical choices that matter: image formats, how to source the bytes, resolution, and the failure modes that bite in production.

Pick the right format for the job

Not every image format behaves well inside a PDF. A quick cheat sheet:

  • PNG — Best for logos, UI screenshots, anything with transparency or sharp edges. Lossless.
  • JPEG — Best for photos. Smaller file size, no transparency, lossy.
  • SVG — Best for logos if your renderer supports it well. Scales perfectly, but support varies.
  • WebP / AVIF — Avoid. Most PDF engines don't support them natively.
  • GIF — Avoid. Convert to PNG first.

If you're generating PDFs from HTML (via Chromium, WeasyPrint, or an API like Kamy), SVG is usually the best choice for logos because it stays crisp at any zoom level. If you're using a low-level library like pdf-lib or PDFKit, stick to PNG or JPEG — SVG support is limited or nonexistent.

Never load images from remote URLs at render time

This is the single biggest reliability issue. If your template references a logo like this:

html
<img src="https://cdn.example.com/logo.png" />

…then every PDF render makes an HTTP request. That request can fail because:

  • The CDN had a hiccup
  • Your renderer is sandboxed and can't reach the network
  • DNS resolution is slow and the render times out
  • The image moved and nobody updated the template

The fix: embed images as base64 data URIs, or fetch and cache them locally before rendering.

Here's a small helper for Node.js that inlines a remote image:

ts
import { readFile } from "node:fs/promises"; import path from "node:path"; async function toDataUri(filePath: string): Promise<string> { const buffer = await readFile(filePath); const ext = path.extname(filePath).slice(1).toLowerCase(); const mime = ext === "svg" ? "image/svg+xml" : `image/${ext}`; return `data:${mime};base64,${buffer.toString("base64")}`; } const logo = await toDataUri("./assets/logo.png"); const html = `<img src="${logo}" alt="Logo" style="height: 40px" />`;

For assets that live in object storage, fetch them once at service startup and keep them in memory. Logos rarely change — there's no reason to re-download them per request.

Get the resolution right

PDFs are print-oriented. Browsers render at 96 DPI; print expects 300 DPI. If you drop a 100×100 logo into a slot that's 2 inches wide, it'll look fuzzy on paper even if it looks fine on screen.

Rule of thumb: the source image's pixel dimensions should be at least 2x the rendered size in points. A logo rendered at 150pt wide should ideally be 300px wide or more.

For HTML-based PDF generation, be explicit about display size using CSS:

css
.logo { width: 180px; height: auto; image-rendering: -webkit-optimize-contrast; }

Don't rely on the browser's default scaling — set dimensions explicitly so you know what the final output looks like.

Embedding with pdf-lib (low-level approach)

If you're assembling PDFs programmatically, here's the pattern:

ts
import { PDFDocument } from "pdf-lib"; import { readFile } from "node:fs/promises"; const pdfDoc = await PDFDocument.create(); const page = pdfDoc.addPage([595, 842]); // A4 const logoBytes = await readFile("./assets/logo.png"); const logo = await pdfDoc.embedPng(logoBytes); const logoDims = logo.scale(0.5); page.drawImage(logo, { x: 50, y: 760, width: logoDims.width, height: logoDims.height, }); const pdfBytes = await pdfDoc.save();

Two things to watch: use embedPng or embedJpg depending on the format (there's no auto-detect), and call scale() to preserve the aspect ratio instead of hardcoding dimensions.

Using an HTML-to-PDF API

If you're using an API-based approach like Kamy, you can pass image assets inline with your template data:

ts
import { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "invoice", data: { invoiceNumber: "INV-2024-001", logoUrl: "https://assets.yourapp.com/logo.svg", customer: { name: "Acme Corp" }, }, });

The Kamy renderer fetches referenced assets server-side with retries and caching, so a transient CDN failure doesn't break the render. For maximum safety, you can also pre-upload logos as named assets and reference them by ID — avoiding external URLs entirely.

Testing checklist

Before shipping PDF generation to production, verify:

  • Logos render at both screen view and when printed at 100%
  • PDFs generate correctly with no network access to third-party domains
  • File sizes are reasonable (a logo shouldn't add 2MB to every invoice)
  • Trans