From d061c61fb9184d676de87f7933170fbc4eb1a0a3 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 27 Sep 2026 20:21:05 +0200 Subject: [PATCH] Pick the generic BUCKET, not the whole font family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit css.FontFamily 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 — `Spectral, Georgia, serif`, which is every real page — matched none of the bare buckets and fell through to the default. So every serif and monospace run was being embedded in the sans face. On the A0 poster this module is exercised on, the file went from Inter + Lora + Go Mono down to Inter alone: all its body serif and all its figures' monospace silently reset to a sans face. This is mine, from that type change, and it is a live regression on main — hence a fix of its own rather than a line inside the @font-face work that found it. Both new tests fail on the previous code, verified by restoring fonts.go alone: pick sends a named serif to the sans face, and an exported document naming a serif and a monospace family embeds no Lora and no Go Mono. Co-Authored-By: Claude Opus 5 --- fontpick_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++ fonts.go | 12 +++++++--- go.mod | 6 ++--- go.sum | 12 +++++----- 4 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 fontpick_test.go 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=