diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6477184..a0199a2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -63,7 +63,7 @@ body: id: gpdf-version attributes: label: gpdf Version - placeholder: "v1.0.9" + placeholder: "v1.0.11" validations: required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4430a..3ad44c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [1.0.11] - 2026-05-18 + +### Fixed +- `ResolvedFont.ID` now uses the font registration key instead of the resolved family name, so per-family overrides (e.g. registering a CJK font under a custom key) are honored at render time (#30) + - `template/fontresolver.go`: the resolver stores the input registration key on `ResolvedFont.ID` so downstream font lookups in the renderer match the registered font, not the resolved family fallback + - `template/fontresolver_test.go`: regression coverage for registration-key vs family-name ID + - Golden refresh: `_examples/testdata/golden/32_cjk_text.pdf` and `32d_cjk_mixed.pdf` regenerated to reflect the corrected font ID embedded in the content stream + +## [1.0.10] - 2026-05-07 + ### Added - Per-column horizontal text alignment for tables (#26) - Builder: `template.ColumnAlign(aligns ...document.TextAlign)` — sets the horizontal alignment for each column in both the header and body. Columns without a provided alignment fall back to the default left alignment. @@ -172,7 +182,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - Reed-Solomon coefficient order in QR code encoder - binary.Write return value handling for errcheck lint -[Unreleased]: https://github.com/gpdf-dev/gpdf/compare/v1.0.9...HEAD +[Unreleased]: https://github.com/gpdf-dev/gpdf/compare/v1.0.11...HEAD +[1.0.11]: https://github.com/gpdf-dev/gpdf/compare/v1.0.10...v1.0.11 +[1.0.10]: https://github.com/gpdf-dev/gpdf/compare/v1.0.9...v1.0.10 [1.0.9]: https://github.com/gpdf-dev/gpdf/compare/v1.0.8...v1.0.9 [1.0.8]: https://github.com/gpdf-dev/gpdf/compare/v1.0.7...v1.0.8 [1.0.7]: https://github.com/gpdf-dev/gpdf/compare/v1.0.6...v1.0.7 diff --git a/_examples/builder/32_cjk_text_test.go b/_examples/builder/32_cjk_text_test.go index c3555e9..9a83216 100644 --- a/_examples/builder/32_cjk_text_test.go +++ b/_examples/builder/32_cjk_text_test.go @@ -80,7 +80,7 @@ func TestExample_32_CJK_Text(t *testing.T) { page.AutoRow(func(r *template.RowBuilder) { r.Col(12, func(c *template.ColBuilder) { c.Spacer(document.Mm(3)) - c.Line(template.LineThickness(document.Pt(0.5)), template.LineColor(pdf.Gray(0.7))) + c.Line(template.LineThickness(document.Pt(0.5)), template.LineColor(pdf.RGBHex(0xB3B3B3))) c.Spacer(document.Mm(3)) }) }) @@ -110,7 +110,7 @@ func TestExample_32_CJK_Text(t *testing.T) { page.AutoRow(func(r *template.RowBuilder) { r.Col(12, func(c *template.ColBuilder) { c.Spacer(document.Mm(3)) - c.Line(template.LineThickness(document.Pt(0.5)), template.LineColor(pdf.Gray(0.7))) + c.Line(template.LineThickness(document.Pt(0.5)), template.LineColor(pdf.RGBHex(0xB3B3B3))) c.Spacer(document.Mm(3)) }) }) @@ -140,7 +140,7 @@ func TestExample_32_CJK_Text(t *testing.T) { page.AutoRow(func(r *template.RowBuilder) { r.Col(12, func(c *template.ColBuilder) { c.Spacer(document.Mm(3)) - c.Line(template.LineThickness(document.Pt(0.5)), template.LineColor(pdf.Gray(0.7))) + c.Line(template.LineThickness(document.Pt(0.5)), template.LineColor(pdf.RGBHex(0xB3B3B3))) c.Spacer(document.Mm(3)) }) }) @@ -155,7 +155,7 @@ func TestExample_32_CJK_Text(t *testing.T) { }) headerStyle := []template.TextOption{ - template.Bold(), template.TextColor(pdf.White), template.BgColor(pdf.RGBHex(0x37474F)), + template.Bold(), template.TextColor(pdf.RGBHex(0xFFFFFF)), template.BgColor(pdf.RGBHex(0x37474F)), } // Header row page.AutoRow(func(r *template.RowBuilder) { @@ -191,7 +191,7 @@ func TestExample_32_CJK_Text(t *testing.T) { r.Col(12, func(c *template.ColBuilder) { c.Spacer(document.Mm(5)) c.Text("CJK characters are fully supported through TrueType font embedding.", - template.AlignCenter(), template.Italic(), template.TextColor(pdf.Gray(0.5)), + template.AlignCenter(), template.Italic(), template.TextColor(pdf.RGBHex(0x808080)), template.FontFamily("NotoSansJP")) }) }) @@ -399,7 +399,7 @@ func TestExample_32d_CJK_Mixed(t *testing.T) { r.Col(12, func(c *template.ColBuilder) { c.Spacer(document.Mm(5)) c.Text("CJK characters are fully supported through TrueType font embedding.", - template.AlignCenter(), template.Italic(), template.TextColor(pdf.Gray(0.5)), + template.AlignCenter(), template.Italic(), template.TextColor(pdf.RGBHex(0x808080)), template.FontFamily("NotoSansJP")) }) }) diff --git a/_examples/gotemplate/32_cjk_text_test.go b/_examples/gotemplate/32_cjk_text_test.go index 6300a07..45a0f78 100644 --- a/_examples/gotemplate/32_cjk_text_test.go +++ b/_examples/gotemplate/32_cjk_text_test.go @@ -56,7 +56,7 @@ func TestTmpl_32_CJK_Text(t *testing.T) { {"row": {"cols": [ {"span": 12, "elements": [ {"type": "spacer", "height": "3mm"}, - {"type": "line", "thickness": "0.5pt", "color": "#B3B3B3"}, + {"type": "line", "line": {"thickness": "0.5pt", "color": "#B3B3B3"}}, {"type": "spacer", "height": "3mm"} ]} ]}}, @@ -81,7 +81,7 @@ func TestTmpl_32_CJK_Text(t *testing.T) { {"row": {"cols": [ {"span": 12, "elements": [ {"type": "spacer", "height": "3mm"}, - {"type": "line", "thickness": "0.5pt", "color": "#B3B3B3"}, + {"type": "line", "line": {"thickness": "0.5pt", "color": "#B3B3B3"}}, {"type": "spacer", "height": "3mm"} ]} ]}}, @@ -106,7 +106,7 @@ func TestTmpl_32_CJK_Text(t *testing.T) { {"row": {"cols": [ {"span": 12, "elements": [ {"type": "spacer", "height": "3mm"}, - {"type": "line", "thickness": "0.5pt", "color": "#B3B3B3"}, + {"type": "line", "line": {"thickness": "0.5pt", "color": "#B3B3B3"}}, {"type": "spacer", "height": "3mm"} ]} ]}}, diff --git a/_examples/json/32_cjk_text_test.go b/_examples/json/32_cjk_text_test.go index ad0a8fb..ef44345 100644 --- a/_examples/json/32_cjk_text_test.go +++ b/_examples/json/32_cjk_text_test.go @@ -56,7 +56,7 @@ func TestJSON_32_CJK_Text(t *testing.T) { {"row": {"cols": [ {"span": 12, "elements": [ {"type": "spacer", "height": "3mm"}, - {"type": "line", "thickness": "0.5pt", "color": "#B3B3B3"}, + {"type": "line", "line": {"thickness": "0.5pt", "color": "#B3B3B3"}}, {"type": "spacer", "height": "3mm"} ]} ]}}, @@ -81,7 +81,7 @@ func TestJSON_32_CJK_Text(t *testing.T) { {"row": {"cols": [ {"span": 12, "elements": [ {"type": "spacer", "height": "3mm"}, - {"type": "line", "thickness": "0.5pt", "color": "#B3B3B3"}, + {"type": "line", "line": {"thickness": "0.5pt", "color": "#B3B3B3"}}, {"type": "spacer", "height": "3mm"} ]} ]}}, @@ -106,7 +106,7 @@ func TestJSON_32_CJK_Text(t *testing.T) { {"row": {"cols": [ {"span": 12, "elements": [ {"type": "spacer", "height": "3mm"}, - {"type": "line", "thickness": "0.5pt", "color": "#B3B3B3"}, + {"type": "line", "line": {"thickness": "0.5pt", "color": "#B3B3B3"}}, {"type": "spacer", "height": "3mm"} ]} ]}}, diff --git a/_examples/testdata/golden/32_cjk_text.pdf b/_examples/testdata/golden/32_cjk_text.pdf index 61fa926..18e5b3e 100644 Binary files a/_examples/testdata/golden/32_cjk_text.pdf and b/_examples/testdata/golden/32_cjk_text.pdf differ diff --git a/_examples/testdata/golden/32d_cjk_mixed.pdf b/_examples/testdata/golden/32d_cjk_mixed.pdf index 705e732..839e7a6 100644 Binary files a/_examples/testdata/golden/32d_cjk_mixed.pdf and b/_examples/testdata/golden/32d_cjk_mixed.pdf differ diff --git a/internal/buildinfo/version.go b/internal/buildinfo/version.go index 86ec36d..6e42753 100644 --- a/internal/buildinfo/version.go +++ b/internal/buildinfo/version.go @@ -3,4 +3,4 @@ package buildinfo // Version is the library version. It is the single source of truth used by // the public gpdf.Version constant and the default PDF Producer metadata. -const Version = "1.0.10" +const Version = "1.0.11" diff --git a/template/fontresolver.go b/template/fontresolver.go index fc6a864..cce1398 100644 --- a/template/fontresolver.go +++ b/template/fontresolver.go @@ -34,9 +34,17 @@ func (r *builtinFontResolver) Resolve(family string, weight document.FontWeight, variantID := buildFontVariantID(family, bold, italic) // Try variant-specific font first, then fall back to base family. + // Track which registration key actually resolved so that MeasureString + // and LineBreak can look the font back up in r.fonts. Using ttf.Name() + // (the PostScript name) here breaks that lookup when the caller + // registered the font under a different key via WithFont. + resolvedKey := variantID ttf, ok := r.fonts[variantID] if !ok { ttf, ok = r.fonts[family] + if ok { + resolvedKey = family + } } if !ok { @@ -70,7 +78,7 @@ func (r *builtinFontResolver) Resolve(family string, weight document.FontWeight, m := ttf.Metrics() scale := 1.0 / float64(m.UnitsPerEm) return layout.ResolvedFont{ - ID: ttf.Name(), + ID: resolvedKey, Metrics: layout.FontMetrics{ Ascender: float64(m.Ascender) * scale, Descender: float64(m.Descender) * scale, diff --git a/template/fontresolver_test.go b/template/fontresolver_test.go index e930dc0..58729b6 100644 --- a/template/fontresolver_test.go +++ b/template/fontresolver_test.go @@ -2,6 +2,8 @@ package template import ( "math" + "os" + "path/filepath" "testing" "github.com/gpdf-dev/gpdf/document" @@ -68,6 +70,47 @@ func TestFontResolver_Resolve_Standard14Metrics(t *testing.T) { } } +// Regression for #30: ResolvedFont.ID must match the registration key, not +// the TrueType PostScript name. When they diverge (e.g. WithFont("NotoSansJP", +// data) where the PS name is "NotoSansJP-Regular"), MeasureString and +// LineBreak look up r.fonts[f.ID] and silently fall back to approximate +// metrics, which makes layout disagree with the embedded glyphs. +func TestFontResolver_Resolve_IDMatchesRegistrationKey(t *testing.T) { + // Use the same Noto fixture as the CJK example tests. Skip when absent + // so this doesn't gate CI environments that don't ship the font. + path := filepath.Join("..", "..", "NotoSansJP-Regular.ttf") + data, err := os.ReadFile(path) + if err != nil { + t.Skipf("font fixture not found: %s", path) + } + ttf, err := font.ParseTrueType(data) + if err != nil { + t.Fatalf("ParseTrueType: %v", err) + } + // Sanity: this fixture must actually exhibit the PS-name/key divergence, + // otherwise the test wouldn't be exercising the bug. + if ttf.Name() == "NotoSansJP" { + t.Skip("fixture PS name matches registration key; no divergence to test") + } + + r := newBuiltinFontResolver(map[string]*font.TrueTypeFont{ + "NotoSansJP": ttf, + }) + f := r.Resolve("NotoSansJP", document.WeightNormal, false) + + if f.ID != "NotoSansJP" { + t.Errorf("ResolvedFont.ID = %q, want %q (registration key, not PS name)", f.ID, "NotoSansJP") + } + + // MeasureString must dispatch to the registered TTF, not the + // approximate fallback (which would yield runes * size * 0.5). + got := r.MeasureString(f, "あ", 12) + approx := 1.0 * 12 * 0.5 + if math.Abs(got-approx) < 0.001 { + t.Errorf("MeasureString fell back to approximation (%.3f); ID lookup missed registered TTF", got) + } +} + // Italic Helvetica must resolve to the same metrics as Helvetica-Oblique // (buildFontVariantID emits "-Italic"; Standard14 data aliases it). func TestFontResolver_Resolve_HelveticaItalicAliasedToOblique(t *testing.T) {