Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Images

Images can be embedded with <img> and with the CSS background-image.

Supported formatsPNG, JPEG, WebP, and SVG
What src may containA local relative or absolute path, an http(s) URL, or a data: URI

GIF is not supported.

SVG

SVG (.svg, and gzip-compressed .svgz) is embedded into the PDF as vectors rather than being rasterised. It stays resolution-independent when scaled up, and the size of the PDF depends on the number of shapes rather than on a pixel count. Parsing and normalisation are done by usvg, and the translation into PDF drawing operators by [svg2pdf](both come from [typst]).

Only referenced SVG is supported. It works from both <img src> and background-image: url(), but an inline <svg> element written directly in the HTML is not drawn (see below).

<img src="logo.svg" width="120">
<div style="background-image: url(pattern.svg)"></div>

data: URIs work too. SVG is commonly written without ;base64 (percent-encoded), so both forms are accepted.

<img src="data:image/svg+xml,%3Csvg%20xmlns%3D...%3E">
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0...">
/* the same works in CSS url() */
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D...%3E");

The format is detected from the bytes themselves. Neither the file extension nor the mime type a data: URI claims is used, so an SVG stored in a .txt file is drawn, and so is an SVG that claims to be image/png (conversely, a PNG named .svg is treated as a PNG).

Sizing, caching, and error handling work exactly as they do for raster images. The SVG’s width/height (or its viewBox if they are absent) becomes the intrinsic size. Unlike raster images the intrinsic size may be fractional, and it is kept as it is (not rounded). That way a ratio-driven value such as object-fit: contain stays exact even for an SVG with width="40.6".

object-fit and object-position

They work just as they do for raster images. An SVG enters the PDF normalised to the unit square, so fill, contain, cover, none, scale-down, and object-position all go through exactly the same implementation as raster images.

img.logo {
  width: 200px; height: 80px;
  object-fit: contain;      /* fit while keeping the ratio (leaves empty space) */
  object-position: 0% 50%;  /* align to the left */
}

Even a value that overflows, such as cover, is clipped to the content box when drawn.

Text and fonts inside an SVG

By default, <text> inside an SVG is not drawn. It is not converted to paths either, so nothing appears (the other shapes in that SVG are still drawn). A warning is printed when an SVG containing <text> is found.

Enabling the svg-text feature embeds it as glyphs (as selectable, searchable text). The fonts available to it are exactly the fonts the document uses, and they are resolved the same way as on the HTML side.

font-family in the SVGFont that is used
A family name from inside the font file (DejaVu Sans and the like)That font
A name declared with @font-faceThat font
serif / sans-serif / monospace--serif-font / --gothic-font / --mono-font (the default font if none is given)
Not specifiedThe document’s default font (the first one passed with --font)
A name the document does not haveThe document’s default font
@font-face { font-family: BrandFace; src: url(brand.ttf); }
<!-- both the name declared with `@font-face` and the family name inside the font work -->
<text font-family="BrandFace">売上</text>
  • System fonts are never looked up again for the sake of an SVG. A font the document does not have will not appear only inside an SVG; it falls back to the document’s default font instead
  • The document side and the SVG side get separate subsets of the same font file (they need different glyphs). Each font is embedded once
  • svg-text is off by default because enabling it adds 25 crates such as rustybuzz and resvg to the dependencies (svg2pdf’s text feature requires them). There is no need to add them if your SVG has no text

Inline SVG is not drawn

An <svg> element written directly in the HTML is dropped along with its subtree (through svg { display: none } in the user agent stylesheet). Its text does not leak into the body text either. A warning is printed if the document contains even one.

<!-- not drawn -->
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="20">
  <rect width="40" height="20" fill="red"/>
</svg>

<!-- written this way it is drawn -->
<img src="logo.svg" width="40" height="20">
<img src="data:image/svg+xml,%3Csvg%20...%3E" width="40" height="20">

Supporting it would mean rebuilding the SVG XML from the HTML DOM and handing that to usvg, and the case of attribute names (viewBox and friends), CSS inheritance, and how to treat currentColor all become questions separate from simply referencing an external file. For now it is limited to referencing an external SVG.

