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 }