codeberg.org/go-pdf/fpdf@v0.11.1/font_afm.go (about)

     1  // Copyright ©2025 The go-pdf Authors. All rights reserved.
     2  // Use of this source code is governed by a MIT-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package fpdf
     6  
     7  import (
     8  	"bufio"
     9  	"fmt"
    10  	"io"
    11  	"math"
    12  	"strconv"
    13  	"strings"
    14  )
    15  
    16  type afmParser struct {
    17  	s    *bufio.Scanner
    18  	err  error
    19  	line int
    20  	toks []string
    21  
    22  	wdmap map[string]int
    23  }
    24  
    25  func newAFMParser(r io.Reader) *afmParser {
    26  	return &afmParser{
    27  		s:     bufio.NewScanner(r),
    28  		wdmap: make(map[string]int),
    29  	}
    30  }
    31  
    32  func (p *afmParser) parse(fnt *fontInfoType) error {
    33  loop:
    34  	for p.scan() {
    35  		switch p.toks[0] {
    36  		case "StartFontMetrics":
    37  			// ok.
    38  		case "FontName":
    39  			fnt.FontName = p.readStr(1)
    40  		case "Weight":
    41  			weight := strings.ToLower(p.readStr(1))
    42  			fnt.Bold = weight == "bold" || weight == "black"
    43  		case "FontBBox":
    44  			fnt.Desc.FontBBox.Xmin = p.readFixed(1)
    45  			fnt.Desc.FontBBox.Ymin = p.readFixed(2)
    46  			fnt.Desc.FontBBox.Xmax = p.readFixed(3)
    47  			fnt.Desc.FontBBox.Ymax = p.readFixed(4)
    48  		case "CapHeight":
    49  			fnt.Desc.CapHeight = p.readFixed(1)
    50  		case "Ascender":
    51  			fnt.Desc.Ascent = p.readFixed(1)
    52  		case "Descender":
    53  			fnt.Desc.Descent = p.readFixed(1)
    54  		case "StdVW":
    55  			fnt.Desc.StemV = p.readFixed(1)
    56  		case "UnderlinePosition":
    57  			fnt.UnderlinePosition = p.readFixed(1)
    58  		case "UnderlineThickness":
    59  			fnt.UnderlineThickness = p.readFixed(1)
    60  		case "ItalicAngle":
    61  			fnt.Desc.ItalicAngle = p.readFixed(1)
    62  		case "StartCharMetrics":
    63  			err := p.parseCharMetrics(fnt, p.readInt(1))
    64  			if err != nil {
    65  				return fmt.Errorf("could not scan AFM CharMetrics section: %w", err)
    66  			}
    67  		case "EndFontMetrics":
    68  			break loop
    69  		default:
    70  			//	log.Printf("invalid FontMetrics token %q (line=%d)", p.toks[0], p.line)
    71  		}
    72  	}
    73  
    74  	if p.err != nil {
    75  		return fmt.Errorf("could not parse AFM file: %w", p.err)
    76  	}
    77  
    78  	if err := p.s.Err(); err != nil {
    79  		return fmt.Errorf("could not parse AFM file: %w", p.err)
    80  	}
    81  
    82  	return nil
    83  }
    84  
    85  func (p *afmParser) scan() bool {
    86  	if p.err != nil {
    87  		return false
    88  	}
    89  	p.line++
    90  	ok := p.s.Scan()
    91  	p.toks = strings.Fields(strings.TrimSpace(p.s.Text()))
    92  	if ok && len(p.toks) == 0 {
    93  		// skip empty lines.
    94  		return p.scan()
    95  	}
    96  	p.err = p.s.Err()
    97  	return ok
    98  }
    99  
   100  func (p *afmParser) parseCharMetrics(fnt *fontInfoType, n int) error {
   101  	for p.scan() {
   102  		switch p.toks[0] {
   103  		case "EndCharMetrics":
   104  			return nil
   105  		case "Comment":
   106  			// ignore.
   107  		case "C":
   108  			err := p.parseCharMetric(fnt)
   109  			if err != nil {
   110  				return fmt.Errorf("could not parse CharMetric entry: %w", err)
   111  			}
   112  		case "CH",
   113  			"WX", "W0X", "W1X",
   114  			"WY", "W0Y", "W1Y",
   115  			"W", "W0", "W1",
   116  			"VV",
   117  			"N",
   118  			"B",
   119  			"L":
   120  			// ignore.
   121  		default:
   122  			return fmt.Errorf("invalid CharMetrics token %q", p.toks[0])
   123  		}
   124  	}
   125  	return p.err
   126  }
   127  
   128  func (p *afmParser) parseCharMetric(fnt *fontInfoType) error {
   129  	type metric struct {
   130  		Name string
   131  		Wd   int
   132  	}
   133  	var ch metric
   134  	for _, v := range strings.Split(p.s.Text(), ";") {
   135  		v = strings.TrimSpace(v)
   136  		if v == "" {
   137  			continue
   138  		}
   139  		p.toks = strings.Fields(v)
   140  		switch p.toks[0] {
   141  		case "C":
   142  			// ignore.
   143  		case "CH":
   144  			// ignore.
   145  		case "WX", "W0X":
   146  			ch.Wd = p.readFixed(1)
   147  		case "W1X":
   148  			// ignore.
   149  		case "WY", "W0Y":
   150  			// ignore.
   151  		case "W1Y":
   152  			// ignore.
   153  		case "W", "W0":
   154  			// ignore.
   155  		case "W1":
   156  			// ignore.
   157  		case "VV":
   158  			// ignore.
   159  		case "N":
   160  			ch.Name = p.readStr(1)
   161  		case "B":
   162  			// ignore.
   163  		case "L":
   164  			// ignore.
   165  		}
   166  	}
   167  	p.wdmap[ch.Name] = ch.Wd
   168  	return p.err
   169  }
   170  
   171  func (p *afmParser) readStr(i int) string {
   172  	if len(p.toks) <= i {
   173  		return ""
   174  	}
   175  	return p.toks[i]
   176  }
   177  
   178  func (p *afmParser) readInt(i int) int {
   179  	if len(p.toks) <= i {
   180  		return 0
   181  	}
   182  	return atoi(p.toks[i])
   183  }
   184  
   185  func (p *afmParser) readFixed(i int) int {
   186  	if len(p.toks) <= i {
   187  		return 0
   188  	}
   189  	return fixedFrom(p.toks[i])
   190  }
   191  
   192  func fixedFrom(v string) int {
   193  	v = strings.Replace(v, ",", ".", 1)
   194  	o, err := parseInt16_16(v)
   195  	if err != nil {
   196  		panic(err)
   197  	}
   198  	// FIXME(sbinet): keep digits and precision.
   199  	return int(math.Round(o))
   200  }
   201  
   202  func atoi(s string) int {
   203  	v, err := strconv.Atoi(s)
   204  	if err != nil {
   205  		panic(err)
   206  	}
   207  	return v
   208  }
   209  
   210  func parseInt16_16(s string) (float64, error) {
   211  	f, err := strconv.ParseFloat(s, 32)
   212  	if err != nil {
   213  		return 0, err
   214  	}
   215  	//return int16_16(int32(f * (1 << 16))), nil
   216  	return f, nil
   217  }