Files
tiennm99 fb903e3262 fix: embed font data instead of a path, and bundle a fallback font
PDF rendering failed on Linux with "stat usr/share/fonts/...: no such file or
directory": fpdf joins the font path onto its own font directory, which it
defaults to ".", so path.Join turns an absolute path into a
working-directory-relative one. It only resolved when the process happened to
run from the filesystem root, which is why Windows was unaffected. Font data is
now read by pdfout and handed over as bytes.

LoadFont also falls back to a bundled DejaVu Sans, so a host with no fonts
installed still renders. An explicitly requested font remains a hard error when
unreadable rather than being silently substituted. Tests verify the bundled font
parses and covers Vietnamese, which is the reason it exists.
2026-07-29 23:41:30 +07:00

109 lines
3.5 KiB
Go

package pdfout
import (
_ "embed"
"fmt"
"os"
"path/filepath"
"runtime"
)
// bundledTTF is the font used when nothing else is available, so rendering never
// depends on the host having fonts installed — a minimal container typically has
// none. See fonts/NOTICE.md for provenance and licensing.
//
//go:embed fonts/DejaVuSans.ttf
var bundledTTF []byte
// BundledFontName labels the embedded font in diagnostics. It is not a path;
// the font is compiled into the binary.
const BundledFontName = "DejaVu Sans (bundled)"
// Font is font data ready to embed in a PDF.
//
// The data is carried as bytes rather than as a path because fpdf joins a font
// path onto its own font directory, which it defaults to "." — turning an
// absolute path into a working-directory-relative one that resolves only when
// the process happens to run from the filesystem root.
type Font struct {
// Name identifies the font for diagnostics: the file path it was read
// from, or BundledFontName.
Name string
Data []byte
}
// Vietnamese text needs the Latin Extended Additional block (ư, ạ, ế, ộ …).
// Every font listed here ships with its platform and covers it; fonts with only
// basic Latin would silently drop the diacritics.
func fontCandidates() []string {
switch runtime.GOOS {
case "windows":
dir := filepath.Join(os.Getenv("SystemRoot"), "Fonts")
if os.Getenv("SystemRoot") == "" {
dir = `C:\Windows\Fonts`
}
return []string{
filepath.Join(dir, "segoeui.ttf"),
filepath.Join(dir, "arial.ttf"),
filepath.Join(dir, "calibri.ttf"),
filepath.Join(dir, "tahoma.ttf"),
filepath.Join(dir, "times.ttf"),
}
case "darwin":
return []string{
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/Library/Fonts/Arial.ttf",
"/System/Library/Fonts/Supplemental/Times New Roman.ttf",
}
default:
return []string{
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
"/usr/share/fonts/TTF/DejaVuSans.ttf",
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/liberation/LiberationSans-Regular.ttf",
}
}
}
// FindFont returns the first available system font suitable for Vietnamese.
// Callers that just need something that works should use LoadFont, which falls
// back to the bundled font instead of failing.
func FindFont() (string, error) {
candidates := fontCandidates()
for _, path := range candidates {
if info, err := os.Stat(path); err == nil && !info.IsDir() {
return path, nil
}
}
return "", fmt.Errorf("no Vietnamese-capable system font found (looked for %v)", candidates)
}
// LoadFont resolves the font to embed.
//
// An explicit path wins, and is a hard error when it cannot be read: a caller
// that named a font wants that font, not a substitute. Otherwise a system font
// is used, and when none can be read the bundled font is returned — so with an
// empty path LoadFont always succeeds.
func LoadFont(path string) (Font, error) {
if path != "" {
data, err := os.ReadFile(path)
if err != nil {
return Font{}, fmt.Errorf("read font: %w", err)
}
return Font{Name: path, Data: data}, nil
}
if found, err := FindFont(); err == nil {
if data, err := os.ReadFile(found); err == nil {
return Font{Name: found, Data: data}, nil
}
// A listed font that cannot be read is no better than a missing one.
}
return BundledFont(), nil
}
// BundledFont returns the font compiled into the binary.
func BundledFont() Font {
return Font{Name: BundledFontName, Data: bundledTTF}
}