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 }