Streaming mode
With --streaming, the HTML is read in chunks, each page is written to the PDF as soon as its layout is final, and the memory that page used is released.
sghtmltopdf big.html -o big.pdf --streaming
It pays off for HTML with tens of thousands of elements (see Memory and processing time). In exchange, anything that cannot be decided without seeing the whole document becomes unavailable.
Not available (these are errors)
Passing any of these exits with code 3.
| Not available | Why |
|---|---|
counter(pages), and [topage] in headers and footers | The total page count is not known in a single pass |
--toc | Same reason; a table of contents needs the page numbers of the body |
A background colour or border on <html> or <body> itself | That decoration would have to be reproduced across every page |
<style> or <link rel="stylesheet"> after <body> | It cannot be applied retroactively to pages that are already written |
Warned about, but processing continues
These change the result, so a warning is printed rather than passing over them silently.
| Limitation | Behaviour |
|---|---|
Finding a system font from a font-family name | The default font is used instead. Naming the font with --font, --gothic-font, --serif-font, --mono-font, or @font-face resolves it. |
| Searching the system for a font that can draw a given character | Not done, because the whole document cannot be read ahead. Only when no font at all is given, one CJK font is loaded up front (see Fonts). Characters that cannot be drawn are reported one by one. |
:last-child, :nth-last-child, :last-of-type, :nth-last-of-type, :only-child, :only-of-type, and :empty | Never match, because they cannot be decided until the parent’s child list is complete |
Selectors that look backwards do not work because the style has to be settled the moment the element is read. Rewrite rules such as “remove the rule on the last row only” by adding a class and writing .last { … } instead.
Still available
The following work exactly as they do in the normal mode.
--cover, the cover page- Headers and footers, as far as
[page];[topage]is not available - Page setup and PDF metadata
--grayscale,--dpi, and--zoom- The options that change what is drawn, such as
--no-images - Page breaks, including
break-before,break-after,break-inside,orphans, andwidows
Memory and processing time
These are measured figures for converting the same HTML in both modes. Each cell shows peak memory and processing time.
| Elements | HTML size | Normal mode | --streaming |
|---|---|---|---|
| 1,000 | 46KB | 11MB / 0.02s | 8MB / 0.02s |
| 5,000 | 233KB | 26MB / 0.08s | 10MB / 0.10s |
| 20,000 | 946KB | 81MB / 0.35s | 15MB / 0.38s |
| 60,000 | 2.8MB | 228MB / 1.05s | 28MB / 1.07s |
Peak memory in the normal mode grows roughly in proportion to the document, while with --streaming it barely grows at all; at 60,000 elements it is about one eighth. The slight growth that remains comes from the PDF cross-reference table, which holds the position of every object, and from the tally of glyphs in use, both of which are kept until the end.
Processing time is much the same in both modes. --streaming comes out slightly slower, but the difference stays within 0.03 seconds at every size, and shrinks in relative terms as the document grows (2% at 60,000 elements). You are not trading time away for the large drop in memory.
The measurements were taken as follows.
- A release build of sghtmltopdf 0.1.0, on an Intel Core Ultra 7 258V with 16GB of memory, under WSL2 (Linux 5.15)
- HTML consisting of nothing but the given number of
<p>elements, each 60px tall, with the font named through--font - Peak memory is the maximum resident set size (RSS) of the process, taking the better of two runs
When to use it
- Producing a statement of several thousand pages from a single HTML file
- Running where memory is tightly capped, such as under a container limit or on a serverless platform
For a document of a few dozen pages, such as an invoice, the normal mode is fine. There is no reason to take on the limitations.
In server mode the same mode is selected by adding streaming to the query. Combined with ?stream=1, which returns the body chunked, input, rendering, and output all become incremental.
curl --data-binary @big.html 'http://127.0.0.1:8080/pdf?stream=1&streaming' -o out.pdf
How streaming works
flowchart TD
A["HTML chunk"] --> B["Streaming parser"]
B --> C["Style cascade"]
C --> D["Layout + pagination"]
D --> E["PDF writer"]
E --> F["PDF output"]
D -. "on every settled page" .-> E
E -. "free the memory and read on" .-> B
As soon as a page boundary is final, that page is written to the PDF and the memory it used is released before reading on, which is how large documents are processed without memory use growing.