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.4"
placeholder: "v1.0.5"
validations:
required: true

Expand Down
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [1.0.5] - 2026-04-19

### Fixed
- Text alignment precision for Standard 14 fonts — right/center alignment now lands at the true container edge instead of drifting up to ~17pt for large bold text
- `template/fontresolver.go`: `MeasureString` and `LineBreak` now use Adobe Core 14 AFM advance widths when no TTF is registered, instead of the `charCount × size × 0.5` approximation
- `Resolve` now normalizes an empty `FontFamily` to `Helvetica`, matching the PDF renderer's default
- Metrics (Ascender / Descender / CapHeight) from AFM are returned for Standard 14 fonts — previously hard-coded 0.8 / -0.2 / 0.7 fallback

### Added
- `pdf/font/standard14.go` — Adobe Standard 14 font constants, `IsStandard14`, `Standard14Metrics`, `Standard14Width`, `NewStandard14Font`
- `pdf/font/standard14_data.go` — AFM-derived width tables and metrics for Helvetica / Times / Courier / Symbol / ZapfDingbats families (14 fonts, printable ASCII coverage)
- Tests: `pdf/font/standard14_test.go`, `template/fontresolver_test.go`

## [1.0.4] - 2026-04-07

### Added
Expand Down Expand Up @@ -119,7 +132,8 @@ 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.4...HEAD
[Unreleased]: https://github.com/gpdf-dev/gpdf/compare/v1.0.5...HEAD
[1.0.5]: https://github.com/gpdf-dev/gpdf/compare/v1.0.4...v1.0.5
[1.0.4]: https://github.com/gpdf-dev/gpdf/compare/v1.0.3...v1.0.4
[1.0.3]: https://github.com/gpdf-dev/gpdf/compare/v1.0.2...v1.0.3
[1.0.2]: https://github.com/gpdf-dev/gpdf/compare/v1.0.1...v1.0.2
Expand Down
2 changes: 1 addition & 1 deletion _benchmark/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ replace github.com/gpdf-dev/gpdf => ../
require (
github.com/go-pdf/fpdf v1.0.4
github.com/johnfercher/maroto/v2 v2.3.3
github.com/gpdf-dev/gpdf v1.0.4
github.com/gpdf-dev/gpdf v1.0.5
github.com/signintech/gopdf v0.36.0
golang.org/x/image v0.18.0
)
Expand Down
Binary file modified _examples/component/testdata/golden/01_invoice.pdf
Binary file not shown.
Binary file modified _examples/component/testdata/golden/02_report.pdf
Binary file not shown.
Binary file modified _examples/component/testdata/golden/03_letter.pdf
Binary file not shown.
Binary file modified _examples/gotemplate/testdata/golden/00_from_template.pdf
Binary file not shown.
Binary file modified _examples/gotemplate/testdata/golden/00_invoice.pdf
Binary file not shown.
Binary file modified _examples/json/testdata/golden/00_overview.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/02_text_styling.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/03_grid_layout.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/04_fixed_height_row.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/08_table_styled.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/12_multi_page.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/13_header_footer.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/16_margins.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/17_colors.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/18_invoice.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/19_report.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/21_facade.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/23_text_indent.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/24_text_decoration.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/25_table_vertical_align.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/26_page_number.pdf
Binary file not shown.
Binary file modified _examples/testdata/golden/29_qr_barcode_invoice.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.4"
const Version = "1.0.5"
106 changes: 106 additions & 0 deletions pdf/font/standard14.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package font

// Adobe Standard 14 font names. PDF viewers render these without any
// embedded font data, using their own built-in metrics. gpdf writes text
// with these fonts as Type1 font entries (see pdf/writer.go:RegisterFont);
// layout measurement must match the AFM widths that viewers use so that
// right/center alignment lands at the same coordinate the PDF viewer
// actually draws the glyphs at.
const (
Helvetica = "Helvetica"
HelveticaBold = "Helvetica-Bold"
HelveticaOblique = "Helvetica-Oblique"
HelveticaBoldOblique = "Helvetica-BoldOblique"
TimesRoman = "Times-Roman"
TimesBold = "Times-Bold"
TimesItalic = "Times-Italic"
TimesBoldItalic = "Times-BoldItalic"
Courier = "Courier"
CourierBold = "Courier-Bold"
CourierOblique = "Courier-Oblique"
CourierBoldOblique = "Courier-BoldOblique"
Symbol = "Symbol"
ZapfDingbats = "ZapfDingbats"
)

