394 lines
8.8 KiB
Go
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
|
|
}
|