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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ body:
id: gpdf-version
attributes:
label: gpdf Version
placeholder: "v1.0.9"
placeholder: "v1.0.11"
validations:
required: true

Expand Down
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions _examples/builder/32_cjk_text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
})
Expand Down Expand Up @@ -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))
})
})
Expand Down Expand Up @@ -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))
})
})
Expand All @@ -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) {
Expand Down Expand Up @@ -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"))
})
})
Expand Down Expand Up @@ -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"))
})
})
Expand Down
6 changes: 3 additions & 3 deletions _examples/gotemplate/32_cjk_text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
]}
]}},
Expand All @@ -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"}
]}
]}},
Expand All @@ -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"}
]}
]}},
Expand Down
6 changes: 3 additions & 3 deletions _examples/json/32_cjk_text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
]}
]}},
Expand All @@ -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"}
]}
]}},
Expand All @@ -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"}
]}
]}},
Expand Down
Binary file modified _examples/testdata/golden/32_cjk_text.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/32d_cjk_mixed.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion internal/buildinfo/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
10 changes: 9 additions & 1 deletion template/fontresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions template/fontresolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package template

import (
"math"
"os"
"path/filepath"
"testing"

"github.com/gpdf-dev/gpdf/document"
Expand Down Expand Up @@ -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) {
Expand Down
Loading