github.com/pdfcpu/pdfcpu@v0.11.1/pkg/font/metrics.go (about)

     1  /*
     2  Copyright 2018 The pdfcpu Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package font
    18  
    19  import (
    20  	"encoding/gob"
    21  	"fmt"
    22  	"math"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"runtime/debug"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  
    31  	"github.com/pdfcpu/pdfcpu/internal/corefont/metrics"
    32  	"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types"
    33  
    34  	"github.com/pkg/errors"
    35  )
    36  
    37  // TTFLight represents a TrueType font w/o font file.
    38  type TTFLight struct {
    39  	PostscriptName     string            // name: NameID 6
    40  	Protected          bool              // OS/2: fsType
    41  	UnitsPerEm         int               // head: unitsPerEm
    42  	Ascent             int               // OS/2: sTypoAscender
    43  	Descent            int               // OS/2: sTypoDescender
    44  	CapHeight          int               // OS/2: sCapHeight
    45  	FirstChar          uint16            // OS/2: fsFirstCharIndex
    46  	LastChar           uint16            // OS/2: fsLastCharIndex
    47  	UnicodeRange       [4]uint32         // OS/2: Unicode Character Range
    48  	LLx, LLy, URx, URy float64           // head: xMin, yMin, xMax, yMax (fontbox)
    49  	ItalicAngle        float64           // post: italicAngle
    50  	FixedPitch         bool              // post: isFixedPitch
    51  	Bold               bool              // OS/2: usWeightClass == 7
    52  	HorMetricsCount    int               // hhea: numOfLongHorMetrics
    53  	GlyphCount         int               // maxp: numGlyphs
    54  	GlyphWidths        []int             // hmtx: fd.HorMetricsCount.advanceWidth
    55  	Chars              map[uint32]uint16 // cmap: Unicode character to glyph index
    56  	ToUnicode          map[uint16]uint32 // map glyph index to unicode character
    57  	Planes             map[int]bool      // used Unicode planes
    58  }
    59  
    60  func (fd TTFLight) String() string {
    61  	return fmt.Sprintf(`
    62   PostscriptName = %s
    63        Protected = %t
    64       UnitsPerEm = %d
    65           Ascent = %d
    66          Descent = %d
    67        CapHeight = %d
    68        FirstChar = %d
    69         LastChar = %d
    70  FontBoundingBox = (%.2f, %.2f, %.2f, %.2f)
    71      ItalicAngle = %.2f
    72       FixedPitch = %t
    73             Bold = %t
    74  HorMetricsCount = %d
    75  	 GlyphCount = %d
    76  len(GlyphWidths) = %d`,
    77  		fd.PostscriptName,
    78  		fd.Protected,
    79  		fd.UnitsPerEm,
    80  		fd.Ascent,
    81  		fd.Descent,
    82  		fd.CapHeight,
    83  		fd.FirstChar,
    84  		fd.LastChar,
    85  		fd.LLx, fd.LLy, fd.URx, fd.URy,
    86  		fd.ItalicAngle,
    87  		fd.FixedPitch,
    88  		fd.Bold,
    89  		fd.HorMetricsCount,
    90  		fd.GlyphCount,
    91  		len(fd.GlyphWidths),
    92  	)
    93  }
    94  
    95  func (fd TTFLight) supportsUnicodeBlock(bit int) bool {
    96  	i := fd.UnicodeRange[bit/32]
    97  	i >>= uint32(bit) % 32
    98  	return i&1 > 0
    99  }
   100  
   101  func (fd TTFLight) supportsUnicodeBlocks(bits []int) bool {
   102  	// return true if we have support for the first or one of the following unicodeBlocks.
   103  	ok := fd.supportsUnicodeBlock(bits[0])
   104  	if ok || len(bits) == 1 {
   105  		return ok
   106  	}
   107  	for i := range bits[1:] {
   108  		if fd.supportsUnicodeBlock(i) {
   109  			return true
   110  		}
   111  	}
   112  	return false
   113  }
   114  
   115  func (fd TTFLight) unicodeRangeBits(id string) []int {
   116  	// Map iso15924 script codes (=id) to corresponding unicode blocks.
   117  	// Returns a slice of relevant unicodeRangeBits.
   118  	//
   119  	// This mapping is incomplete as we only cover unicode blocks of the most popular scripts.
   120  	// Please go to https://github.com/pdfcpu/pdfcpu/issues/new/choose for an extension request.
   121  	//
   122  	//  0 Basic Latin						0000-007F
   123  	//  1 Latin-1 Supplement				0080-00FF
   124  	//  2 Latin Extended-A					0100-017F
   125  	//  3 Latin Extended-B					0180-024F
   126  	//  7 Greek								0370-03FF
   127  	//  9 Cyrillic							0400-04FF
   128  	// 10 Armenian							0530-058F
   129  	// 11 Hebrew							0590-05FF
   130  	// 13 Arabic							0600-06FF
   131  	// 15 Devanagari						0900-097F
   132  	// 16 Bengali							0980-09FF
   133  	// 24 Thai								0E00-0E7F
   134  	// 28 Hangul Jamo						1100-11FF
   135  	// 48 CJK Symbols And Punctuation		3000-303F
   136  	// 49 Hiragana							3040-309F
   137  	// 50 Katakana							30A0-30FF
   138  	// 52 Hangul Compatibility Jamo			3130-318F
   139  	// 61 CJK Strokes						31C0-31EF
   140  	// 54 Enclosed CJK Letters And Months	3200-32FF
   141  	// 55 CJK Compatibility					3300-33FF
   142  	// 59 CJK Unified Ideographs			4E00-9FFF
   143  	// 56 Hangul Syllables					AC00-D7AF
   144  
   145  	var a []int
   146  	switch id {
   147  	case "LATN": // Latin
   148  		a = append(a, 0, 1, 2, 3)
   149  	case "GREK": // Greek
   150  		a = append(a, 7)
   151  	case "CYRL": // Cyrillic
   152  		a = append(a, 9)
   153  	case "ARMN": // Armenian
   154  		a = append(a, 10)
   155  	case "HEBR": // Hebrew
   156  		a = append(a, 11)
   157  	case "ARAB": // Arabic
   158  		a = append(a, 13)
   159  	case "DEVA": // Devanagari
   160  		a = append(a, 15)
   161  	case "BENG": // Bengali
   162  		a = append(a, 16)
   163  	case "THAI": // Thai
   164  		a = append(a, 24)
   165  	case "HIRA": // Hiragana
   166  		a = append(a, 49)
   167  	case "KANA": // Katakana
   168  		a = append(a, 50)
   169  	case "JPAN": // Japanese
   170  		a = append(a, 59, 49, 50)
   171  	case "KORE", "HANG": // Korean, Hangul
   172  		a = append(a, 59, 28, 52, 56)
   173  	case "HANS", "HANT": // Han Simplified, Han Traditional
   174  		a = append(a, 59)
   175  	}
   176  
   177  	return a
   178  }
   179  
   180  // SupportsScript returns true if ttf supports the unicodeblocks identified by iso15924 id.
   181  func (fd TTFLight) SupportsScript(id string) (bool, error) {
   182  
   183  	if len(id) != 4 {
   184  		return false, errors.New("\"script\" must be a iso15924 code (length = 4")
   185  	}
   186  
   187  	bits := fd.unicodeRangeBits(id)
   188  	if bits == nil {
   189  		return false, errors.New("\"script\" must be one of: ARAB, ARMN, CYRL, GREK, HANG, HANS, HANT, HEBR, HIRA, LATN, JPAN, KANA, KORE, THAI")
   190  	}
   191  
   192  	return fd.supportsUnicodeBlocks(bits), nil
   193  }
   194  
   195  // UserFontDir is the location for installed TTF or OTF font files.
   196  var UserFontDir string
   197  
   198  // UserFontMetrics represents font metrics for TTF or OTF font files installed into UserFontDir.
   199  var UserFontMetrics = map[string]TTFLight{}
   200  var UserFontMetricsLock = &sync.RWMutex{}
   201  
   202  func load(fileName string, fd *TTFLight) error {
   203  	//fmt.Printf("reading gob from: %s\n", fileName)
   204  	f, err := os.Open(fileName)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	defer f.Close()
   209  	dec := gob.NewDecoder(f)
   210  	return dec.Decode(fd)
   211  }
   212  
   213  // Read reads in the font file bytes from gob
   214  func Read(fileName string) ([]byte, error) {
   215  	fn := filepath.Join(UserFontDir, fileName+".gob")
   216  	f, err := os.Open(fn)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  	defer f.Close()
   221  	dec := gob.NewDecoder(f)
   222  	ff := &struct{ FontFile []byte }{}
   223  	err = dec.Decode(ff)
   224  	return ff.FontFile, err
   225  }
   226  
   227  func isSupportedFontFile(filename string) bool {
   228  	return strings.HasSuffix(strings.ToLower(filename), ".gob")
   229  }
   230  
   231  // LoadUserFonts loads any installed TTF or OTF font files.
   232  func LoadUserFonts() error {
   233  	//fmt.Printf("loading userFonts from %s\n", UserFontDir)
   234  	files, err := os.ReadDir(UserFontDir)
   235  	if err != nil {
   236  		return err
   237  	}
   238  	for _, f := range files {
   239  		if !isSupportedFontFile(f.Name()) {
   240  			continue
   241  		}
   242  		ttf := TTFLight{}
   243  		fn := filepath.Join(UserFontDir, f.Name())
   244  		if err := load(fn, &ttf); err != nil {
   245  			return err
   246  		}
   247  		fn = strings.TrimSuffix(f.Name(), path.Ext(f.Name()))
   248  		//fmt.Printf("loading %s.ttf...\n", fn)
   249  		//fmt.Printf("Loaded %s:\n%s", fn, ttf)
   250  		UserFontMetricsLock.Lock()
   251  		UserFontMetrics[fn] = ttf
   252  		UserFontMetricsLock.Unlock()
   253  	}
   254  	return nil
   255  }
   256  
   257  // BoundingBox returns the font bounding box for a given font as specified in the corresponding AFM file.
   258  func BoundingBox(fontName string) *types.Rectangle {
   259  	if IsCoreFont(fontName) {
   260  		return metrics.CoreFontMetrics[fontName].FBox
   261  	}
   262  	UserFontMetricsLock.RLock()
   263  	defer UserFontMetricsLock.RUnlock()
   264  	llx := UserFontMetrics[fontName].LLx
   265  	lly := UserFontMetrics[fontName].LLy
   266  	urx := UserFontMetrics[fontName].URx
   267  	ury := UserFontMetrics[fontName].URy
   268  	return types.NewRectangle(llx, lly, urx, ury)
   269  }
   270  
   271  // CharWidth returns the character width for a char and font in glyph space units.
   272  func CharWidth(fontName string, r rune) int {
   273  	if IsCoreFont(fontName) {
   274  		return metrics.CoreFontCharWidth(fontName, int(r))
   275  	}
   276  	UserFontMetricsLock.RLock()
   277  	defer UserFontMetricsLock.RUnlock()
   278  	ttf, ok := UserFontMetrics[fontName]
   279  	if !ok {
   280  		fmt.Fprintf(os.Stderr, "pdfcpu: user font not loaded: %s\n", fontName)
   281  		debug.PrintStack()
   282  		os.Exit(1)
   283  	}
   284  
   285  	pos, ok := ttf.Chars[uint32(r)]
   286  	if !ok {
   287  		pos = 0
   288  	}
   289  	return int(ttf.GlyphWidths[pos])
   290  }
   291  
   292  // UserSpaceUnits transforms glyphSpaceUnits into userspace units.
   293  func UserSpaceUnits(glyphSpaceUnits float64, fontScalingFactor int) float64 {
   294  	return glyphSpaceUnits / 1000 * float64(fontScalingFactor)
   295  }
   296  
   297  // GlyphSpaceUnits transforms userSpaceUnits into glyphspace Units.
   298  func GlyphSpaceUnits(userSpaceUnits float64, fontScalingFactor int) float64 {
   299  	return userSpaceUnits * 1000 / float64(fontScalingFactor)
   300  }
   301  
   302  func fontScalingFactor(glyphSpaceUnits, userSpaceUnits float64) int {
   303  	return int(math.Round(userSpaceUnits / glyphSpaceUnits * 1000))
   304  }
   305  
   306  // Descent returns fontname's descent in userspace units corresponding to fontSize.
   307  func Descent(fontName string, fontSize int) float64 {
   308  	fbb := BoundingBox(fontName)
   309  	return UserSpaceUnits(-fbb.LL.Y, fontSize)
   310  }
   311  
   312  // Ascent returns fontname's ascent in userspace units corresponding to fontSize.
   313  func Ascent(fontName string, fontSize int) float64 {
   314  	fbb := BoundingBox(fontName)
   315  	return UserSpaceUnits(fbb.Height()+fbb.LL.Y, fontSize)
   316  }
   317  
   318  // LineHeight returns fontname's line height in userspace units corresponding to fontSize.
   319  func LineHeight(fontName string, fontSize int) float64 {
   320  	fbb := BoundingBox(fontName)
   321  	return UserSpaceUnits(fbb.Height(), fontSize)
   322  }
   323  
   324  func glyphSpaceWidth(text, fontName string) int {
   325  	var w int
   326  	if IsCoreFont(fontName) {
   327  		for i := 0; i < len(text); i++ {
   328  			c := text[i]
   329  			w += CharWidth(fontName, rune(c))
   330  		}
   331  		return w
   332  	}
   333  	for _, r := range text {
   334  		w += CharWidth(fontName, r)
   335  	}
   336  	return w
   337  }
   338  
   339  // TextWidth represents the width in user space units for a given text string, font name and font size.
   340  func TextWidth(text, fontName string, fontSize int) float64 {
   341  	w := glyphSpaceWidth(text, fontName)
   342  	return UserSpaceUnits(float64(w), fontSize)
   343  }
   344  
   345  // Size returns the needed font size (aka. font scaling factor) in points
   346  // for rendering a given text string using a given font name with a given user space width.
   347  func Size(text, fontName string, width float64) int {
   348  	w := glyphSpaceWidth(text, fontName)
   349  	return fontScalingFactor(float64(w), width)
   350  }
   351  
   352  // SizeForLineHeight returns the needed font size in points
   353  // for rendering using a given font name fitting into given line height lh.
   354  func SizeForLineHeight(fontName string, lh float64) int {
   355  	fbb := BoundingBox(fontName)
   356  	return int(math.Round(lh / (fbb.Height() / 1000)))
   357  }
   358  
   359  // UserSpaceFontBBox returns the font box for given font name and font size in user space coordinates.
   360  func UserSpaceFontBBox(fontName string, fontSize int) *types.Rectangle {
   361  	fontBBox := BoundingBox(fontName)
   362  	llx := UserSpaceUnits(fontBBox.LL.X, fontSize)
   363  	lly := UserSpaceUnits(fontBBox.LL.Y, fontSize)
   364  	urx := UserSpaceUnits(fontBBox.UR.X, fontSize)
   365  	ury := UserSpaceUnits(fontBBox.UR.Y, fontSize)
   366  	return types.NewRectangle(llx, lly, urx, ury)
   367  }
   368  
   369  // IsCoreFont returns true for the 14 PDF standard Type 1 	fonts.
   370  func IsCoreFont(fontName string) bool {
   371  	_, ok := metrics.CoreFontMetrics[fontName]
   372  	return ok
   373  }
   374  
   375  // CoreFontNames returns a list of the 14 PDF standard Type 1 fonts.
   376  func CoreFontNames() []string {
   377  	ss := []string{}
   378  	for fontName := range metrics.CoreFontMetrics {
   379  		ss = append(ss, fontName)
   380  	}
   381  	return ss
   382  }
   383  
   384  // IsUserFont returns true for installed TrueType fonts.
   385  func IsUserFont(fontName string) bool {
   386  	UserFontMetricsLock.RLock()
   387  	defer UserFontMetricsLock.RUnlock()
   388  	_, ok := UserFontMetrics[fontName]
   389  	return ok
   390  }
   391  
   392  // UserFontNames return a list of all installed TrueType fonts.
   393  func UserFontNames() []string {
   394  	ss := []string{}
   395  	UserFontMetricsLock.RLock()
   396  	defer UserFontMetricsLock.RUnlock()
   397  	for fontName := range UserFontMetrics {
   398  		ss = append(ss, fontName)
   399  	}
   400  	return ss
   401  }
   402  
   403  // UserFontNamesVerbose return a list of all installed TrueType fonts including glyph count.
   404  func UserFontNamesVerbose() []string {
   405  	ss := []string{}
   406  	UserFontMetricsLock.RLock()
   407  	defer UserFontMetricsLock.RUnlock()
   408  	for fName, ttf := range UserFontMetrics {
   409  		s := fName + " (" + strconv.Itoa(ttf.GlyphCount) + " glyphs)"
   410  		ss = append(ss, s)
   411  	}
   412  	return ss
   413  }
   414  
   415  // SupportedFont returns true for core fonts or user installed fonts.
   416  func SupportedFont(fontName string) bool {
   417  	return IsCoreFont(fontName) || IsUserFont(fontName)
   418  }
   419  
   420  func (fd TTFLight) Gids() []int {
   421  	gids := make([]int, 0, len(fd.Chars))
   422  	for _, g := range fd.Chars {
   423  		gids = append(gids, int(g))
   424  	}
   425  	return gids
   426  }