diff --git a/fontpick_test.go b/fontpick_test.go new file mode 100644 index 0000000..3209579 --- /dev/null +++ b/fontpick_test.go @@ -0,0 +1,58 @@ +// Copyright (c) the go-pdfkit/html2pdf authors. All rights reserved. +// Use of this source code is governed by a BSD-3-Clause license that can be +// found in the LICENSE file at the root of this repository. + +package html2pdf + +import ( + "bytes" + "testing" + + "github.com/go-webengine/engine/css" +) + +// A page that NAMES its typefaces must still get the right generic bucket for +// each. css.FontFamily used to be an enum of three values, so comparing it was +// comparing the bucket; since it became {Names, Generic} a declaration like +// `Spectral, Georgia, serif` matches none of the bare buckets, and every serif +// and monospace run on the page was being embedded in the sans face. +func TestPickUsesTheGenericBucketNotTheWholeFamily(t *testing.T) { + fs, err := loadFonts() + if err != nil { + t.Fatalf("loadFonts: %v", err) + } + named := func(names string, g css.Generic) css.FontFamily { + return css.FontFamily{Names: names, Generic: g} + } + if got := fs.pick(named("spectral,georgia", css.GenericSerif), false, false); got != fs.serif { + t.Error("a named serif family must still pick the serif face") + } + if got := fs.pick(named("ibm plex mono,menlo", css.GenericMono), false, false); got != fs.mono { + t.Error("a named monospace family must still pick the mono face") + } + if got := fs.pick(named("ibm plex sans", css.GenericSans), true, true); got != fs.sansBI { + t.Error("a named sans family must still pick the bold-italic sans face") + } + // The bare buckets keep working, and the styles still resolve. + if fs.pick(css.Serif, true, false) != fs.serifB || fs.pick(css.Serif, false, true) != fs.serifI { + t.Error("the bare serif bucket lost its styles") + } + if fs.pick(css.Mono, true, true) != fs.mono { + t.Error("mono has one face for every style") + } +} + +// End to end: a document naming a serif and a monospace family must embed +// three families, not one. +func TestExportEmbedsAllThreeBucketsForNamedFamilies(t *testing.T) { + pdf := exportBytes(t, ` +

serif text

+

mono text

+

sans text