// Standard14EmUnits is the design unit size for all Adobe 14 fonts.
const Standard14EmUnits = 1000

// IsStandard14 reports whether name is one of the Adobe 14 standard fonts.
func IsStandard14(name string) bool {
_, ok := standard14Data[name]
return ok
}

// Standard14Metrics returns design-unit metrics for a Standard 14 font,
// or zero Metrics and false if the name is not a Standard 14 font.
func Standard14Metrics(name string) (Metrics, bool) {
d, ok := standard14Data[name]
if !ok {
return Metrics{}, false
}
return d.metrics, true
}

// Standard14Width returns the advance width of r in the named font, in
// design units (1000 per em). Returns the space glyph's width when r is
// not present in the font's width table, matching the fallback most PDF
// viewers apply when asked to measure characters outside the font's
// encoding.
func Standard14Width(name string, r rune) (int, bool) {
d, ok := standard14Data[name]
if !ok {
return 0, false
}
if w, ok := d.widths[r]; ok {
return w, true
}
return d.widths[' '], true
}

// standard14FontEntry carries the metrics and per-rune advance widths for
// one Adobe 14 font.
type standard14FontEntry struct {
metrics Metrics
widths map[rune]int
}

// standard14Font implements Font for layout measurement of Adobe 14 fonts
// for which no TTF is registered. Encode/Subset are not meaningful for
// non-embedded Type1 fonts and return empty results.
type standard14Font struct {
name string
metrics Metrics
widths map[rune]int
}

// NewStandard14Font returns a Font backed by the AFM width tables for the
// named Adobe 14 font. Returns (nil, false) if name is not a Standard 14
// font. The returned Font is safe for layout measurement (Metrics and
// GlyphWidth) but Encode returns the raw UTF-8 bytes and Subset is a
// no-op — non-embedded Type1 fonts do not require subsetting.
func NewStandard14Font(name string) (Font, bool) {
d, ok := standard14Data[name]
if !ok {
return nil, false
}
return &standard14Font{name: name, metrics: d.metrics, widths: d.widths}, true
}

func (f *standard14Font) Name() string { return f.name }
func (f *standard14Font) Metrics() Metrics { return f.metrics }

func (f *standard14Font) GlyphWidth(r rune) (int, bool) {
if w, ok := f.widths[r]; ok {
return w, true
}
return 0, false
}

func (f *standard14Font) Encode(text string) []byte {
return []byte(text)
}

func (f *standard14Font) Subset(_ []rune) ([]byte, error) {
return nil, nil
}
221 changes: 221 additions & 0 deletions pdf/font/standard14_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package font

// Advance widths and metrics for the Adobe Core 14 fonts in 1000 em units.
//
// Values transcribed from the Adobe Font Metrics (AFM) files published by
// Adobe as part of the Core Font Program. Coverage is printable ASCII
// (0x20-0x7E); characters outside this range fall back to the space
// glyph's width — which matches how PDF viewers render unmapped runes for
// non-embedded Type1 fonts.

// printableASCIIWidths maps rune positions 0x20..0x7E (space through ~) to
// their advance widths. Helper to expand a compact [95]uint16 into the
// rune-keyed map stored on each font entry.
func printableASCIIWidths(ws [95]uint16) map[rune]int {
m := make(map[rune]int, 95)
for i, w := range ws {
m[rune(0x20+i)] = int(w)
}
return m
}

// monospaceWidths returns a 95-entry table where every printable ASCII
// glyph has the same advance width — used for the Courier family.
func monospaceWidths(w uint16) [95]uint16 {
var arr [95]uint16
for i := range arr {
arr[i] = w
}
return arr
}

