375 lines
8.7 KiB
Go
375 lines
8.7 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 parse TTF font files
|
|
// Version: 1.0
|
|
// Date: 2011-06-18
|
|
// Author: Olivier PLATHEY
|
|
// Port to Go: Kurt Jung, 2013-07-15
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// TtfType contains metrics of a TrueType font.
|
|
type TtfType struct {
|
|
Embeddable bool
|
|
UnitsPerEm uint16
|
|
PostScriptName string
|
|
Bold bool
|
|
ItalicAngle int16
|
|
IsFixedPitch bool
|
|
TypoAscender int16
|
|
TypoDescender int16
|
|
UnderlinePosition int16
|
|
UnderlineThickness int16
|
|
Xmin, Ymin, Xmax, Ymax int16
|
|
CapHeight int16
|
|
Widths []uint16
|
|
Chars map[uint16]uint16
|
|
}
|
|
|
|
type ttfParser struct {
|
|
rec TtfType
|
|
f *os.File
|
|
tables map[string]uint32
|
|
numberOfHMetrics uint16
|
|
numGlyphs uint16
|
|
}
|
|
|
|
// TtfParse extracts various metrics from a TrueType font file.
|
|
func TtfParse(fileStr string) (TtfRec TtfType, err error) {
|
|
var t ttfParser
|
|
t.f, err = os.Open(fileStr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
version, err := t.ReadStr(4)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if version == "OTTO" {
|
|
err = fmt.Errorf("fonts based on PostScript outlines are not supported")
|
|
return
|
|
}
|
|
if version != "\x00\x01\x00\x00" {
|
|
err = fmt.Errorf("unrecognized file format")
|
|
return
|
|
}
|
|
numTables := int(t.ReadUShort())
|
|
t.Skip(3 * 2) // searchRange, entrySelector, rangeShift
|
|
t.tables = make(map[string]uint32)
|
|
var tag string
|
|
for j := 0; j < numTables; j++ {
|
|
tag, err = t.ReadStr(4)
|
|
if err != nil {
|
|
return
|
|
}
|
|
t.Skip(4) // checkSum
|
|
offset := t.ReadULong()
|
|
t.Skip(4) // length
|
|
t.tables[tag] = offset
|
|
}
|
|
err = t.ParseComponents()
|
|
if err != nil {
|
|
return
|
|
}
|
|
t.f.Close()
|
|
TtfRec = t.rec
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ParseComponents() (err error) {
|
|
err = t.ParseHead()
|
|
if err == nil {
|
|
err = t.ParseHhea()
|
|
if err == nil {
|
|
err = t.ParseMaxp()
|
|
if err == nil {
|
|
err = t.ParseHmtx()
|
|
if err == nil {
|
|
err = t.ParseCmap()
|
|
if err == nil {
|
|
err = t.ParseName()
|
|
if err == nil {
|
|
err = t.ParseOS2()
|
|
if err == nil {
|
|
err = t.ParsePost()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ParseHead() (err error) {
|
|
err = t.Seek("head")
|
|
t.Skip(3 * 4) // version, fontRevision, checkSumAdjustment
|
|
magicNumber := t.ReadULong()
|
|
if magicNumber != 0x5F0F3CF5 {
|
|
err = fmt.Errorf("incorrect magic number")
|
|
return
|
|
}
|
|
t.Skip(2) // flags
|
|
t.rec.UnitsPerEm = t.ReadUShort()
|
|
t.Skip(2 * 8) // created, modified
|
|
t.rec.Xmin = t.ReadShort()
|
|
t.rec.Ymin = t.ReadShort()
|
|
t.rec.Xmax = t.ReadShort()
|
|
t.rec.Ymax = t.ReadShort()
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ParseHhea() (err error) {
|
|
err = t.Seek("hhea")
|
|
if err == nil {
|
|
t.Skip(4 + 15*2)
|
|
t.numberOfHMetrics = t.ReadUShort()
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ParseMaxp() (err error) {
|
|
err = t.Seek("maxp")
|
|
if err == nil {
|
|
t.Skip(4)
|
|
t.numGlyphs = t.ReadUShort()
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ParseHmtx() (err error) {
|
|
err = t.Seek("hmtx")
|
|
if err == nil {
|
|
t.rec.Widths = make([]uint16, 0, 8)
|
|
for j := uint16(0); j < t.numberOfHMetrics; j++ {
|
|
t.rec.Widths = append(t.rec.Widths, t.ReadUShort())
|
|
t.Skip(2) // lsb
|
|
}
|
|
if t.numberOfHMetrics < t.numGlyphs {
|
|
lastWidth := t.rec.Widths[t.numberOfHMetrics-1]
|
|
for j := t.numberOfHMetrics; j < t.numGlyphs; j++ {
|
|
t.rec.Widths = append(t.rec.Widths, lastWidth)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ParseCmap() (err error) {
|
|
var offset int64
|
|
if err = t.Seek("cmap"); err != nil {
|
|
return
|
|
}
|
|
t.Skip(2) // version
|
|
numTables := int(t.ReadUShort())
|
|
offset31 := int64(0)
|
|
for j := 0; j < numTables; j++ {
|
|
platformID := t.ReadUShort()
|
|
encodingID := t.ReadUShort()
|
|
offset = int64(t.ReadULong())
|
|
if platformID == 3 && encodingID == 1 {
|
|
offset31 = offset
|
|
}
|
|
}
|
|
if offset31 == 0 {
|
|
err = fmt.Errorf("no Unicode encoding found")
|
|
return
|
|
}
|
|
startCount := make([]uint16, 0, 8)
|
|
endCount := make([]uint16, 0, 8)
|
|
idDelta := make([]int16, 0, 8)
|
|
idRangeOffset := make([]uint16, 0, 8)
|
|
t.rec.Chars = make(map[uint16]uint16)
|
|
t.f.Seek(int64(t.tables["cmap"])+offset31, os.SEEK_SET)
|
|
format := t.ReadUShort()
|
|
if format != 4 {
|
|
err = fmt.Errorf("unexpected subtable format: %d", format)
|
|
return
|
|
}
|
|
t.Skip(2 * 2) // length, language
|
|
segCount := int(t.ReadUShort() / 2)
|
|
t.Skip(3 * 2) // searchRange, entrySelector, rangeShift
|
|
for j := 0; j < segCount; j++ {
|
|
endCount = append(endCount, t.ReadUShort())
|
|
}
|
|
t.Skip(2) // reservedPad
|
|
for j := 0; j < segCount; j++ {
|
|
startCount = append(startCount, t.ReadUShort())
|
|
}
|
|
for j := 0; j < segCount; j++ {
|
|
idDelta = append(idDelta, t.ReadShort())
|
|
}
|
|
offset, _ = t.f.Seek(int64(0), os.SEEK_CUR)
|
|
for j := 0; j < segCount; j++ {
|
|
idRangeOffset = append(idRangeOffset, t.ReadUShort())
|
|
}
|
|
for j := 0; j < segCount; j++ {
|
|
c1 := startCount[j]
|
|
c2 := endCount[j]
|
|
d := idDelta[j]
|
|
ro := idRangeOffset[j]
|
|
if ro > 0 {
|
|
t.f.Seek(offset+2*int64(j)+int64(ro), os.SEEK_SET)
|
|
}
|
|
for c := c1; c <= c2; c++ {
|
|
if c == 0xFFFF {
|
|
break
|
|
}
|
|
var gid int32
|
|
if ro > 0 {
|
|
gid = int32(t.ReadUShort())
|
|
if gid > 0 {
|
|
gid += int32(d)
|
|
}
|
|
} else {
|
|
gid = int32(c) + int32(d)
|
|
}
|
|
if gid >= 65536 {
|
|
gid -= 65536
|
|
}
|
|
if gid > 0 {
|
|
t.rec.Chars[c] = uint16(gid)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ParseName() (err error) {
|
|
err = t.Seek("name")
|
|
if err == nil {
|
|
tableOffset, _ := t.f.Seek(0, os.SEEK_CUR)
|
|
t.rec.PostScriptName = ""
|
|
t.Skip(2) // format
|
|
count := t.ReadUShort()
|
|
stringOffset := t.ReadUShort()
|
|
for j := uint16(0); j < count && t.rec.PostScriptName == ""; j++ {
|
|
t.Skip(3 * 2) // platformID, encodingID, languageID
|
|
nameID := t.ReadUShort()
|
|
length := t.ReadUShort()
|
|
offset := t.ReadUShort()
|
|
if nameID == 6 {
|
|
// PostScript name
|
|
t.f.Seek(int64(tableOffset)+int64(stringOffset)+int64(offset), os.SEEK_SET)
|
|
var s string
|
|
s, err = t.ReadStr(int(length))
|
|
if err != nil {
|
|
return
|
|
}
|
|
s = strings.Replace(s, "\x00", "", -1)
|
|
var re *regexp.Regexp
|
|
if re, err = regexp.Compile("[(){}<> /%[\\]]"); err != nil {
|
|
return
|
|
}
|
|
t.rec.PostScriptName = re.ReplaceAllString(s, "")
|
|
}
|
|
}
|
|
if t.rec.PostScriptName == "" {
|
|
err = fmt.Errorf("the name PostScript was not found")
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ParseOS2() (err error) {
|
|
err = t.Seek("OS/2")
|
|
if err == nil {
|
|
version := t.ReadUShort()
|
|
t.Skip(3 * 2) // xAvgCharWidth, usWeightClass, usWidthClass
|
|
fsType := t.ReadUShort()
|
|
t.rec.Embeddable = (fsType != 2) && (fsType&0x200) == 0
|
|
t.Skip(11*2 + 10 + 4*4 + 4)
|
|
fsSelection := t.ReadUShort()
|
|
t.rec.Bold = (fsSelection & 32) != 0
|
|
t.Skip(2 * 2) // usFirstCharIndex, usLastCharIndex
|
|
t.rec.TypoAscender = t.ReadShort()
|
|
t.rec.TypoDescender = t.ReadShort()
|
|
if version >= 2 {
|
|
t.Skip(3*2 + 2*4 + 2)
|
|
t.rec.CapHeight = t.ReadShort()
|
|
} else {
|
|
t.rec.CapHeight = 0
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ParsePost() (err error) {
|
|
err = t.Seek("post")
|
|
if err == nil {
|
|
t.Skip(4) // version
|
|
t.rec.ItalicAngle = t.ReadShort()
|
|
t.Skip(2) // Skip decimal part
|
|
t.rec.UnderlinePosition = t.ReadShort()
|
|
t.rec.UnderlineThickness = t.ReadShort()
|
|
t.rec.IsFixedPitch = t.ReadULong() != 0
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) Seek(tag string) (err error) {
|
|
ofs, ok := t.tables[tag]
|
|
if ok {
|
|
t.f.Seek(int64(ofs), os.SEEK_SET)
|
|
} else {
|
|
err = fmt.Errorf("table not found: %s", tag)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) Skip(n int) {
|
|
t.f.Seek(int64(n), os.SEEK_CUR)
|
|
}
|
|
|
|
func (t *ttfParser) ReadStr(length int) (str string, err error) {
|
|
var n int
|
|
buf := make([]byte, length)
|
|
n, err = t.f.Read(buf)
|
|
if err == nil {
|
|
if n == length {
|
|
str = string(buf)
|
|
} else {
|
|
err = fmt.Errorf("unable to read %d bytes", length)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ReadUShort() (val uint16) {
|
|
binary.Read(t.f, binary.BigEndian, &val)
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ReadShort() (val int16) {
|
|
binary.Read(t.f, binary.BigEndian, &val)
|
|
return
|
|
}
|
|
|
|
func (t *ttfParser) ReadULong() (val uint32) {
|
|
binary.Read(t.f, binary.BigEndian, &val)
|
|
return
|
|
}
|