Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ doc.Write(f)

Or from the shell:

```
```sh
go run ./cmd/html2pdf -in report.html -out report.pdf
```

Expand Down Expand Up @@ -56,16 +56,20 @@ package bundles — Inter (sans), Lora (serif), Go Mono (mono) — so the glyphs
drawn always match the metrics the layout pass measured against; there is no
web-font fetch to fail silently.

Two gaps, both inherited from — not introduced by — the layout engine:
Images — raster `<img>`, `<img src="*.svg">` and inline `<svg>` — go through
the engine's own fetch/decode/size pipeline (`Engine.LoadImages`) and are
embedded as bitmaps, so they're laid out and drawn exactly as the engine's
raster canvas would draw them. A relative `src` resolves against
`Options.BaseURL`; an image that fails to fetch or decode is simply left out,
as on the raster canvas. This is the one place `Export` touches the network.

One gap remains, inherited from — not introduced by — the layout engine:

- **Inline-level background/border/padding does not paint.** A styled
`<span>` never gets its own box in go-webengine's layout (confirmed against
its reference raster painter too — a shared engine limitation). Style the
*containing* block/table-cell instead of an inner inline element when you
need a filled badge or pill.
- **Inline `<svg>` and `<img>` are not painted yet.** Build charts from plain
block/table markup (backgrounds, borders, percentage widths) rather than
inline SVG until this lands.

## Status

Expand Down
44 changes: 36 additions & 8 deletions corpus/CORPUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

| URL | Status | Pages | PDF | Text chars | Fetch | Render |
|---|---|---|---|---|---|---|
| [https://example.com/](https://example.com/) | ✅ | 1 | 26601 B | 127 | 41ms | 63ms |
| [https://en.wikipedia.org/wiki/Go_(programming_language)](https://en.wikipedia.org/wiki/Go_(programming_language)) | ✅ | 18 | 3694522 B | 63058 | 186ms | 178ms |
| [https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)](https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)) | ✅ | 9 | 1389439 B | 22970 | 54ms | 230ms |
| [https://go.dev/blog/subtests](https://go.dev/blog/subtests) | ✅ | 6 | 849596 B | 13470 | 446ms | 57ms |
| [https://pkg.go.dev/net/http](https://pkg.go.dev/net/http) | ✅ | 49 | 8516859 B | 150969 | 233ms | 177ms |
| [https://www.rfc-editor.org/rfc/rfc9110.html](https://www.rfc-editor.org/rfc/rfc9110.html) | ✅ | 120 | 28978109 B | 449957 | 300ms | 675ms |
| [https://news.ycombinator.com/](https://news.ycombinator.com/) | ✅ | 2 | 248721 B | 3985 | 450ms | 45ms |
| [https://react.dev/](https://react.dev/) | ✅ | 5 | 526645 B | 7965 | 91ms | 58ms |
| [https://example.com/](https://example.com/) | ✅ | 1 | 26601 B | 127 | 38ms | 31ms |
| [https://en.wikipedia.org/wiki/Go_(programming_language)](https://en.wikipedia.org/wiki/Go_(programming_language)) | ✅ | 18 | 3737845 B | 63064 | 140ms | 244ms |
| [https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)](https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)) | ✅ | 9 | 1402676 B | 22953 | 43ms | 595ms |
| [https://go.dev/blog/subtests](https://go.dev/blog/subtests) | ✅ | 6 | 984773 B | 13478 | 166ms | 601ms |
| [https://pkg.go.dev/net/http](https://pkg.go.dev/net/http) | ✅ | 49 | 8656699 B | 150974 | 207ms | 1109ms |
| [https://www.rfc-editor.org/rfc/rfc9110.html](https://www.rfc-editor.org/rfc/rfc9110.html) | ✅ | 120 | 28978109 B | 449957 | 241ms | 464ms |
| [https://news.ycombinator.com/](https://news.ycombinator.com/) | ✅ | 1 | 257685 B | 4074 | 467ms | 471ms |
| [https://react.dev/](https://react.dev/) | ✅ | 12 | 9095777 B | 7977 | 47ms | 558ms |

<!-- BEGIN ANALYSIS -->

Expand Down Expand Up @@ -67,6 +67,34 @@ and `out/news-ycombinator-com-p1-1.png` after the fix both show full-width,
readable text at a normal size — the scale-down doesn't make anything too
small to read at these ratios (642/1024 ≈ 0.63×).

### Images now painted — `<img>`, `<img src="*.svg">` and inline `<svg>`

Both image gaps the README used to document are closed in one step. Rather
than re-implement fetch/decode/budgeting, `Export` calls the engine's own
pipeline — `Engine.LoadImages`, exported for exactly this in
go-webengine/engine#114 — and hands its intrinsic-size map to
`layout.LayoutDocument` and its bitmap map to the PDF painter. So an image is
laid out at, and drawn at, precisely the size the engine's raster canvas would
use; an inline `<svg>` arrives already rasterised by the same path.

Embedded image XObjects per page, this run: Wikipedia (Go) 30, countries
list 10, `go.dev/blog` 40, Hacker News 6, `pkg.go.dev/net/http` 88,
`react.dev` 166, `example.com` 0 and RFC 9110 0 (neither carries an `<img>`).
Wikipedia's logo and react.dev's logo/icons land in the right place at the
right size on their page-1 previews. Text length is unchanged everywhere.

Two things to read correctly in the table above after this change:

- **`react.dev` grew 5→12 pages and 0.5→9 MB.** That is 166 icon-sized
inline SVGs, each rasterised and stored as raw FlateDecode RGB samples —
pdfkit does not JPEG-encode or deduplicate identical bitmaps. Correct, but
heavy; deduplicating repeated bitmaps into one shared XObject is the
obvious next saving.
- **Render times rose** (`pkg.go.dev` 177→1109 ms, countries 143→595 ms):
image fetch + decode now runs inside `Export`, so the Render column is
network-bound for image-bearing pages. The Fetch column still measures only
the page's own HTML.

### Confirmed-expected: `react.dev` shows only its static shell

7 pages of real content (nav, hero, first section, one code sample) — the
Expand Down
9 changes: 5 additions & 4 deletions corpus/cmd/corpus/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,15 @@ func pdfTextChars(path string) int {

// renderWithRecover isolates a panic in the layout/paint pipeline to one
// corpus entry instead of aborting the whole run — real-world pages are
// exactly where an unanticipated shape shows up first.
func renderWithRecover(html string) (pdfBytes []byte, err error) {
// exactly where an unanticipated shape shows up first. baseURL lets a
// relative <img src> resolve, so image-bearing pages are exercised for real.
func renderWithRecover(html, baseURL string) (pdfBytes []byte, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
doc, exportErr := html2pdf.Export(html, html2pdf.Options{})
doc, exportErr := html2pdf.Export(html, html2pdf.Options{BaseURL: baseURL})
if exportErr != nil {
return nil, exportErr
}
Expand Down Expand Up @@ -149,7 +150,7 @@ func run() int {
r.HTMLBytes = len(doc.HTML)

t1 := time.Now()
pdfBytes, err := renderWithRecover(doc.HTML)
pdfBytes, err := renderWithRecover(doc.HTML, doc.URL)
r.RenderMs = time.Since(t1).Milliseconds()
if err != nil {
r.Error = "render: " + err.Error()
Expand Down
2 changes: 1 addition & 1 deletion corpus/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.26.4

require (
github.com/go-pdfkit/html2pdf v0.0.0
github.com/go-webengine/engine v0.3.12-0.20260904101840-577bc2930954
github.com/go-webengine/engine v0.3.12-0.20260904140318-e12c4d72307a
)

require (
Expand Down
4 changes: 2 additions & 2 deletions corpus/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TC
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-typeset/bidi v0.3.0 h1:4fjGjejvjE2LzLNzY4si8PkVO321NcsKIiANhWT3jF4=
github.com/go-typeset/bidi v0.3.0/go.mod h1:ct3cmYT8Qt1FGJQ+2QaakrUxqaP4ZysgdQQg2ym5+Xo=
github.com/go-webengine/engine v0.3.12-0.20260904101840-577bc2930954 h1:o8bNYAjmFwthTEh2A+6yZgtGrOWpv6ky3uCpbNrl/9E=
github.com/go-webengine/engine v0.3.12-0.20260904101840-577bc2930954/go.mod h1:CAI39d/IqtDZMjjh9NzyVNTMXxUc/miXgweCo8vG2iI=
github.com/go-webengine/engine v0.3.12-0.20260904140318-e12c4d72307a h1:uu/Ojq18zvxh6ogjgRvVJt8e7jTXmari37Zr3hFME/s=
github.com/go-webengine/engine v0.3.12-0.20260904140318-e12c4d72307a/go.mod h1:CAI39d/IqtDZMjjh9NzyVNTMXxUc/miXgweCo8vG2iI=
github.com/go-webengine/esbuildsandbox v0.1.0 h1:1LdmX+0/yJarQlxZgJ7JIMu+Qy6P7Yzu4NFcX9+jFl8=
github.com/go-webengine/esbuildsandbox v0.1.0/go.mod h1:za8oj1vbDfVkTpHLeRk7JfEQjmJfj+GHNq4R47681vQ=
github.com/go-widgets/mvvm v0.9.0 h1:NwW/hAAsJUGlxmNt0ErQ7fUrud3w5Ys1RWm7hVugdJs=
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified corpus/out/go-dev-blog-subtests-p1-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified corpus/out/news-ycombinator-com-p1-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified corpus/out/pkg-go-dev-net-http-p1-01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added corpus/out/react-dev-p1-01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 31 additions & 31 deletions corpus/results.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"url": "https://example.com/",
"slug": "example-com",
"ok": true,
"fetch_ms": 41,
"render_ms": 63,
"fetch_ms": 38,
"render_ms": 31,
"pdf_bytes": 26601,
"pages": 1,
"text_chars": 127,
Expand All @@ -14,52 +14,52 @@
"url": "https://en.wikipedia.org/wiki/Go_(programming_language)",
"slug": "en-wikipedia-org-wiki-Go_programming_language",
"ok": true,
"fetch_ms": 186,
"render_ms": 178,
"pdf_bytes": 3694522,
"fetch_ms": 140,
"render_ms": 244,
"pdf_bytes": 3737845,
"pages": 18,
"text_chars": 63058,
"text_chars": 63064,
"html_bytes": 734710
},
{
"url": "https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)",
"slug": "en-wikipedia-org-wiki-List_of_countries_by_population_United_Nations",
"ok": true,
"fetch_ms": 54,
"render_ms": 230,
"pdf_bytes": 1389439,
"fetch_ms": 43,
"render_ms": 595,
"pdf_bytes": 1402676,
"pages": 9,
"text_chars": 22970,
"text_chars": 22953,
"html_bytes": 773512
},
{
"url": "https://go.dev/blog/subtests",
"slug": "go-dev-blog-subtests",
"ok": true,
"fetch_ms": 446,
"render_ms": 57,
"pdf_bytes": 849596,
"fetch_ms": 166,
"render_ms": 601,
"pdf_bytes": 984773,
"pages": 6,
"text_chars": 13470,
"text_chars": 13478,
"html_bytes": 46512
},
{
"url": "https://pkg.go.dev/net/http",
"slug": "pkg-go-dev-net-http",
"ok": true,
"fetch_ms": 233,
"render_ms": 177,
"pdf_bytes": 8516859,
"fetch_ms": 207,
"render_ms": 1109,
"pdf_bytes": 8656699,
"pages": 49,
"text_chars": 150969,
"text_chars": 150974,
"html_bytes": 482331
},
{
"url": "https://www.rfc-editor.org/rfc/rfc9110.html",
"slug": "www-rfc-editor-org-rfc-rfc9110-html",
"ok": true,
"fetch_ms": 300,
"render_ms": 675,
"fetch_ms": 241,
"render_ms": 464,
"pdf_bytes": 28978109,
"pages": 120,
"text_chars": 449957,
Expand All @@ -69,22 +69,22 @@
"url": "https://news.ycombinator.com/",
"slug": "news-ycombinator-com",
"ok": true,
"fetch_ms": 450,
"render_ms": 45,
"pdf_bytes": 248721,
"pages": 2,
"text_chars": 3985,
"html_bytes": 34445
"fetch_ms": 467,
"render_ms": 471,
"pdf_bytes": 257685,
"pages": 1,
"text_chars": 4074,
"html_bytes": 34632
},
{
"url": "https://react.dev/",
"slug": "react-dev",
"ok": true,
"fetch_ms": 91,
"render_ms": 58,
"pdf_bytes": 526645,
"pages": 5,
"text_chars": 7965,
"fetch_ms": 47,
"render_ms": 558,
"pdf_bytes": 9095777,
"pages": 12,
"text_chars": 7977,
"html_bytes": 272458
}
]
15 changes: 14 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ go 1.26.4
require (
github.com/go-opentype/fonts v0.9.0
github.com/go-pdfkit/pdfkit v0.9.0
github.com/go-webengine/engine v0.3.12-0.20260904101840-577bc2930954
github.com/go-webengine/engine v0.3.12-0.20260904140318-e12c4d72307a
)

require (
github.com/ajroetker/go-highway v0.0.4 // indirect
github.com/ajroetker/go-jpeg2000 v0.0.2 // indirect
github.com/andybalholm/brotli v1.2.3 // indirect
github.com/breml/rootcerts v0.3.7 // indirect
github.com/coder/websocket v1.8.15 // indirect
github.com/dlclark/regexp2/v2 v2.5.2 // indirect
github.com/dop251/goja v0.0.0-20260826204918-8f1c0696a37b // indirect
github.com/evanw/esbuild v0.28.2 // indirect
github.com/go-browserhttp/browserhttp v0.2.0 // indirect
github.com/go-crdt/collab v0.37.0 // indirect
github.com/go-crdt/crdt v0.39.0 // indirect
github.com/go-gfx/gfx v0.19.0 // indirect
Expand All @@ -21,13 +26,21 @@ require (
github.com/go-opentype/opentype v0.12.0 // indirect
github.com/go-opentype/shape v0.5.0 // indirect
github.com/go-richdoc/richdoc v0.3.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/go-typeset/bidi v0.3.0 // indirect
github.com/go-webengine/esbuildsandbox v0.1.0 // indirect
github.com/go-widgets/mvvm v0.9.0 // indirect
github.com/go-widgets/painter v0.12.0 // indirect
github.com/go-widgets/toolkit v0.301.0 // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
github.com/sergeymakinen/go-ico v1.0.0 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/tannevaled/gobig2 v0.1.0 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/image v0.45.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/sys v0.47.0 // indirect
Expand Down
Loading
Loading