var helveticaASCII = [95]uint16{
// 0x20..0x27: ! " # $ % & '
278, 278, 355, 556, 556, 889, 667, 191,
// 0x28..0x2F: ( ) * + , - . /
333, 333, 389, 584, 278, 333, 278, 278,
// 0x30..0x37: 0 1 2 3 4 5 6 7
556, 556, 556, 556, 556, 556, 556, 556,
// 0x38..0x3F: 8 9 : ; < = > ?
556, 556, 278, 278, 584, 584, 584, 556,
// 0x40..0x47: @ A B C D E F G
1015, 667, 667, 722, 722, 667, 611, 778,
// 0x48..0x4F: H I J K L M N O
722, 278, 500, 667, 556, 833, 722, 778,
// 0x50..0x57: P Q R S T U V W
667, 778, 722, 667, 611, 722, 667, 944,
// 0x58..0x5F: X Y Z [ \ ] ^ _
667, 667, 611, 278, 278, 278, 469, 556,
// 0x60..0x67: ` a b c d e f g
333, 556, 556, 500, 556, 556, 278, 556,
// 0x68..0x6F: h i j k l m n o
556, 222, 222, 500, 222, 833, 556, 556,
// 0x70..0x77: p q r s t u v w
556, 556, 333, 500, 278, 556, 500, 722,
// 0x78..0x7E: x y z { | } ~
500, 500, 500, 334, 260, 334, 584,
}

var helveticaBoldASCII = [95]uint16{
278, 333, 474, 556, 556, 889, 722, 238,
333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556,
556, 556, 333, 333, 584, 584, 584, 611,
975, 722, 722, 722, 722, 667, 611, 778,
722, 278, 556, 722, 611, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944,
667, 667, 611, 333, 278, 333, 584, 556,
333, 556, 611, 556, 611, 556, 333, 611,
611, 278, 278, 556, 278, 889, 611, 611,
611, 611, 389, 556, 333, 611, 556, 778,
556, 556, 500, 389, 280, 389, 584,
}

var timesRomanASCII = [95]uint16{
250, 333, 408, 500, 500, 833, 778, 180,
333, 333, 500, 564, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500,
500, 500, 278, 278, 564, 564, 564, 444,
921, 722, 667, 667, 722, 611, 556, 722,
722, 333, 389, 722, 611, 889, 722, 722,
556, 722, 667, 556, 611, 722, 722, 944,
722, 722, 611, 333, 278, 333, 469, 500,
333, 444, 500, 444, 500, 444, 333, 500,
500, 278, 278, 500, 278, 778, 500, 500,
500, 500, 333, 389, 278, 500, 500, 722,
500, 500, 444, 480, 200, 480, 541,
}

var timesBoldASCII = [95]uint16{
250, 333, 555, 500, 500, 1000, 833, 278,
333, 333, 500, 570, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500,
500, 500, 333, 333, 570, 570, 570, 500,
930, 722, 667, 722, 722, 667, 611, 778,
778, 389, 500, 778, 667, 944, 722, 778,
611, 778, 722, 556, 667, 722, 722, 1000,
722, 722, 667, 333, 278, 333, 581, 500,
333, 500, 556, 444, 556, 444, 333, 500,
556, 278, 333, 556, 278, 833, 556, 500,
556, 556, 444, 389, 333, 556, 500, 722,
500, 500, 444, 394, 220, 394, 520,
}

var timesItalicASCII = [95]uint16{
250, 333, 420, 500, 500, 833, 778, 214,
333, 333, 500, 675, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500,
500, 500, 333, 333, 675, 675, 675, 500,
920, 611, 611, 667, 722, 611, 611, 722,
722, 333, 444, 667, 556, 833, 667, 722,
611, 722, 611, 500, 556, 722, 611, 833,
611, 556, 556, 389, 278, 389, 422, 500,
333, 500, 500, 444, 500, 444, 278, 500,
500, 278, 278, 444, 278, 722, 500, 500,
500, 500, 389, 389, 278, 500, 444, 667,
444, 444, 389, 400, 275, 400, 541,
}

var timesBoldItalicASCII = [95]uint16{
250, 389, 555, 500, 500, 833, 778, 278,
333, 333, 500, 570, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500,
500, 500, 333, 333, 570, 570, 570, 500,
832, 667, 667, 667, 722, 667, 667, 722,
778, 389, 500, 667, 611, 889, 722, 722,
611, 722, 667, 556, 611, 722, 667, 889,
667, 611, 611, 333, 278, 333, 570, 500,
333, 500, 500, 444, 500, 444, 333, 500,
556, 278, 278, 500, 278, 778, 556, 500,
500, 500, 389, 389, 278, 556, 444, 667,
500, 444, 389, 348, 220, 348, 570,
}

