475 lines
13 KiB
Go
475 lines
13 KiB
Go
|
/*
|
||
|
* Copyright (c) 2013 Kurt Jung (Gmail: kurt.w.jung)
|
||
|
*
|
||
|
* Permission to use, copy, modify, and distribute this software for any
|
||
|
* purpose with or without fee is hereby granted, provided that the above
|
||
|
* copyright notice and this permission notice appear in all copies.
|
||
|
*
|
||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||
|
*/
|
||
|
|
||
|
package gofpdf
|
||
|
|
||
|
// Utility to generate font definition files
|
||
|
|
||
|
// Version: 1.2
|
||
|
// Date: 2011-06-18
|
||
|
// Author: Olivier PLATHEY
|
||
|
// Port to Go: Kurt Jung, 2013-07-15
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"compress/zlib"
|
||
|
"encoding/binary"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
func baseNoExt(fileStr string) string {
|
||
|
str := filepath.Base(fileStr)
|
||
|
extLen := len(filepath.Ext(str))
|
||
|
if extLen > 0 {
|
||
|
str = str[:len(str)-extLen]
|
||
|
}
|
||
|
return str
|
||
|
}
|
||
|
|
||
|
func loadMap(encodingFileStr string) (encList encListType, err error) {
|
||
|
// printf("Encoding file string [%s]\n", encodingFileStr)
|
||
|
var f *os.File
|
||
|
// f, err = os.Open(encodingFilepath(encodingFileStr))
|
||
|
f, err = os.Open(encodingFileStr)
|
||
|
if err == nil {
|
||
|
defer f.Close()
|
||
|
for j := range encList {
|
||
|
encList[j].uv = -1
|
||
|
encList[j].name = ".notdef"
|
||
|
}
|
||
|
scanner := bufio.NewScanner(f)
|
||
|
var enc encType
|
||
|
var pos int
|
||
|
for scanner.Scan() {
|
||
|
// "!3F U+003F question"
|
||
|
_, err = fmt.Sscanf(scanner.Text(), "!%x U+%x %s", &pos, &enc.uv, &enc.name)
|
||
|
if err == nil {
|
||
|
if pos < 256 {
|
||
|
encList[pos] = enc
|
||
|
} else {
|
||
|
err = fmt.Errorf("map position 0x%2X exceeds 0xFF", pos)
|
||
|
return
|
||
|
}
|
||
|
} else {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
if err = scanner.Err(); err != nil {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// getInfoFromTrueType returns information from a TrueType font
|
||
|
func getInfoFromTrueType(fileStr string, msgWriter io.Writer, embed bool, encList encListType) (info fontInfoType, err error) {
|
||
|
info.Widths = make([]int, 256)
|
||
|
var ttf TtfType
|
||
|
ttf, err = TtfParse(fileStr)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
if embed {
|
||
|
if !ttf.Embeddable {
|
||
|
err = fmt.Errorf("font license does not allow embedding")
|
||
|
return
|
||
|
}
|
||
|
info.Data, err = ioutil.ReadFile(fileStr)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
info.OriginalSize = len(info.Data)
|
||
|
}
|
||
|
k := 1000.0 / float64(ttf.UnitsPerEm)
|
||
|
info.FontName = ttf.PostScriptName
|
||
|
info.Bold = ttf.Bold
|
||
|
info.Desc.ItalicAngle = int(ttf.ItalicAngle)
|
||
|
info.IsFixedPitch = ttf.IsFixedPitch
|
||
|
info.Desc.Ascent = round(k * float64(ttf.TypoAscender))
|
||
|
info.Desc.Descent = round(k * float64(ttf.TypoDescender))
|
||
|
info.UnderlineThickness = round(k * float64(ttf.UnderlineThickness))
|
||
|
info.UnderlinePosition = round(k * float64(ttf.UnderlinePosition))
|
||
|
info.Desc.FontBBox = fontBoxType{
|
||
|
round(k * float64(ttf.Xmin)),
|
||
|
round(k * float64(ttf.Ymin)),
|
||
|
round(k * float64(ttf.Xmax)),
|
||
|
round(k * float64(ttf.Ymax)),
|
||
|
}
|
||
|
// printf("FontBBox\n")
|
||
|
// dump(info.Desc.FontBBox)
|
||
|
info.Desc.CapHeight = round(k * float64(ttf.CapHeight))
|
||
|
info.Desc.MissingWidth = round(k * float64(ttf.Widths[0]))
|
||
|
var wd int
|
||
|
for j := 0; j < len(info.Widths); j++ {
|
||
|
wd = info.Desc.MissingWidth
|
||
|
if encList[j].name != ".notdef" {
|
||
|
uv := encList[j].uv
|
||
|
pos, ok := ttf.Chars[uint16(uv)]
|
||
|
if ok {
|
||
|
wd = round(k * float64(ttf.Widths[pos]))
|
||
|
} else {
|
||
|
fmt.Fprintf(msgWriter, "Character %s is missing\n", encList[j].name)
|
||
|
}
|
||
|
}
|
||
|
info.Widths[j] = wd
|
||
|
}
|
||
|
// printf("getInfoFromTrueType/FontBBox\n")
|
||
|
// dump(info.Desc.FontBBox)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
type segmentType struct {
|
||
|
marker uint8
|
||
|
tp uint8
|
||
|
size uint32
|
||
|
data []byte
|
||
|
}
|
||
|
|
||
|
func segmentRead(r io.Reader) (s segmentType, err error) {
|
||
|
if err = binary.Read(r, binary.LittleEndian, &s.marker); err != nil {
|
||
|
return
|
||
|
}
|
||
|
if s.marker != 128 {
|
||
|
err = fmt.Errorf("font file is not a valid binary Type1")
|
||
|
return
|
||
|
}
|
||
|
if err = binary.Read(r, binary.LittleEndian, &s.tp); err != nil {
|
||
|
return
|
||
|
}
|
||
|
if err = binary.Read(r, binary.LittleEndian, &s.size); err != nil {
|
||
|
return
|
||
|
}
|
||
|
s.data = make([]byte, s.size)
|
||
|
_, err = r.Read(s.data)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// -rw-r--r-- 1 root root 9532 2010-04-22 11:27 /usr/share/fonts/type1/mathml/Symbol.afm
|
||
|
// -rw-r--r-- 1 root root 37744 2010-04-22 11:27 /usr/share/fonts/type1/mathml/Symbol.pfb
|
||
|
|
||
|
// getInfoFromType1 return information from a Type1 font
|
||
|
func getInfoFromType1(fileStr string, msgWriter io.Writer, embed bool, encList encListType) (info fontInfoType, err error) {
|
||
|
info.Widths = make([]int, 256)
|
||
|
if embed {
|
||
|
var f *os.File
|
||
|
f, err = os.Open(fileStr)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
defer f.Close()
|
||
|
// Read first segment
|
||
|
var s1, s2 segmentType
|
||
|
s1, err = segmentRead(f)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
s2, err = segmentRead(f)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
info.Data = s1.data
|
||
|
info.Data = append(info.Data, s2.data...)
|
||
|
info.Size1 = s1.size
|
||
|
info.Size2 = s2.size
|
||
|
}
|
||
|
afmFileStr := fileStr[0:len(fileStr)-3] + "afm"
|
||
|
size, ok := fileSize(afmFileStr)
|
||
|
if !ok {
|
||
|
err = fmt.Errorf("font file (ATM) %s not found", afmFileStr)
|
||
|
return
|
||
|
} else if size == 0 {
|
||
|
err = fmt.Errorf("font file (AFM) %s empty or not readable", afmFileStr)
|
||
|
return
|
||
|
}
|
||
|
var f *os.File
|
||
|
f, err = os.Open(afmFileStr)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
defer f.Close()
|
||
|
scanner := bufio.NewScanner(f)
|
||
|
var fields []string
|
||
|
var wd int
|
||
|
var wt, name string
|
||
|
wdMap := make(map[string]int)
|
||
|
for scanner.Scan() {
|
||
|
fields = strings.Fields(strings.TrimSpace(scanner.Text()))
|
||
|
// Comment Generated by FontForge 20080203
|
||
|
// FontName Symbol
|
||
|
// C 32 ; WX 250 ; N space ; B 0 0 0 0 ;
|
||
|
if len(fields) >= 2 {
|
||
|
switch fields[0] {
|
||
|
case "C":
|
||
|
if wd, err = strconv.Atoi(fields[4]); err == nil {
|
||
|
name = fields[7]
|
||
|
wdMap[name] = wd
|
||
|
}
|
||
|
case "FontName":
|
||
|
info.FontName = fields[1]
|
||
|
case "Weight":
|
||
|
wt = strings.ToLower(fields[1])
|
||
|
case "ItalicAngle":
|
||
|
info.Desc.ItalicAngle, err = strconv.Atoi(fields[1])
|
||
|
case "Ascender":
|
||
|
info.Desc.Ascent, err = strconv.Atoi(fields[1])
|
||
|
case "Descender":
|
||
|
info.Desc.Descent, err = strconv.Atoi(fields[1])
|
||
|
case "UnderlineThickness":
|
||
|
info.UnderlineThickness, err = strconv.Atoi(fields[1])
|
||
|
case "UnderlinePosition":
|
||
|
info.UnderlinePosition, err = strconv.Atoi(fields[1])
|
||
|
case "IsFixedPitch":
|
||
|
info.IsFixedPitch = fields[1] == "true"
|
||
|
case "FontBBox":
|
||
|
if info.Desc.FontBBox.Xmin, err = strconv.Atoi(fields[1]); err == nil {
|
||
|
if info.Desc.FontBBox.Ymin, err = strconv.Atoi(fields[2]); err == nil {
|
||
|
if info.Desc.FontBBox.Xmax, err = strconv.Atoi(fields[3]); err == nil {
|
||
|
info.Desc.FontBBox.Ymax, err = strconv.Atoi(fields[4])
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
case "CapHeight":
|
||
|
info.Desc.CapHeight, err = strconv.Atoi(fields[1])
|
||
|
case "StdVW":
|
||
|
info.Desc.StemV, err = strconv.Atoi(fields[1])
|
||
|
}
|
||
|
}
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
if err = scanner.Err(); err != nil {
|
||
|
return
|
||
|
}
|
||
|
if info.FontName == "" {
|
||
|
err = fmt.Errorf("the field FontName missing in AFM file %s", afmFileStr)
|
||
|
return
|
||
|
}
|
||
|
info.Bold = wt == "bold" || wt == "black"
|
||
|
var missingWd int
|
||
|
missingWd, ok = wdMap[".notdef"]
|
||
|
if ok {
|
||
|
info.Desc.MissingWidth = missingWd
|
||
|
}
|
||
|
for j := 0; j < len(info.Widths); j++ {
|
||
|
info.Widths[j] = info.Desc.MissingWidth
|
||
|
}
|
||
|
for j := 0; j < len(info.Widths); j++ {
|
||
|
name = encList[j].name
|
||
|
if name != ".notdef" {
|
||
|
wd, ok = wdMap[name]
|
||
|
if ok {
|
||
|
info.Widths[j] = wd
|
||
|
} else {
|
||
|
fmt.Fprintf(msgWriter, "Character %s is missing\n", name)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// printf("getInfoFromType1/FontBBox\n")
|
||
|
// dump(info.Desc.FontBBox)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func makeFontDescriptor(info *fontInfoType) {
|
||
|
if info.Desc.CapHeight == 0 {
|
||
|
info.Desc.CapHeight = info.Desc.Ascent
|
||
|
}
|
||
|
info.Desc.Flags = 1 << 5
|
||
|
if info.IsFixedPitch {
|
||
|
info.Desc.Flags |= 1
|
||
|
}
|
||
|
if info.Desc.ItalicAngle != 0 {
|
||
|
info.Desc.Flags |= 1 << 6
|
||
|
}
|
||
|
if info.Desc.StemV == 0 {
|
||
|
if info.Bold {
|
||
|
info.Desc.StemV = 120
|
||
|
} else {
|
||
|
info.Desc.StemV = 70
|
||
|
}
|
||
|
}
|
||
|
// printf("makeFontDescriptor/FontBBox\n")
|
||
|
// dump(info.Desc.FontBBox)
|
||
|
}
|
||
|
|
||
|
// makeFontEncoding builds differences from reference encoding
|
||
|
func makeFontEncoding(encList encListType, refEncFileStr string) (diffStr string, err error) {
|
||
|
var refList encListType
|
||
|
if refList, err = loadMap(refEncFileStr); err != nil {
|
||
|
return
|
||
|
}
|
||
|
var buf fmtBuffer
|
||
|
last := 0
|
||
|
for j := 32; j < 256; j++ {
|
||
|
if encList[j].name != refList[j].name {
|
||
|
if j != last+1 {
|
||
|
buf.printf("%d ", j)
|
||
|
}
|
||
|
last = j
|
||
|
buf.printf("/%s ", encList[j].name)
|
||
|
}
|
||
|
}
|
||
|
diffStr = strings.TrimSpace(buf.String())
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func makeDefinitionFile(fileStr, tpStr, encodingFileStr string, embed bool, encList encListType, info fontInfoType) error {
|
||
|
var err error
|
||
|
var def fontDefType
|
||
|
def.Tp = tpStr
|
||
|
def.Name = info.FontName
|
||
|
makeFontDescriptor(&info)
|
||
|
def.Desc = info.Desc
|
||
|
// printf("makeDefinitionFile/FontBBox\n")
|
||
|
// dump(def.Desc.FontBBox)
|
||
|
def.Up = info.UnderlinePosition
|
||
|
def.Ut = info.UnderlineThickness
|
||
|
def.Cw = info.Widths
|
||
|
def.Enc = baseNoExt(encodingFileStr)
|
||
|
// fmt.Printf("encodingFileStr [%s], def.Enc [%s]\n", encodingFileStr, def.Enc)
|
||
|
// fmt.Printf("reference [%s]\n", filepath.Join(filepath.Dir(encodingFileStr), "cp1252.map"))
|
||
|
def.Diff, err = makeFontEncoding(encList, filepath.Join(filepath.Dir(encodingFileStr), "cp1252.map"))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
def.File = info.File
|
||
|
def.Size1 = int(info.Size1)
|
||
|
def.Size2 = int(info.Size2)
|
||
|
def.OriginalSize = info.OriginalSize
|
||
|
// printf("Font definition file [%s]\n", fileStr)
|
||
|
var buf []byte
|
||
|
buf, err = json.Marshal(def)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
var f *os.File
|
||
|
f, err = os.Create(fileStr)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
_, err = f.Write(buf)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
err = f.Close()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// MakeFont generates a font definition file in JSON format. A definition file
|
||
|
// of this type is required to use non-core fonts in the PDF documents that
|
||
|
// gofpdf generates. See the makefont utility in the gofpdf package for a
|
||
|
// command line interface to this function.
|
||
|
//
|
||
|
// fontFileStr is the name of the TrueType file (extension .ttf), OpenType file
|
||
|
// (extension .otf) or binary Type1 file (extension .pfb) from which to
|
||
|
// generate a definition file. If an OpenType file is specified, it must be one
|
||
|
// that is based on TrueType outlines, not PostScript outlines; this cannot be
|
||
|
// determined from the file extension alone. If a Type1 file is specified, a
|
||
|
// metric file with the same pathname except with the extension .afm must be
|
||
|
// present.
|
||
|
//
|
||
|
// encodingFileStr is the name of the encoding file that corresponds to the
|
||
|
// font.
|
||
|
//
|
||
|
// dstDirStr is the name of the directory in which to save the definition file
|
||
|
// and, if embed is true, the compressed font file.
|
||
|
//
|
||
|
// msgWriter is the writer that is called to display messages throughout the
|
||
|
// process. Use nil to turn off messages.
|
||
|
//
|
||
|
// embed is true if the font is to be embedded in the PDF files.
|
||
|
func MakeFont(fontFileStr, encodingFileStr, dstDirStr string, msgWriter io.Writer, embed bool) error {
|
||
|
if msgWriter == nil {
|
||
|
msgWriter = ioutil.Discard
|
||
|
}
|
||
|
if !fileExist(fontFileStr) {
|
||
|
return fmt.Errorf("font file not found: %s", fontFileStr)
|
||
|
}
|
||
|
extStr := strings.ToLower(fontFileStr[len(fontFileStr)-3:])
|
||
|
// printf("Font file extension [%s]\n", extStr)
|
||
|
var tpStr string
|
||
|
switch extStr {
|
||
|
case "ttf":
|
||
|
fallthrough
|
||
|
case "otf":
|
||
|
tpStr = "TrueType"
|
||
|
case "pfb":
|
||
|
tpStr = "Type1"
|
||
|
default:
|
||
|
return fmt.Errorf("unrecognized font file extension: %s", extStr)
|
||
|
}
|
||
|
|
||
|
var info fontInfoType
|
||
|
encList, err := loadMap(encodingFileStr)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// printf("Encoding table\n")
|
||
|
// dump(encList)
|
||
|
if tpStr == "TrueType" {
|
||
|
info, err = getInfoFromTrueType(fontFileStr, msgWriter, embed, encList)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
} else {
|
||
|
info, err = getInfoFromType1(fontFileStr, msgWriter, embed, encList)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
baseStr := baseNoExt(fontFileStr)
|
||
|
// fmt.Printf("Base [%s]\n", baseStr)
|
||
|
if embed {
|
||
|
var f *os.File
|
||
|
info.File = baseStr + ".z"
|
||
|
zFileStr := filepath.Join(dstDirStr, info.File)
|
||
|
f, err = os.Create(zFileStr)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
cmp := zlib.NewWriter(f)
|
||
|
_, err = cmp.Write(info.Data)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
err = cmp.Close()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
fmt.Fprintf(msgWriter, "Font file compressed: %s\n", zFileStr)
|
||
|
}
|
||
|
defFileStr := filepath.Join(dstDirStr, baseStr+".json")
|
||
|
err = makeDefinitionFile(defFileStr, tpStr, encodingFileStr, embed, encList, info)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
fmt.Fprintf(msgWriter, "Font definition file successfully generated: %s\n", defFileStr)
|
||
|
return nil
|
||
|
}
|