From dd650d3eb0f5776e66ae3258ddce8d0384316af2 Mon Sep 17 00:00:00 2001 From: Taiki Noda Date: Sat, 16 May 2026 14:17:54 +0900 Subject: [PATCH 1/3] fix(template): use registration key for ResolvedFont.ID (#30) --- _examples/builder/32_cjk_text_test.go | 12 +++--- _examples/gotemplate/32_cjk_text_test.go | 6 +-- _examples/json/32_cjk_text_test.go | 6 +-- _examples/testdata/golden/32_cjk_text.pdf | Bin 11677186 -> 11677471 bytes _examples/testdata/golden/32d_cjk_mixed.pdf | Bin 11641189 -> 11641256 bytes template/fontresolver.go | 10 ++++- template/fontresolver_test.go | 43 ++++++++++++++++++++ 7 files changed, 64 insertions(+), 13 deletions(-) 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 61fa9268c812043bc2d1a7c53ab96211746d8994..18e5b3e9a985f330f7e4c31c023280420df530b8 100644 GIT binary patch delta 1506 zcmbW%S5wqb7{>8gU@5Dxh^wf8tYvM$dXj9C6MF;2hJppfu80*Cdqr6l8-gHW$KG88 zF8m672uH_jKY+LXv&=9Xgc-+VelwXT?|V+pnIv`Jdfe^bd)!Sq@lw+}{XgSPp;(46 zWk^#rQ`JL$5&V~^raLa7{QsLPlEVMr{KNChm8dI0DP^-aD_!y1XHAu-_oB3=(VNws zoSN%ORL!7N2d}A2OLn>Spk@qM(%tCIbGc$iyg&#u#KF z8#x$@T=+2#<1qmf5x^u&#uQA&G~^*41qfm~1Qe*y5P}W^CL}D(z)Z}-Y|O!2%)@*v zz(OoSA&RgVOHhoZScc_Tft4u1Dy&8+%CH7&u@37|jt$s|3T(n=Y{6D+!*=YzPV7P@ zcB2Y=uowHV9|v#{)i{L1ID#4+#W5Vm37o_!oW>cP#W|eE1=QjqE};%#T*eh#MLn+J zI&R=5ZlM9U(TFD8K{Hx#7p=I5Hr&SpJj5eB#uGe6JD%Y=Uf?BO;WggiE#BchKHwuh z;WHxWz!!9Wjrh9iGRrK*@<-ooe|4%y2t_eW!<0&FCN!bxmgcAv(zJw7Wo%uv*9e8$ z!uef+T4@=vH|;qL5pp!x18b)4tg|yq(;U6_11Q4mpYd08_DWMqp+@U!h3H)oPb#G< z{wlOo$!BjLeRY;l3`b_KC}dcUl}9tijP12EodILAl-{kBnr!PR=qmPDhV%pig(XFv EKgc=CGXMYp delta 1216 zcmZY5S5p&F7{>8gNCbkBphi@RQH&kj&F-Gv#a=+Mp<=<_#foCVE<#l7q7ku0u=fJF z@FU0!-gxggJjtj99(D$W!anLW#J)Oi9z#m>MWd2C=w5KT({MDT`cLLm;3i5qI`Aw~akx z@nC`15v9V2_6A#eg9_4+4iCKOgA8OM3qE9{FZ}3-90ZVy{uqFPkb4e79tL9wh9VyY zC`1v4A%x)=fntnA7^5&6V=xxuFdh?7f>KO`3JnqHFknK!f(-{{n1sogf~lB>>6n3; zn1$IWM+N3!E-En(^RWO6u?SUIj3ro#YAnNYtiVdtU=>zl4c1~E)?))UViPuF3$|h# zwxbq1uoJtm8+))9b=ZgfsK)^u#339;1CHP*j^Q{?;3Q7rG#YURO=w0GXK@ba(Si%O zh)cMPE4YelxQJ4@`=T!^c2&wW5)%ks@s%gR!h9golRabRWn65h2wrs~T^whd!uWmZ+ zoh9AjMxkk`KjjgoC0q@1W8JXabutr%?dl~n>`3I_%(NVLFPE|`BU#t@cOrRH%ZdC~ z=*XGnt^Q1&oXECanViTlG}q+G%ye9X$jlV3*U0+)l%}b-e<|&*^a)${g~R1l6~14| C^Q310 diff --git a/_examples/testdata/golden/32d_cjk_mixed.pdf b/_examples/testdata/golden/32d_cjk_mixed.pdf index 705e73206a4f4cf52293ebb7722ef3307d0b7975..839e7a68587f724e44a6937e0cb93b56e3fe2b47 100644 GIT binary patch delta 1229 zcma*m=XVTI7{~ELHpCDmdI_T>`sS9oQ-~H4q6JyKt%O8N5WOyf=%NNeM2&7=dCxid zPxw20cZ0K8UbyFcUd(Uindd%p=6?U$p4s-TJ+m=4!x4Vm4uxVu2CndYM@#J~V{8#N z{}I(yTSXD8phVhI*^w$s+JZ_Ym1`489oD`p{w!&`o)>nFFm|NIDilIFs6b_XG}I6c zbwO8TAaR|EZs?9I^nit)=!IF$y+DV+_V( z9P*Hl@tA;#n1sogf~g3j00I&U8V(Fxc<>Qm8m6NVGcXggFdK7Fgt?f9Vw7M$7N8Uh zu?UN?1WQqdWmt|CC`SY#+eFu?d?I#THbc5?iqiRj9^x?7&X!!fw={ z7JIN4`%s7dIDmsVgu^(3qd11+IDwNmg&0ob49=n+=Wreu(144$gv+>stGI^ixPeC8 z#4X&$9o)q|G~qrT;31mv2#@guEqIDnJi~Lmz)QTsYqa4F-r^nJ;{!h86Fz^5hT^)p z+!KzS{NwG~Y)c3!T}K$#PtAlDN(#eaNvw|NgzvdZ2P!p7)^Z}7eze5%V{VXoFHtF7 z)%hKoBkPOwUNQ)L4s>Gu>P3HHw3nVq>36PWPtrIYePh3i6QQU6dQgdfWJYz`Gz00t langs>LE0e6!TCn18_Y_Et};zIt?^RJGuq0_D=sUsegg6QO8o!; delta 1113 zcmZY5S69?P7{>8~WEG<*_68cuqF8VynM@LU0TdNc5gYbivG=knDy|hpT@?#hvAb8^ z>p6TAzlZ;T7n>#L{4Vm$yz|aG?SDFAe}8wxnu>BACDWE+#+fK*oaNoj4jxTZP<<-*oj@(jXl_leb|o!IEX{2 z$6*}7Q5?f@oWMz(!fBkrS)9XpT);(KLIRg@1y|94Yq*XZXv9q`8DH=f-|!tj@bg#FNLRJg zXvece_*wP&rVvtUC0rphGvNqZ1ip%7zNc-+b~CfiT1Tr?No%?|A?(bxu!D5n=oyZX zfv2*2$smw^q&DoYJdvGAX-8|hlT?0J-^BlcL>Ow_ePhr)vz3lqnt`0a@v?_>f~-M0 b_XW}6^15Eq^;5NN=}J@k+Kk7`t1HZZ8uL$G 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) { From 1e22ff1daf642e17367df18fab740649c7ee69a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 05:23:50 +0000 Subject: [PATCH 2/3] chore: bump version to v1.0.11 --- internal/buildinfo/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 37bd709cb43adf8f08eb9980e19f770894d20296 Mon Sep 17 00:00:00 2001 From: Taiki Noda Date: Mon, 18 May 2026 21:04:26 +0900 Subject: [PATCH 3/3] chore: bump version to v1.0.11 --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- CHANGELOG.md | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) 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