// Symbol and ZapfDingbats use specialized encodings that don't map to
// printable ASCII in the usual way. For layout measurement we return a
// plausible average width — users generally don't set body text in these
// fonts, and any mismatch only affects alignment within those contexts.
var symbolFallbackWidth = uint16(500)
var zapfDingbatsFallbackWidth = uint16(788)

// standard14Data holds per-font metrics and ASCII width tables.
// Helvetica-Oblique / Helvetica-BoldOblique share widths with their upright
// counterparts (obliquing does not alter advance widths). Courier-* are all
// 600 em units wide (monospace).
var standard14Data = map[string]standard14FontEntry{
Helvetica: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 718, Descender: -207, CapHeight: 718, XHeight: 523, LineGap: 0},
widths: printableASCIIWidths(helveticaASCII),
},
HelveticaBold: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 718, Descender: -207, CapHeight: 718, XHeight: 532, LineGap: 0},
widths: printableASCIIWidths(helveticaBoldASCII),
},
HelveticaOblique: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 718, Descender: -207, CapHeight: 718, XHeight: 523, LineGap: 0, ItalicAngle: -12},
widths: printableASCIIWidths(helveticaASCII),
},
HelveticaBoldOblique: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 718, Descender: -207, CapHeight: 718, XHeight: 532, LineGap: 0, ItalicAngle: -12},
widths: printableASCIIWidths(helveticaBoldASCII),
},
TimesRoman: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 683, Descender: -217, CapHeight: 662, XHeight: 450, LineGap: 0},
widths: printableASCIIWidths(timesRomanASCII),
},
TimesBold: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 683, Descender: -217, CapHeight: 676, XHeight: 461, LineGap: 0},
widths: printableASCIIWidths(timesBoldASCII),
},
TimesItalic: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 683, Descender: -217, CapHeight: 653, XHeight: 441, LineGap: 0, ItalicAngle: -15.5},
widths: printableASCIIWidths(timesItalicASCII),
},
TimesBoldItalic: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 683, Descender: -217, CapHeight: 669, XHeight: 462, LineGap: 0, ItalicAngle: -15},
widths: printableASCIIWidths(timesBoldItalicASCII),
},
Courier: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 629, Descender: -157, CapHeight: 562, XHeight: 426, LineGap: 0},
widths: printableASCIIWidths(monospaceWidths(600)),
},
CourierBold: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 629, Descender: -157, CapHeight: 562, XHeight: 439, LineGap: 0},
widths: printableASCIIWidths(monospaceWidths(600)),
},
CourierOblique: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 629, Descender: -157, CapHeight: 562, XHeight: 426, LineGap: 0, ItalicAngle: -12},
widths: printableASCIIWidths(monospaceWidths(600)),
},
CourierBoldOblique: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 629, Descender: -157, CapHeight: 562, XHeight: 439, LineGap: 0, ItalicAngle: -12},
widths: printableASCIIWidths(monospaceWidths(600)),
},
Symbol: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 1010, Descender: -293, CapHeight: 729, XHeight: 525, LineGap: 0},
widths: printableASCIIWidths(monospaceWidths(symbolFallbackWidth)),
},
ZapfDingbats: {
metrics: Metrics{UnitsPerEm: 1000, Ascender: 820, Descender: -143, CapHeight: 820, XHeight: 456, LineGap: 0},
widths: printableASCIIWidths(monospaceWidths(zapfDingbatsFallbackWidth)),
},
}

// Variant aliases: template.buildFontVariantID emits generic "-Italic"
// suffixes regardless of family, but the Adobe Core 14 canonical names use
// "-Oblique" for Helvetica/Courier (and "-Roman" as the base form of
// Times). Map those variants so layout lookups find the same metrics the
// PDF renderer writes out via base14Variants in pdftarget.go.
func init() {
for alias, canonical := range map[string]string{
"Helvetica-Italic": HelveticaOblique,
"Helvetica-BoldItalic": HelveticaBoldOblique,
"Courier-Italic": CourierOblique,
"Courier-BoldItalic": CourierBoldOblique,
"Times": TimesRoman,
} {
if _, exists := standard14Data[alias]; !exists {
standard14Data[alias] = standard14Data[canonical]
}
}
}
Loading
Loading