Other limitations

  • SVG filters (<filter>) are not supported. Resolving them would require rasterisation, so the machinery for it is not included
  • A raster image embedded inside an SVG (<image>) is not drawn
  • --grayscale does not apply to SVG (the colours live inside the individual drawing operators). Passing it prints a warning, and only the SVG stays in colour
  • External references from inside an SVG, such as <image href="...">, are not resolved. Each one is ignored with a warning. Reading a file from inside an SVG would bypass the containment that applies to <img> (the base directory, --allow-path, and --disable-local-file-access), so the path is closed off entirely. data: URIs are exempt (they are self-contained within the SVG itself)

Turning off the svg feature (--no-default-features) drops usvg from the build, and a reference to an SVG is then treated as a decoding failure.

<img>

<img src="logo.png" width="120">
<img src="https://example.com/chart.png" alt="Sales over time">
<img src="data:image/png;base64,iVBORw0…">
  • An <img> sits on the line as an inline replaced element. Give it display: block to put it on a line of its own
  • The width and height attributes and the CSS width and height are both honoured. With neither, the intrinsic size is used; with only one, the other is derived while keeping the aspect ratio
  • An image that cannot be fetched or decoded leaves just that element empty; it does not stop the document from being produced. Pass --load-media-error-handling abort to stop instead
  • However many times the same image is used, it is fetched, decoded, and embedded once

object-fit and object-position

These control how the image is fitted into the box you give it.

img.thumb {
  width: 120px;
  height: 80px;
  object-fit: cover;          /* fill | contain | cover | none | scale-down */
  object-position: 50% 50%;
}

Background images

.watermark {
  background-image: url("stamp.png");
  background-position: center;
  background-size: contain;
  background-repeat: no-repeat;
}

Only url() is accepted in background-image. Gradient functions such as linear-gradient() and comma separated multiple backgrounds are not supported. By default the image is tiled at its intrinsic size.

When border-radius is combined with a background image, the image is not clipped to the rounded corners; the radius applies only to the background colour.

Fetching remote images

This is off by default. Turn it on explicitly with --allow-remote-assets.

sghtmltopdf report.html --allow-remote-assets

Even when enabled, requests to destinations that are not globally reachable are always blocked. The rule is to allow only global unicast; the following are rejected.

KindRanges
Loopback127.0.0.0/8, ::1
Private10/8, 172.16/12, 192.168/16, fc00::/7
Link-local169.254/16 (including the cloud metadata endpoint 169.254.169.254), fe80::/10
CGNAT100.64.0.0/10 (cloud-internal load balancers and the like)
Other non-global0.0.0.0/8, 192.0.0.0/24, 198.18.0.0/15, 240.0.0.0/4, multicast, documentation
IPv6 special-purposeTeredo 2001::/32, 2001:db8::/32, ORCHIDv2 2001:20::/28, 100::/64

IPv6 forms that embed an IPv4 address (IPv4-mapped ::ffff:a.b.c.d, IPv4-compatible ::a.b.c.d, NAT64 64:ff9b::/96, 6to4 2002::/16) are judged by the embedded IPv4 address. Letting them through would allow the IPv4 filter to be bypassed.

The check is applied to the result of name resolution, so DNS rebinding and redirect-based bypasses are prevented by the same mechanism.

Private

When converting untrusted HTML, narrow the range of local references as well with --allow-path.

sghtmltopdf untrusted.html --allow-path /var/app/assets

JPEG is embedded as is

JPEG images are not decoded. Only their dimensions are read, and the data goes into the PDF unchanged, as DCTDecode. Nothing is re-encoded, so quality is preserved and conversion is faster.

The trade-off is that --grayscale leaves JPEG and CMYK images in colour, since there is no decoder for them. If you need them in greyscale, convert the images before rendering.

PNG and WebP are fully decoded, and an alpha channel is carried through as transparency.

Loading no images at all

sghtmltopdf invoice.html --no-images

This stops both <img> and the CSS background-image from being loaded.