mirror of
https://github.com/unidoc/unipdf.git
synced 2025-04-27 13:48:51 +08:00
543 lines
13 KiB
Go
543 lines
13 KiB
Go
// +build unidev
|
|
|
|
// Parse character metrics from an AFM file to convert into a static go code declaration.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"flag"
|
|
|
|
pdfcommon "github.com/unidoc/unipdf/v3/common"
|
|
"github.com/unidoc/unipdf/v3/internal/fonts"
|
|
"github.com/unidoc/unipdf/v3/model"
|
|
)
|
|
|
|
func main() {
|
|
filepath := flag.String("file", "", "AFM input file")
|
|
method := flag.String("method", "charmetrics", "charmetrics/charcodes/glyph-to-charcode")
|
|
|
|
flag.Parse()
|
|
|
|
if len(*filepath) == 0 && *method != "descriptor" {
|
|
fmt.Println("Please specify an input file. Run with -h to get options.")
|
|
return
|
|
}
|
|
|
|
var err error
|
|
switch *method {
|
|
case "charmetrics":
|
|
err = runCharmetricsOnFile(*filepath)
|
|
case "charcodes":
|
|
err = runCharcodeToGlyphRetrievalOnFile(*filepath)
|
|
case "glyph-to-charcode":
|
|
err = runGlyphToCharcodeRetrievalOnFile(*filepath)
|
|
case "descriptor":
|
|
err = runFontDescriptorOnFiles()
|
|
}
|
|
|
|
if err != nil {
|
|
fmt.Printf("Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// --charmetrics to get char metric data.
|
|
// --charcodes to get charcode to glyph data
|
|
}
|
|
|
|
// Generate a glyph to charmetrics (width and height) map.
|
|
func runCharmetricsOnFile(path string) error {
|
|
metrics, err := GetCharmetricsFromAfmFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keys := []string{}
|
|
for key := range metrics {
|
|
keys = append(keys, key)
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
fmt.Printf("var xxfontCharMetrics map[string]CharMetrics = map[string]CharMetrics{\n")
|
|
for _, key := range keys {
|
|
metric := metrics[key]
|
|
fmt.Printf("\t\"%s\":\t{Wx:%f, Wy:%f},\n", key, metric.Wx, metric.Wy)
|
|
}
|
|
fmt.Printf("}\n")
|
|
return nil
|
|
}
|
|
|
|
func runCharcodeToGlyphRetrievalOnFile(afmpath string) error {
|
|
charcodeToGlyphMap, err := GetCharcodeToGlyphEncodingFromAfmFile(afmpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keys := []int{}
|
|
for key := range charcodeToGlyphMap {
|
|
keys = append(keys, int(key))
|
|
}
|
|
sort.Ints(keys)
|
|
|
|
fmt.Printf("var xxfontCharcodeToGlyphMap map[byte]string = map[byte]string{\n")
|
|
for _, key := range keys {
|
|
fmt.Printf("\t%d: \"%s\",\n", key, charcodeToGlyphMap[byte(key)])
|
|
}
|
|
fmt.Printf("}\n")
|
|
|
|
return nil
|
|
}
|
|
|
|
func runGlyphToCharcodeRetrievalOnFile(afmpath string) error {
|
|
charcodeToGlyphMap, err := GetCharcodeToGlyphEncodingFromAfmFile(afmpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keys := []int{}
|
|
for key := range charcodeToGlyphMap {
|
|
keys = append(keys, int(key))
|
|
}
|
|
sort.Ints(keys)
|
|
|
|
fmt.Printf("var xxfontGlyphToCharcodeMap map[string]byte = map[string]byte ={\n")
|
|
for _, key := range keys {
|
|
fmt.Printf("\t\"%s\":\t%d,\n", charcodeToGlyphMap[byte(key)], key)
|
|
}
|
|
fmt.Printf("}\n")
|
|
|
|
return nil
|
|
}
|
|
|
|
func GetCharmetricsFromAfmFile(filename string) (map[string]model.CharMetrics, error) {
|
|
glyphMetricsMap := map[string]model.CharMetrics{}
|
|
|
|
f, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
readingCharMetrics := false
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
parts := strings.Split(line, " ")
|
|
if len(parts) < 1 {
|
|
continue
|
|
}
|
|
if !readingCharMetrics && parts[0] == "StartCharMetrics" {
|
|
readingCharMetrics = true
|
|
continue
|
|
}
|
|
if readingCharMetrics && parts[0] == "EndCharMetrics" {
|
|
break
|
|
}
|
|
if !readingCharMetrics {
|
|
continue
|
|
}
|
|
if parts[0] != "C" {
|
|
continue
|
|
}
|
|
|
|
parts = strings.Split(line, ";")
|
|
var metrics model.CharMetrics
|
|
glyphName := ""
|
|
for _, part := range parts {
|
|
cmd := strings.TrimSpace(part)
|
|
if len(cmd) == 0 {
|
|
continue
|
|
}
|
|
args := strings.Split(cmd, " ")
|
|
if len(args) < 1 {
|
|
continue
|
|
}
|
|
|
|
switch args[0] {
|
|
case "N":
|
|
if len(args) != 2 {
|
|
pdfcommon.Log.Debug("Failed C line: ", line)
|
|
return nil, errors.New("invalid C line")
|
|
}
|
|
glyphName = strings.TrimSpace(args[1])
|
|
case "WX":
|
|
if len(args) != 2 {
|
|
pdfcommon.Log.Debug("WX: Invalid number of args != 1 (%s)\n", line)
|
|
return nil, errors.New("invalid range")
|
|
}
|
|
wx, err := strconv.ParseFloat(args[1], 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
metrics.Wx = wx
|
|
case "WY":
|
|
if len(args) != 2 {
|
|
pdfcommon.Log.Debug("WY: Invalid number of args != 1 (%s)\n", line)
|
|
return nil, errors.New("invalid range")
|
|
}
|
|
wy, err := strconv.ParseFloat(args[1], 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
metrics.Wy = wy
|
|
case "W":
|
|
if len(args) != 2 {
|
|
pdfcommon.Log.Debug("W: Invalid number of args != 1 (%s)\n", line)
|
|
return nil, errors.New("invalid range")
|
|
}
|
|
w, err := strconv.ParseFloat(args[1], 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
metrics.Wy = w
|
|
metrics.Wx = w
|
|
}
|
|
}
|
|
|
|
if len(glyphName) > 0 {
|
|
glyphMetricsMap[glyphName] = metrics
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return glyphMetricsMap, nil
|
|
}
|
|
|
|
func GetCharcodeToGlyphEncodingFromAfmFile(filename string) (map[byte]string, error) {
|
|
charcodeToGlypMap := map[byte]string{}
|
|
|
|
f, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
readingCharMetrics := false
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
parts := strings.Split(line, " ")
|
|
if len(parts) < 1 {
|
|
continue
|
|
}
|
|
if !readingCharMetrics && parts[0] == "StartCharMetrics" {
|
|
readingCharMetrics = true
|
|
continue
|
|
}
|
|
if readingCharMetrics && parts[0] == "EndCharMetrics" {
|
|
break
|
|
}
|
|
if !readingCharMetrics {
|
|
continue
|
|
}
|
|
if parts[0] != "C" {
|
|
continue
|
|
}
|
|
|
|
parts = strings.Split(line, ";")
|
|
var charcode int64
|
|
var glyph string
|
|
|
|
for _, part := range parts {
|
|
cmd := strings.TrimSpace(part)
|
|
if len(cmd) == 0 {
|
|
continue
|
|
}
|
|
args := strings.Split(cmd, " ")
|
|
if len(args) < 1 {
|
|
continue
|
|
}
|
|
|
|
switch args[0] {
|
|
case "C":
|
|
if len(args) != 2 {
|
|
pdfcommon.Log.Debug("Failed C line: %s", line)
|
|
return nil, errors.New("invalid C line")
|
|
}
|
|
charcode, err = strconv.ParseInt(strings.TrimSpace(args[1]), 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case "N":
|
|
if len(args) != 2 {
|
|
pdfcommon.Log.Debug("Failed C line: %s", line)
|
|
return nil, errors.New("invalid C line")
|
|
}
|
|
|
|
glyph = strings.TrimSpace(args[1])
|
|
if charcode >= 0 && charcode <= 255 {
|
|
charcodeToGlypMap[byte(charcode)] = glyph
|
|
} else {
|
|
fmt.Printf("NOT included: %d -> %s\n", charcode, glyph)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return charcodeToGlypMap, nil
|
|
}
|
|
|
|
func runFontDescriptorOnFiles() error {
|
|
parts := []string{}
|
|
for _, name := range standard14Names {
|
|
path := filepath.Join(".", fmt.Sprintf("%s.afm", name))
|
|
text, err := createFontDescriptor(name, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
parts = append(parts, text)
|
|
}
|
|
fmt.Println("\n// =========================================================\n")
|
|
fmt.Println(" Standard14Descriptors = map[string]DescriptorLiteral {")
|
|
for _, text := range parts {
|
|
fmt.Printf("%s,\n", text)
|
|
}
|
|
fmt.Println("}")
|
|
return nil
|
|
}
|
|
|
|
var standard14Names = []string{
|
|
"Courier",
|
|
"Courier-Bold",
|
|
"Courier-BoldOblique",
|
|
"Courier-Oblique",
|
|
"Helvetica",
|
|
"Helvetica-Bold",
|
|
"Helvetica-BoldOblique",
|
|
"Helvetica-Oblique",
|
|
"Times-Roman",
|
|
"Times-Bold",
|
|
"Times-BoldItalic",
|
|
"Times-Italic",
|
|
"Symbol",
|
|
"ZapfDingbats",
|
|
}
|
|
|
|
// createFontDescriptor creates a literal DescriptorLiteral that can be copied into
|
|
// pdf/internal/fonts/const.go.
|
|
func createFontDescriptor(name, filename string) (string, error) {
|
|
fmt.Println("----------------------------------------------------")
|
|
fmt.Printf("createFontDescriptor: name=%#q filename=%q\n", name, filename)
|
|
|
|
nameVal, err := parseAfmFile(filename)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
descriptor, err := parseNameVal(nameVal)
|
|
if err != nil {
|
|
fmt.Printf("ERROR: createFontDescriptor failed. name=%q err=%v\n", name, err)
|
|
return "", err
|
|
}
|
|
return writeDescriptorLiteral(name, descriptor), nil
|
|
}
|
|
|
|
func parseAfmFile(filename string) (map[string]string, error) {
|
|
|
|
f, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
nameVal := map[string]string{}
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
k, v, ok := asKeyValue(line)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if k == "StartCharMetrics" {
|
|
break
|
|
}
|
|
nameVal[k] = v
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nameVal, nil
|
|
}
|
|
|
|
// firstWord returns the first word in `line`.
|
|
func asKeyValue(line string) (string, string, bool) {
|
|
line = strings.Trim(line, "\r\n\t ")
|
|
i := strings.IndexByte(line, ' ')
|
|
if i < 0 {
|
|
return "", "", false
|
|
}
|
|
return line[:i], line[i+1:], true
|
|
}
|
|
|
|
func parseNameVal(nameVal map[string]string) (fonts.DescriptorLiteral, error) {
|
|
fmt.Printf("nameVal=%q\n", nameVal)
|
|
descriptor := fonts.DescriptorLiteral{}
|
|
descriptor.FontName = nameVal["FontName"]
|
|
descriptor.FontFamily = nameVal["FamilyName"]
|
|
weight, ok := textWeight[nameVal["Weight"]]
|
|
if !ok {
|
|
return descriptor, fmt.Errorf("Invalid Weight=%#q. nameVal=%q",
|
|
nameVal["Weight"], nameVal)
|
|
}
|
|
descriptor.FontWeight = weight
|
|
symbolic := descriptor.FontFamily == "Symbol" || descriptor.FontFamily == "ZapfDingbats"
|
|
|
|
descriptor.FontBBox = toBBox(nameVal["FontBBox"])
|
|
descriptor.ItalicAngle = toFloat(nameVal, false, "ItalicAngle")
|
|
descriptor.CapHeight = toFloat(nameVal, symbolic, "CapHeight")
|
|
descriptor.XHeight = toFloat(nameVal, symbolic, "XHeight")
|
|
descriptor.Ascent = toFloat(nameVal, symbolic, "Ascender")
|
|
descriptor.Descent = toFloat(nameVal, symbolic, "Descender")
|
|
descriptor.StemH = toFloat(nameVal, symbolic, "StdHW")
|
|
descriptor.StemV = toFloat(nameVal, symbolic, "StdVW")
|
|
|
|
fixedPitch := toBool(nameVal, "IsFixedPitch")
|
|
italic := descriptor.ItalicAngle != 0
|
|
descriptor.Flags = getFlags(descriptor.FontName, fixedPitch, symbolic, italic)
|
|
|
|
return descriptor, nil
|
|
}
|
|
|
|
func getFlags(name string, fixedPitch, symbolic, italic bool) uint {
|
|
flags := uint(0)
|
|
if fixedPitch {
|
|
flags |= fontFlagFixedPitch
|
|
}
|
|
if symbolic {
|
|
flags |= fontFlagSymbolic
|
|
} else {
|
|
flags |= fontFlagNonsymbolic
|
|
}
|
|
if italic {
|
|
flags |= fontFlagItalic
|
|
}
|
|
return flags
|
|
}
|
|
|
|
// These consts are copied from model/font.go
|
|
// 9.8.2 Font Descriptor Flags (page 283)
|
|
const (
|
|
fontFlagFixedPitch = 0x00001
|
|
fontFlagSerif = 0x00002
|
|
fontFlagSymbolic = 0x00004
|
|
fontFlagScript = 0x00008
|
|
fontFlagNonsymbolic = 0x00020
|
|
fontFlagItalic = 0x00040
|
|
fontFlagAllCap = 0x10000
|
|
fontFlagSmallCap = 0x20000
|
|
fontFlagForceBold = 0x40000
|
|
)
|
|
|
|
func toInt(keyVal map[string]string, key string) uint {
|
|
text, ok := keyVal[key]
|
|
if !ok {
|
|
panic(fmt.Sprintf("No %#q key", key))
|
|
}
|
|
x, err := strconv.Atoi(text)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return uint(x)
|
|
}
|
|
|
|
func toBool(keyVal map[string]string, key string) bool {
|
|
text, ok := keyVal[key]
|
|
if !ok {
|
|
panic(fmt.Sprintf("No %#q key", key))
|
|
}
|
|
switch text {
|
|
case "true":
|
|
return true
|
|
case "false":
|
|
return false
|
|
default:
|
|
panic(fmt.Sprintf("Bad bool val %q. key=%#q", text, key))
|
|
}
|
|
}
|
|
|
|
func toFloat(keyVal map[string]string, optional bool, key string) float64 {
|
|
text, ok := keyVal[key]
|
|
if !ok {
|
|
// Symbol and ZapfDingbats don't have XHeight and key == "CapHeight" entries
|
|
if optional {
|
|
return 0.0
|
|
}
|
|
panic(fmt.Sprintf("No %#q key", key))
|
|
}
|
|
x, err := strconv.ParseFloat(text, 64)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return x
|
|
}
|
|
|
|
func toBBox(text string) [4]float64 {
|
|
fmt.Printf("toBBox: text=%q\n", text)
|
|
parts := strings.Split(text, " ")
|
|
vals := [4]float64{}
|
|
for i, p := range parts {
|
|
x, err := strconv.ParseFloat(p, 64)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
vals[i] = x
|
|
}
|
|
return vals
|
|
}
|
|
|
|
var textWeight = map[string]fonts.FontWeight{
|
|
"Medium": fonts.FontWeightMedium,
|
|
"Bold": fonts.FontWeightBold,
|
|
"Roman": fonts.FontWeightRoman,
|
|
}
|
|
|
|
var weightVarname = map[fonts.FontWeight]string{
|
|
fonts.FontWeightMedium: "FontWeightMedium",
|
|
fonts.FontWeightBold: "FontWeightBold",
|
|
fonts.FontWeightRoman: "FontWeightRoman",
|
|
}
|
|
|
|
func writeDescriptorLiteral(name string, descriptor fonts.DescriptorLiteral) string {
|
|
|
|
var b bytes.Buffer
|
|
|
|
fmt.Fprintf(&b, "%q: DescriptorLiteral {\n", name)
|
|
fmt.Fprintf(&b, "\tFontName: %q,\n", descriptor.FontName)
|
|
fmt.Fprintf(&b, "\tFontFamily: %q,\n", descriptor.FontFamily)
|
|
fmt.Fprintf(&b, "\tFontWeight: %s,\n", weightVarname[descriptor.FontWeight])
|
|
fmt.Fprintf(&b, "\tFlags: 0x%04x,\n", descriptor.Flags)
|
|
fmt.Fprintf(&b, "\tFontBBox: %#v,\n", descriptor.FontBBox)
|
|
fmt.Fprintf(&b, "\tItalicAngle: %g,\n", descriptor.ItalicAngle)
|
|
fmt.Fprintf(&b, "\tAscent: %g,\n", descriptor.Ascent)
|
|
fmt.Fprintf(&b, "\tDescent: %g,\n", descriptor.Descent)
|
|
fmt.Fprintf(&b, "\tCapHeight: %g,\n", descriptor.CapHeight)
|
|
fmt.Fprintf(&b, "\tXHeight: %g,\n", descriptor.XHeight)
|
|
fmt.Fprintf(&b, "\tStemV: %g,\n", descriptor.StemV)
|
|
fmt.Fprintf(&b, "\tStemH: %g,\n", descriptor.StemH)
|
|
fmt.Fprintf(&b, "}")
|
|
return b.String()
|
|
}
|