Logos & images
Read the file, pass its bytes in images, and point <img src> at the same key:
import { readFileSync, writeFileSync } from "node:fs";
import { renderPdf } from "vellora";
const html = `<!DOCTYPE html>
<html>
<head>
<style>
@page { size: A4; margin: 18mm; }
body { font-family: sans-serif; }
img.logo { width: 120px; height: 40px; }
</style>
</head>
<body>
<img class="logo" src="logo.png" alt="Acme logo" />
<h1>Invoice {{ number }}</h1>
</body>
</html>`;
const pdf = await renderPdf(html, { number: "INV-0042" }, {
images: {
"logo.png": readFileSync("./assets/logo.png"),
},
});
writeFileSync("with-logo.pdf", pdf);The key is the exact src string. PNG, JPEG, GIF and WebP work, and the format is read from the bytes, not from the file name.
Give every image a width and a height
In vellora 0.3.0 an <img> needs both, as width / height attributes or in CSS (for example width: 120px; height: 40px). An image with only a width (or height: auto), or with no size at all, is left out of the PDF without an error. Use the image's own aspect ratio to pick the height.
Why not just use a URL?
vellora makes no network requests while it renders. A template can't leak data to a third-party server, a slow CDN can't hang your render, and the same bytes give the same PDF every time.
If your logo lives on a CDN, download it yourself and pass the bytes, keyed by the URL in the template:
const html = `<img src="https://cdn.example.com/brand/logo.png" width="120" height="40" />`;
const res = await fetch("https://cdn.example.com/brand/logo.png");
if (!res.ok) throw new Error(`logo download failed: ${res.status}`);
const logo = new Uint8Array(await res.arrayBuffer());
const pdf = await renderPdf(html, {}, {
images: { "https://cdn.example.com/brand/logo.png": logo },
});Fetch once and reuse the bytes across renders, or cache them however you already cache assets.
Small images can also go inline as data:image/png;base64,... URLs, with no images entry.
Templates with relative paths: baseUrl
If the template says src="logo.png" but your asset store keys files by full URL, set baseUrl. vellora joins the two to find the key (https://assets.example.com/invoice/logo.png). It never downloads anything from that address.
const pdf = await renderPdf(html, data, {
baseUrl: "https://assets.example.com/invoice/",
images: {
"https://assets.example.com/invoice/logo.png": logoBytes,
},
});When an image is missing
If an <img> would be drawn but no bytes match its src, the render stops instead of leaving a blank box:
VelloraUnsupportedError: Unsupported construct "image:unresolved" at line 2, column 1:
<img> source could not be resolved: no matching `images` entry for its `src` — supply the bytes
via the `images` render option (optionally with `baseUrl`)err.feature is "image:unresolved", and err.line / err.col point at the <img>.
Not supported
- Downloading
http(s)://images during the render. Pass the bytes instead. - SVG files as
<img>sources orimagesentries. Inline<svg>elements are drawn as vector graphics; an<image>inside one must use adata:URL. Details in COMPATIBILITY.md.
Using the CLI
vellora render takes one --image key=path flag per asset:
vellora render templates/invoice.html \
--data templates/invoice.json \
--image logo.png=assets/logo.png \
--out out/invoice.pdf