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 |
|---|---|
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-of-type, :only-of-type, :nth-last-child(), :nth-last-of-type(), :has(~ ...) | The result changes: they match too much, or miss altogether |
An element directly under <body> has its style settled the moment its next sibling appears. Only selectors that need to know whether more elements of the same kind follow are therefore undecidable. Rewrite rules such as “remove the rule on the last row only” by adding a class and writing .last { … } instead.
Every other sibling selector — +, ~, :first-child, :nth-child(), :last-child and so on — gives the same result as batch mode. See Selectors for the details.
Still available
The following work exactly as they do in the normal mode.
--cover(a cover page),--toc(a table of contents), and--page-offset- Headers and footers,
[page]and[topage]included, HTML ones too counter(page)andcounter(pages)in@pagemargin boxes- 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
Decoration that needs the total page count is written last, as PDF Form XObjects the pages refer to ahead of time. Body pages are still written and released as they settle, so neither seeking nor keeping the whole body is needed. The only extra information kept is a reference and a page number per page, and the decoration is drawn one page at a time as well. No object already written is rewritten, so neither an incremental update nor a second cross-reference table is needed. The PDF is complete once the final cross-reference table and trailer are written.
For the table of contents, only each heading’s text, link target, and body page number are kept, and the body’s layout is released. The table of contents pages are written last, but the finished PDF orders its pages as cover, table of contents, then body. The cover counts neither in the page numbers nor in the total. The table of contents and the body are numbered continuously from 1 + --page-offset, and the total does not include the offset. With a table of contents, the body’s headers, footers, and margin boxes are also drawn once the number of table of contents pages is settled.
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.