Files
pdfmerge/internal/pdf/model/font.go
2025-12-15 17:44:00 +01:00

394 lines
8.8 KiB
Go

package model
import (
"errors"
"os"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/common"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/core"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model/fonts"
"gitea.tecamino.com/paadi/pdfmerge/internal/pdf/model/textencoding"
)
// The PdfFont structure represents an underlying font structure which can be of type:
// - Type0
// - Type1
// - TrueType
// etc.
type PdfFont struct {
context any // The underlying font: Type0, Type1, Truetype, etc..
}
// Set the encoding for the underlying font.
func (font PdfFont) SetEncoder(encoder textencoding.TextEncoder) {
switch t := font.context.(type) {
case *pdfFontTrueType:
t.SetEncoder(encoder)
}
}
func (font PdfFont) GetGlyphCharMetrics(glyph string) (fonts.CharMetrics, bool) {
switch t := font.context.(type) {
case *pdfFontTrueType:
return t.GetGlyphCharMetrics(glyph)
}
return fonts.CharMetrics{}, false
}
func (font PdfFont) ToPdfObject() core.PdfObject {
switch f := font.context.(type) {
case *pdfFontTrueType:
return f.ToPdfObject()
}
// If not supported, return null..
common.Log.Debug("Unsupported font (%T) - returning null object", font.context)
return core.MakeNull()
}
type pdfFontTrueType struct {
Encoder textencoding.TextEncoder
firstChar int
lastChar int
charWidths []float64
// Subtype shall be TrueType.
// Encoding is subject to limitations that are described in 9.6.6, "Character Encoding".
// BaseFont is derived differently.
BaseFont core.PdfObject
FirstChar core.PdfObject
LastChar core.PdfObject
Widths core.PdfObject
FontDescriptor *PdfFontDescriptor
Encoding core.PdfObject
ToUnicode core.PdfObject
container *core.PdfIndirectObject
}
func (font pdfFontTrueType) SetEncoder(encoder textencoding.TextEncoder) {
font.Encoder = encoder
}
func (font pdfFontTrueType) GetGlyphCharMetrics(glyph string) (fonts.CharMetrics, bool) {
metrics := fonts.CharMetrics{}
code, found := font.Encoder.GlyphToCharcode(glyph)
if !found {
return metrics, false
}
if int(code) < font.firstChar {
common.Log.Debug("Code lower than firstchar (%d < %d)", code, font.firstChar)
return metrics, false
}
if int(code) > font.lastChar {
common.Log.Debug("Code higher than lastchar (%d < %d)", code, font.lastChar)
return metrics, false
}
index := int(code) - font.firstChar
if index >= len(font.charWidths) {
common.Log.Debug("Code outside of widths range")
return metrics, false
}
width := font.charWidths[index]
metrics.Wx = width
return metrics, true
}
func (ftt *pdfFontTrueType) ToPdfObject() core.PdfObject {
if ftt.container == nil {
ftt.container = &core.PdfIndirectObject{}
}
d := core.MakeDict()
ftt.container.PdfObject = d
d.Set("Type", core.MakeName("Font"))
d.Set("Subtype", core.MakeName("TrueType"))
if ftt.BaseFont != nil {
d.Set("BaseFont", ftt.BaseFont)
}
if ftt.FirstChar != nil {
d.Set("FirstChar", ftt.FirstChar)
}
if ftt.LastChar != nil {
d.Set("LastChar", ftt.LastChar)
}
if ftt.Widths != nil {
d.Set("Widths", ftt.Widths)
}
if ftt.FontDescriptor != nil {
d.Set("FontDescriptor", ftt.FontDescriptor.ToPdfObject())
}
if ftt.Encoding != nil {
d.Set("Encoding", ftt.Encoding)
}
if ftt.ToUnicode != nil {
d.Set("ToUnicode", ftt.ToUnicode)
}
return ftt.container
}
func NewPdfFontFromTTFFile(filePath string) (*PdfFont, error) {
ttf, err := fonts.TtfParse(filePath)
if err != nil {
common.Log.Debug("error loading ttf font: %v", err)
return nil, err
}
truefont := &pdfFontTrueType{}
truefont.Encoder = textencoding.NewWinAnsiTextEncoder()
truefont.firstChar = 32
truefont.lastChar = 255
truefont.BaseFont = core.MakeName(ttf.PostScriptName)
truefont.FirstChar = core.MakeInteger(32)
truefont.LastChar = core.MakeInteger(255)
k := 1000.0 / float64(ttf.UnitsPerEm)
if len(ttf.Widths) <= 0 {
return nil, errors.New("missing required attribute (Widths)")
}
missingWidth := k * float64(ttf.Widths[0])
vals := []float64{}
for charcode := 32; charcode <= 255; charcode++ {
runeVal, found := truefont.Encoder.CharcodeToRune(byte(charcode))
if !found {
common.Log.Debug("Rune not found (charcode: %d)", charcode)
vals = append(vals, missingWidth)
continue
}
pos, ok := ttf.Chars[uint16(runeVal)]
if !ok {
common.Log.Debug("Rune not in TTF Chars")
vals = append(vals, missingWidth)
continue
}
w := k * float64(ttf.Widths[pos])
vals = append(vals, w)
}
truefont.Widths = &core.PdfIndirectObject{PdfObject: core.MakeArrayFromFloats(vals)}
if len(vals) < (255 - 32 + 1) {
common.Log.Debug("invalid length of widths, %d < %d", len(vals), 255-32+1)
return nil, errors.New("range check error")
}
truefont.charWidths = vals[:255-32+1]
// Default.
// XXX/FIXME TODO: Only use the encoder object.
truefont.Encoding = core.MakeName("WinAnsiEncoding")
descriptor := &PdfFontDescriptor{}
descriptor.Ascent = core.MakeFloat(k * float64(ttf.TypoAscender))
descriptor.Descent = core.MakeFloat(k * float64(ttf.TypoDescender))
descriptor.CapHeight = core.MakeFloat(k * float64(ttf.CapHeight))
descriptor.FontBBox = core.MakeArrayFromFloats([]float64{k * float64(ttf.Xmin), k * float64(ttf.Ymin), k * float64(ttf.Xmax), k * float64(ttf.Ymax)})
descriptor.ItalicAngle = core.MakeFloat(float64(ttf.ItalicAngle))
descriptor.MissingWidth = core.MakeFloat(k * float64(ttf.Widths[0]))
ttfBytes, err := os.ReadFile(filePath)
if err != nil {
common.Log.Debug("Unable to read file contents: %v", err)
return nil, err
}
// XXX/TODO: Encode the file...
stream, err := core.MakeStream(ttfBytes, core.NewFlateEncoder())
if err != nil {
common.Log.Debug("Unable to make stream: %v", err)
return nil, err
}
stream.PdfObjectDictionary.Set("Length1", core.MakeInteger(int64(len(ttfBytes))))
descriptor.FontFile2 = stream
if ttf.Bold {
descriptor.StemV = core.MakeInteger(120)
} else {
descriptor.StemV = core.MakeInteger(70)
}
// Flags.
flags := 1 << 5
if ttf.IsFixedPitch {
flags |= 1
}
if ttf.ItalicAngle != 0 {
flags |= 1 << 6
}
descriptor.Flags = core.MakeInteger(int64(flags))
// Build Font.
truefont.FontDescriptor = descriptor
font := &PdfFont{}
font.context = truefont
return font, nil
}
// Font descriptors specifies metrics and other attributes of a font.
type PdfFontDescriptor struct {
FontName core.PdfObject
FontFamily core.PdfObject
FontStretch core.PdfObject
FontWeight core.PdfObject
Flags core.PdfObject
FontBBox core.PdfObject
ItalicAngle core.PdfObject
Ascent core.PdfObject
Descent core.PdfObject
Leading core.PdfObject
CapHeight core.PdfObject
XHeight core.PdfObject
StemV core.PdfObject
StemH core.PdfObject
AvgWidth core.PdfObject
MaxWidth core.PdfObject
MissingWidth core.PdfObject
FontFile core.PdfObject
FontFile2 core.PdfObject
FontFile3 core.PdfObject
CharSet core.PdfObject
// Additional entries for CIDFonts
Style core.PdfObject
Lang core.PdfObject
FD core.PdfObject
CIDSet core.PdfObject
// Container.
container *core.PdfIndirectObject
}
// Convert to a PDF dictionary inside an indirect object.
func (pfd *PdfFontDescriptor) ToPdfObject() core.PdfObject {
d := core.MakeDict()
if pfd.container == nil {
pfd.container = &core.PdfIndirectObject{}
}
pfd.container.PdfObject = d
d.Set("Type", core.MakeName("FontDescriptor"))
if pfd.FontName != nil {
d.Set("FontName", pfd.FontName)
}
if pfd.FontFamily != nil {
d.Set("FontFamily", pfd.FontFamily)
}
if pfd.FontStretch != nil {
d.Set("FontStretch", pfd.FontStretch)
}
if pfd.FontWeight != nil {
d.Set("FontWeight", pfd.FontWeight)
}
if pfd.Flags != nil {
d.Set("Flags", pfd.Flags)
}
if pfd.FontBBox != nil {
d.Set("FontBBox", pfd.FontBBox)
}
if pfd.ItalicAngle != nil {
d.Set("ItalicAngle", pfd.ItalicAngle)
}
if pfd.Ascent != nil {
d.Set("Ascent", pfd.Ascent)
}
if pfd.Descent != nil {
d.Set("Descent", pfd.Descent)
}
if pfd.Leading != nil {
d.Set("Leading", pfd.Leading)
}
if pfd.CapHeight != nil {
d.Set("CapHeight", pfd.CapHeight)
}
if pfd.XHeight != nil {
d.Set("XHeight", pfd.XHeight)
}
if pfd.StemV != nil {
d.Set("StemV", pfd.StemV)
}
if pfd.StemH != nil {
d.Set("StemH", pfd.StemH)
}
if pfd.AvgWidth != nil {
d.Set("AvgWidth", pfd.AvgWidth)
}
if pfd.MaxWidth != nil {
d.Set("MaxWidth", pfd.MaxWidth)
}
if pfd.MissingWidth != nil {
d.Set("MissingWidth", pfd.MissingWidth)
}
if pfd.FontFile != nil {
d.Set("FontFile", pfd.FontFile)
}
if pfd.FontFile2 != nil {
d.Set("FontFile2", pfd.FontFile2)
}
if pfd.FontFile3 != nil {
d.Set("FontFile3", pfd.FontFile3)
}
if pfd.CharSet != nil {
d.Set("CharSet", pfd.CharSet)
}
if pfd.Style != nil {
d.Set("FontName", pfd.FontName)
}
if pfd.Lang != nil {
d.Set("Lang", pfd.Lang)
}
if pfd.FD != nil {
d.Set("FD", pfd.FD)
}
if pfd.CIDSet != nil {
d.Set("CIDSet", pfd.CIDSet)
}
return pfd.container
}