+`, Options{}) + for _, want := range []string{"Lora", "GoMono", "Inter"} { + if !bytes.Contains(pdf, []byte(want)) { + t.Errorf("the file names no %s face — a named family took the wrong bucket", want) + } + } +} diff --git a/fonts.go b/fonts.go index 2a4994f..1d33ecb 100644 --- a/fonts.go +++ b/fonts.go @@ -53,8 +53,14 @@ func loadFonts() (*fontSet, error) { // pick returns the loaded face matching a CSS font-family/weight/style // request. func (fs *fontSet) pick(fam css.FontFamily, bold, italic bool) *pdfkit.Font { - switch fam { - case css.Serif: + // The GENERIC bucket, not the whole FontFamily. It used to be an enum of + // three values, so comparing the value itself was comparing the bucket; + // since it became {Names, Generic} (go-webengine/engine#233) a declaration + // that NAMES a family matched none of these cases and fell through to + // sans. Every serif and monospace run on a page that names its typefaces — + // which is every real page — was being embedded in Inter. + switch fam.Generic { + case css.GenericSerif: switch { case bold && italic: return fs.serifBI @@ -65,7 +71,7 @@ func (fs *fontSet) pick(fam css.FontFamily, bold, italic bool) *pdfkit.Font { default: return fs.serif } - case css.Mono: + case css.GenericMono: return fs.mono default: switch { diff --git a/go.mod b/go.mod index 1f5517f..5280a4c 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,12 @@ require ( github.com/go-gfx/gfx v0.26.0 github.com/go-opentype/fonts v0.9.1-0.20260907071658-dd9656234a3d github.com/go-pdfkit/pdfkit v0.12.0 - github.com/go-webengine/engine v0.3.12-0.20260927154504-daef37e1453e + github.com/go-webengine/engine v0.3.12-0.20260927181450-a8537ad23fd3 ) require ( github.com/ajroetker/go-highway v0.0.4 // indirect - github.com/andybalholm/brotli v1.2.3 // indirect + github.com/andybalholm/brotli v1.2.5 // 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 @@ -23,7 +23,7 @@ require ( github.com/go-icons/iconoir v0.2.0 // indirect github.com/go-images/images v0.0.0-20260912071231-12ebfec58453 // indirect github.com/go-images/jpeg2000 v0.1.0 // indirect - github.com/go-opentype/opentype v0.12.0 // indirect + github.com/go-opentype/opentype v0.13.1-0.20260927180318-ae6327b14eac // 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 diff --git a/go.sum b/go.sum index 06098f3..4f3a1ae 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAw github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/ajroetker/go-highway v0.0.4 h1:RDQo+9OhTXI6BFctLo+5gYpHNbb92VYJ0ObnR4l6xAQ= github.com/ajroetker/go-highway v0.0.4/go.mod h1:C/zYPNSSpOaraejY89FUTZTyQNEhi5+rEbU0LjlqJeU= -github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ= -github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.2.5 h1:BSI8V4zmx/3BAn6OKjF1PmfVq7Aoi52AdFsi6bpCx+s= +github.com/andybalholm/brotli v1.2.5/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/breml/rootcerts v0.3.7 h1:KZZkmd591bmq//te85L0u33zgtI3phyGlfD8u7/pzUg= github.com/breml/rootcerts v0.3.7/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -36,8 +36,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-opentype/fonts v0.9.1-0.20260907071658-dd9656234a3d h1:9WvCVDUtnMBVmviCyC65IGN1Sun3k/goiB7wsp/sbjM= github.com/go-opentype/fonts v0.9.1-0.20260907071658-dd9656234a3d/go.mod h1:b+o8v1zDZ6B3GixA398jGILAh09+WwfyoYz52zKe7DA= -github.com/go-opentype/opentype v0.12.0 h1:wBlcDi+3ZaNZXEt5z+Ixr11/cYYwi5W+jX6yTl/qr1I= -github.com/go-opentype/opentype v0.12.0/go.mod h1:AOixevJf7XQaH7+WG+OMIOZEbYPXfMqklVk26Y6YTUU= +github.com/go-opentype/opentype v0.13.1-0.20260927180318-ae6327b14eac h1:mpzfJQe1RlF2zawKqvL5poyNdaV6pMbHkK1h+GVLMuM= +github.com/go-opentype/opentype v0.13.1-0.20260927180318-ae6327b14eac/go.mod h1:UjZOTCMtkHheDPzfP0ZghDoQ7wIr6L72T/kuC9Ph1hw= github.com/go-opentype/shape v0.5.0 h1:jHNaOMHNBdDj5EixOevlrrsi92svMxvVMKN7GYaPfxo= github.com/go-opentype/shape v0.5.0/go.mod h1:3ImRYNIj6zpwWQ/DV3BWhgMFfmzDITH6XpZw2auQHTo= github.com/go-pdfkit/pdfkit v0.12.0 h1:5hot3DuFYCMT2IyGuPOPE+af7mpEP1tPHqdQ+jdTFwk= @@ -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.20260927154504-daef37e1453e h1:IHrx6cOpcctfYq8qatiwz9ol3hzb0qfkBS7lMij035Q= -github.com/go-webengine/engine v0.3.12-0.20260927154504-daef37e1453e/go.mod h1:wnQ2Uu6d6cXk6+XhfKlh6haFFCP/NwYpzSB/T6BOOa0= +github.com/go-webengine/engine v0.3.12-0.20260927181450-a8537ad23fd3 h1:UBUDj7EESkGF0iD8IncSGvJGkdKX2vEsxB/IPs9YiSg= +github.com/go-webengine/engine v0.3.12-0.20260927181450-a8537ad23fd3/go.mod h1:I1A4+nao2S/mKsq2Kd1PqedmYOXpKPFBCxXJqAtQ1MU= 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=