1 /** 2 OpenType/FreeType parsing. 3 4 Copyright: Guillaume Piolat 2018. 5 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 */ 7 module printed.font.opentype; 8 9 import std.stdio; 10 import std.conv; 11 import std..string; 12 import std.uni; 13 import std.algorithm.searching; 14 15 import binrange; 16 17 18 /// A POD-type to represent a range of Unicode characters. 19 struct CharRange 20 { 21 dchar start; 22 dchar stop; 23 } 24 25 /// Font weight 26 enum OpenTypeFontWeight : int 27 { 28 thinest = 0, // Note: thinest doesn't exist in PostScript 29 thin = 100, 30 extraLight = 200, 31 light = 300, 32 normal = 400, 33 medium = 500, 34 semiBold = 600, 35 bold = 700, 36 extraBold = 800, 37 black = 900 38 } 39 40 /// Font style 41 enum OpenTypeFontStyle 42 { 43 normal, 44 italic, 45 oblique 46 } 47 48 /// Should match printed.canvas `TextBaseline` 49 enum FontBaseline 50 { 51 top, 52 hanging, 53 middle, 54 alphabetic, 55 bottom 56 } 57 58 struct OpenTypeTextMetrics 59 { 60 /// The text's advance width, in glyph units. 61 float horzAdvance; 62 } 63 64 /// OpenType 1.8 file parser, for the purpose of finding all fonts in a file, their family name, their weight, etc. 65 /// This OpenType file might either be: 66 /// - a single font 67 /// - a collection file starting with a TTC Header 68 /// You may find the specification here: www.microsoft.com/typography/otspec/ 69 class OpenTypeFile 70 { 71 this(const(ubyte[]) wholeFileContents) 72 { 73 _wholeFileData = wholeFileContents; 74 const(ubyte)[] file = wholeFileContents[]; 75 76 // Read first tag 77 uint firstTag = popBE!uint(file); 78 79 if (firstTag == 0x00010000 || firstTag == 0x4F54544F /* 'OTTO' */) 80 { 81 _isCollection = false; 82 _numberOfFonts = 1; 83 } 84 else if (firstTag == 0x74746366 /* 'ttcf' */) 85 { 86 _isCollection = true; 87 uint version_ = popBE!uint(file); // ignored for now 88 _numberOfFonts = popBE!int(file); 89 90 offsetToOffsetTable.length = _numberOfFonts; 91 foreach(i; 0.._numberOfFonts) 92 offsetToOffsetTable[i] = popBE!uint(file); 93 } 94 else 95 throw new Exception("Couldn't recognize the font file type"); 96 } 97 98 /// Number of fonts in this OpenType file 99 int numberOfFonts() 100 { 101 return _numberOfFonts; 102 } 103 104 private: 105 const(ubyte)[] _wholeFileData; 106 int[] offsetToOffsetTable; 107 108 int _numberOfFonts; 109 bool _isCollection; // It is a TTC or single font? 110 111 uint offsetForFont(int index) 112 { 113 assert(index < numberOfFonts()); 114 115 if (_isCollection) 116 return offsetToOffsetTable[index]; 117 else 118 return 0; 119 } 120 } 121 122 123 /// Parses a font from a font file (which could contain data for several of them). 124 class OpenTypeFont 125 { 126 public: 127 128 this(OpenTypeFile file, int index) 129 { 130 _file = file; 131 _fontIndex = index; 132 _wholeFileData = file._wholeFileData; 133 134 // Figure out font weight, style, and type immediately, as it is useful for font matching 135 136 _isMonospaced = false; 137 138 const(ubyte)[] os2Table = findTable(0x4F532F32 /* 'OS/2' */); 139 if (os2Table !is null) 140 { 141 // Parse weight and style from the 'OS/2' table 142 // Note: Apple documentation says that "Many fonts have inaccurate information in their 'OS/2' table." 143 // Some fonts don't have this table: https://github.com/princjef/font-finder/issues/5 144 145 skipBytes(os2Table, 4); // table version and xAvgCharWidth 146 int usWeightClass = popBE!ushort(os2Table); 147 _weight = cast(OpenTypeFontWeight)( 100 * ( (usWeightClass + 50) / 100 ) ); // round to multiple of 100 148 skipBytes(os2Table, 2 /*usWidthClass*/ + 2 /*fsType*/ + 10 * 2 /*yXXXXXXX*/ + 2 /*sFamilyClass*/); 149 150 ubyte[10] panose; 151 foreach(b; 0..10) 152 panose[b] = popBE!ubyte(os2Table); 153 154 _isMonospaced = (panose[0] == 2) && (panose[3] == 9); 155 156 skipBytes(os2Table, 4*4 /*ulUnicodeRangeN*/ + 4/*achVendID*/); 157 uint fsSelection = popBE!ushort(os2Table); 158 _style = OpenTypeFontStyle.normal; 159 if (fsSelection & 1) 160 _style = OpenTypeFontStyle.italic; 161 if (fsSelection & 512) 162 _style = OpenTypeFontStyle.oblique; 163 } 164 else 165 { 166 // No OS/2 table? parse 'head' instead (some Mac fonts). For monospace, parse 'post' table. 167 const(ubyte)[] postTable = findTable(0x706F7374 /* 'post' */); 168 if (postTable) 169 { 170 skipBytes(postTable, 4 /*version*/ + 4 /*italicAngle*/ + 2 /*underlinePosition*/ + 2 /*underlineThickness*/); 171 _isMonospaced = popBE!uint(postTable) /*isFixedPitch*/ != 0; 172 } 173 174 const(ubyte)[] headTable = findTable(0x68656164 /* 'head' */); 175 if (headTable !is null) 176 { 177 skipBytes(headTable, 4 /*version*/ + 4 /*fontRevision*/ + 4 /*checkSumAdjustment*/ + 4 /*magicNumber*/ 178 + 2 /*flags*/ + 2 /*_unitsPerEm*/ + 16 /*created+modified*/+4*2/*bounding box*/ ); 179 ushort macStyle = popBE!ushort(headTable); 180 181 _weight = OpenTypeFontWeight.normal; 182 if (macStyle & 1) _weight = OpenTypeFontWeight.bold; 183 _style = OpenTypeFontStyle.normal; 184 if (macStyle & 2) _style = OpenTypeFontStyle.italic; 185 } 186 else 187 { 188 // Last chance heuristics. 189 // Font weight heuristic based on family names 190 string subFamily = subFamilyName().toLower; 191 if (subFamily.canFind("thin")) 192 _weight = OpenTypeFontWeight.thin; 193 else if (subFamily.canFind("ultra light")) 194 _weight = OpenTypeFontWeight.thinest; 195 else if (subFamily.canFind("ultraLight")) 196 _weight = OpenTypeFontWeight.thinest; 197 else if (subFamily.canFind("hairline")) 198 _weight = OpenTypeFontWeight.thinest; 199 else if (subFamily.canFind("extralight")) 200 _weight = OpenTypeFontWeight.extraLight; 201 else if (subFamily.canFind("light")) 202 _weight = OpenTypeFontWeight.light; 203 else if (subFamily.canFind("demi bold")) 204 _weight = OpenTypeFontWeight.semiBold; 205 else if (subFamily.canFind("semibold")) 206 _weight = OpenTypeFontWeight.semiBold; 207 else if (subFamily.canFind("extrabold")) 208 _weight = OpenTypeFontWeight.extraBold; 209 else if (subFamily.canFind("bold")) 210 _weight = OpenTypeFontWeight.bold; 211 else if (subFamily.canFind("heavy")) 212 _weight = OpenTypeFontWeight.bold; 213 else if (subFamily.canFind("medium")) 214 _weight = OpenTypeFontWeight.medium; 215 else if (subFamily.canFind("black")) 216 _weight = OpenTypeFontWeight.black; 217 else if (subFamily.canFind("negreta")) 218 _weight = OpenTypeFontWeight.black; 219 else if (subFamily.canFind("regular")) 220 _weight = OpenTypeFontWeight.normal; 221 else if (subFamily == "italic") 222 _weight = OpenTypeFontWeight.normal; 223 else 224 _weight = OpenTypeFontWeight.normal; 225 226 // Font style heuristic based on family names 227 if (subFamily.canFind("italic")) 228 _style = OpenTypeFontStyle.italic; 229 else if (subFamily.canFind("oblique")) 230 _style = OpenTypeFontStyle.oblique; 231 else 232 _style = OpenTypeFontStyle.normal; 233 } 234 } 235 } 236 237 /// Returns: a typographics family name suitable for grouping fonts per family in menus 238 string familyName() 239 { 240 string family = getName(NameID.preferredFamily); 241 if (family is null) 242 return getName(NameID.fontFamily); 243 else 244 return family; 245 } 246 247 /// Returns: a typographics sub-family name suitable for grouping fonts per family in menus 248 string subFamilyName() 249 { 250 string family = getName(NameID.preferredSubFamily); 251 if (family is null) 252 return getName(NameID.fontSubFamily); 253 else 254 return family; 255 } 256 257 /// Returns: the PostScript name of that font, if available. 258 string postScriptName() 259 { 260 return getName(NameID.postscriptName); 261 } 262 263 /// Returns: `trye` is the font is monospaced. 264 bool isMonospaced() 265 { 266 return _isMonospaced; 267 } 268 269 /// Returns: Font weight. 270 OpenTypeFontWeight weight() 271 { 272 return _weight; 273 } 274 275 /// Returns: Font style. 276 OpenTypeFontStyle style() 277 { 278 return _style; 279 } 280 281 /// Returns: The whole OpenType file where this font is located. 282 const(ubyte)[] fileData() 283 { 284 return _wholeFileData; 285 } 286 287 int[4] boundingBox() 288 { 289 computeFontMetrics(); 290 return _boundingBox; 291 } 292 293 /// Returns: Baseline offset above the normal "alphabetical" baseline. 294 /// In glyph units. 295 float getBaselineOffset(FontBaseline baseline) 296 { 297 computeFontMetrics(); 298 final switch(baseline) with (FontBaseline) 299 { 300 case top: 301 // ascent - descent should give the em square, but if it doesn't rescale to have top of em square 302 float actualUnits = _ascender - _descender; 303 return _ascender * _unitsPerEm / actualUnits; 304 305 case hanging: 306 return ascent(); // TODO: correct? 307 308 case middle: 309 // middle of em square 310 float actualUnits = _ascender - _descender; 311 return 0.5f * (_ascender + _descender) * _unitsPerEm / actualUnits; 312 313 case alphabetic: 314 return 0; // the default "baseline" 315 316 case bottom: 317 // ascent - descent should give the em square, but if it doesn't rescale to have bottom of em square 318 float actualUnits = _ascender - _descender; 319 return _descender * _unitsPerEm / actualUnits; 320 } 321 } 322 323 /// Returns: Maximum height above the baseline reached by glyphs in this font. 324 /// In glyph units. 325 int ascent() 326 { 327 computeFontMetrics(); 328 return _ascender; 329 } 330 331 /// Returns: Maximum depth below the baseline reached by glyphs in this font. 332 /// Should be negative. 333 /// In glyph units. 334 int descent() 335 { 336 computeFontMetrics(); 337 return _descender; 338 } 339 340 /// Returns: The spacing between baselines of consecutive lines of text. 341 /// In glyph units. 342 /// Also called "leading". 343 int lineGap() 344 { 345 computeFontMetrics(); 346 return _ascender - _descender + _lineGap; 347 } 348 349 /// Returns: 'A' height. 350 /// TODO: eventually extract from OS/2 table 351 int capHeight() 352 { 353 computeFontMetrics(); 354 return _ascender; // looks like ascent, but perhaps not 355 } 356 357 /// Returns: Italic angle in counter-clockwise degrees from the vertical. 358 /// Zero for upright text, negative for text that leans to the right (forward). 359 float postScriptItalicAngle() 360 { 361 computeFontMetrics(); 362 return _italicAngle / 65536.0f; 363 } 364 365 /// Does this font has a glyph for this codepoint? 366 bool hasGlyphFor(dchar ch) 367 { 368 computeFontMetrics(); 369 ushort* index = ch in _charToGlyphMapping; 370 return index !is null; 371 } 372 373 ushort glyphIndexFor(dchar ch) 374 { 375 computeFontMetrics(); 376 ushort* index = ch in _charToGlyphMapping; 377 if (index) 378 return *index; 379 else 380 return 0; // special value for non available characters 381 } 382 383 /// Returns: left side bearing for this character. 384 int leftSideBearing(dchar ch) 385 { 386 computeFontMetrics(); 387 return _glyphs[ _charToGlyphMapping[ch] ].leftSideBearing; 388 } 389 390 /// Returns: horizontal advance for this character. 391 int horizontalAdvance(dchar ch) 392 { 393 computeFontMetrics(); 394 return _glyphs[ _charToGlyphMapping[ch] ].horzAdvance; 395 } 396 397 /// Returns: number of glyphs in the font. 398 int numGlyphs() 399 { 400 computeFontMetrics(); 401 return cast(int)(_glyphs.length); 402 } 403 404 /// Returns: horizontal advance for a glyph. 405 /// In glyph units. 406 int horizontalAdvanceForGlyph(int glyphIndex) 407 { 408 computeFontMetrics(); 409 return _glyphs[glyphIndex].horzAdvance; 410 } 411 412 /// maximum Unicode char available in this font 413 dchar maxAvailableChar() 414 { 415 computeFontMetrics(); 416 return _maxCodepoint; 417 } 418 419 const(CharRange)[] charRanges() 420 { 421 return _charRanges; 422 } 423 424 // The number of internal units for 1em 425 float UPM() 426 { 427 return _unitsPerEm; 428 } 429 430 // A scale factpr to convert from glyph units to em 431 float invUPM() 432 { 433 return 1.0f / _unitsPerEm; 434 } 435 436 /// Returns text metrics for this piece of text (single line assumed), in glyph units. 437 OpenTypeTextMetrics measureText(string text) 438 { 439 OpenTypeTextMetrics result; 440 double sum = 0; 441 foreach(dchar ch; text) 442 sum += horizontalAdvance(ch); 443 result.horzAdvance = sum; 444 return result; 445 } 446 447 private: 448 // need whole file since some data may be shared across fonts 449 // And also table offsets are relative to the whole file. 450 const(ubyte)[] _wholeFileData; 451 452 OpenTypeFile _file; 453 int _fontIndex; 454 455 // Computed in constructor 456 OpenTypeFontWeight _weight; 457 OpenTypeFontStyle _style; 458 bool _isMonospaced; 459 460 // <parsed-by-computeFontMetrics> 461 462 bool metricsParsed = false; 463 464 // xmin ymin xmax ymax 465 int[4] _boundingBox; 466 467 int _unitsPerEm; 468 469 short _ascender, _descender, _lineGap; 470 int _italicAngle; // fixed 16.16 format 471 472 static struct GlyphDesc 473 { 474 ushort horzAdvance; 475 short leftSideBearing; 476 } 477 GlyphDesc[] _glyphs; 478 479 /// Unicode char to glyph mapping, parsed from 'cmap' table 480 /// Note: it's not sure at all if parsing the 'cmap' table each time is more costly. 481 /// Also this could be an array sorted by dchar. 482 ushort[dchar] _charToGlyphMapping; 483 484 CharRange[] _charRanges; 485 486 dchar _maxCodepoint; 487 488 // </parsed-by-computeFontMetrics> 489 490 /// Returns: A bounding box for each glyph, in glyph space. 491 void computeFontMetrics() 492 { 493 if (metricsParsed) 494 return; 495 metricsParsed = true; 496 497 const(ubyte)[] headTable = getTable(0x68656164 /* 'head' */); 498 499 skipBytes(headTable, 4); // Table version number 500 skipBytes(headTable, 4); // fontRevision 501 skipBytes(headTable, 4); // checkSumAdjustment 502 uint magicNumber = popBE!uint(headTable); 503 if (magicNumber != 0x5F0F3CF5) 504 throw new Exception("Invalid magicNumber in 'head' table."); 505 skipBytes(headTable, 2); // flags 506 _unitsPerEm = popBE!ushort(headTable); 507 skipBytes(headTable, 8); // created 508 skipBytes(headTable, 8); // modified 509 _boundingBox[0] = popBE!short(headTable); 510 _boundingBox[1] = popBE!short(headTable); 511 _boundingBox[2] = popBE!short(headTable); 512 _boundingBox[3] = popBE!short(headTable); 513 skipBytes(headTable, 2); // macStyle 514 skipBytes(headTable, 2); // lowestRecPPEM 515 skipBytes(headTable, 2); // fontDirectionHint 516 skipBytes(headTable, 2); // indexToLocFormat 517 skipBytes(headTable, 2); // glyphDataFormat 518 519 const(ubyte)[] hheaTable = getTable(0x68686561 /* 'hhea' */); 520 skipBytes(hheaTable, 4); // Table version number 521 _ascender = popBE!short(hheaTable); 522 _descender = popBE!short(hheaTable); 523 _lineGap = popBE!short(hheaTable); 524 skipBytes(hheaTable, 2); // advanceWidthMax 525 skipBytes(hheaTable, 2); // minLeftSideBearing 526 skipBytes(hheaTable, 2); // minRightSideBearing 527 skipBytes(hheaTable, 2); // xMaxExtent 528 skipBytes(hheaTable, 2); // caretSlopeRise 529 skipBytes(hheaTable, 2); // caretSlopeRun 530 skipBytes(hheaTable, 2); // caretOffset 531 skipBytes(hheaTable, 8); // reserved 532 short metricDataFormat = popBE!short(hheaTable); 533 if (metricDataFormat != 0) 534 throw new Exception("Unsupported metricDataFormat in 'hhea' table"); 535 536 int numberOfHMetrics = popBE!ushort(hheaTable); 537 538 const(ubyte)[] maxpTable = getTable(0x6D617870 /* 'maxp' */); 539 skipBytes(maxpTable, 4); // version 540 int numGlyphs = popBE!ushort(maxpTable); 541 542 _glyphs.length = numGlyphs; 543 544 const(ubyte)[] hmtxTable = getTable(0x686D7478 /* 'hmtx' */); 545 546 ushort lastAdvance = 0; 547 foreach(g; 0..numberOfHMetrics) 548 { 549 lastAdvance = popBE!ushort(hmtxTable); 550 _glyphs[g].horzAdvance = lastAdvance; 551 _glyphs[g].leftSideBearing = popBE!short(hmtxTable); 552 } 553 foreach(g; numberOfHMetrics.._glyphs.length) 554 { 555 _glyphs[g].horzAdvance = lastAdvance; 556 _glyphs[g].leftSideBearing = popBE!short(hmtxTable); 557 } 558 559 // Parse italicAngle 560 const(ubyte)[] postTable = getTable(0x706F7374 /* 'post' */); 561 skipBytes(postTable, 4); // version 562 _italicAngle = popBE!int(postTable); 563 564 parseCMAP(); 565 } 566 567 /// Parses all codepoints-to-glyph mappings, fills the hashmap `_charToGlyphMapping` 568 void parseCMAP() 569 { 570 const(ubyte)[] cmapTableFull = getTable(0x636d6170 /* 'cmap' */); 571 const(ubyte)[] cmapTable = cmapTableFull; 572 573 skipBytes(cmapTable, 2); // version 574 int numTables = popBE!ushort(cmapTable); 575 576 // Looking for a BMP Unicode 'cmap' only 577 for(int table = 0; table < numTables; ++table) 578 { 579 ushort platformID = popBE!ushort(cmapTable); 580 ushort encodingID = popBE!ushort(cmapTable); 581 uint offset = popBE!uint(cmapTable); 582 583 // in stb_truetype, only case supported, seems to be common 584 if (platformID == 3 && (encodingID == 1 /* Unicode UCS-2 */ || encodingID == 4 /* Unicode UCS-4 */)) 585 { 586 const(ubyte)[] subTable = cmapTableFull[offset..$]; 587 ushort format = popBE!ushort(subTable); 588 589 // TODO: support other format because this only works within the BMP 590 if (format == 4) 591 { 592 ushort len = popBE!ushort(subTable); 593 skipBytes(subTable, 2); // language, not useful here 594 int segCountX2 = popBE!ushort(subTable); 595 if ((segCountX2 % 2) != 0) 596 throw new Exception("segCountX2 is not an even number"); 597 int segCount = segCountX2/2; 598 int searchRange = popBE!ushort(subTable); 599 int entrySelector = popBE!ushort(subTable); 600 int rangeShift = popBE!ushort(subTable); 601 602 int[] endCount = new int[segCount]; 603 int[] startCount = new int[segCount]; 604 short[] idDelta = new short[segCount]; 605 606 int[] idRangeOffset = new int[segCount]; 607 608 foreach(seg; 0..segCount) 609 endCount[seg] = popBE!ushort(subTable); 610 skipBytes(subTable, 2); // reserved, should be zero 611 612 foreach(seg; 0..segCount) 613 startCount[seg] = popBE!ushort(subTable); 614 615 foreach(seg; 0..segCount) 616 idDelta[seg] = popBE!short(subTable); 617 618 const(ubyte)[] idRangeOffsetArray = subTable; 619 620 foreach(seg; 0..segCount) 621 idRangeOffset[seg] = popBE!ushort(subTable); 622 623 foreach(seg; 0..segCount) 624 { 625 _charRanges ~= CharRange(startCount[seg], endCount[seg]); 626 627 foreach(dchar ch; startCount[seg]..endCount[seg]) 628 { 629 ushort glyphIndex; 630 631 if (idRangeOffset[seg] == 0) 632 { 633 glyphIndex = cast(ushort)( cast(ushort)ch + idDelta[seg] ); 634 } 635 else 636 { 637 if ((idRangeOffset[seg] % 2) != 0) 638 throw new Exception("idRangeOffset[i] is not an even number"); 639 640 // Yes, this is what the spec says to do 641 ushort* p = cast(ushort*)(idRangeOffsetArray.ptr); 642 p = p + seg; 643 p = p + (ch - startCount[seg]); 644 p = p + (idRangeOffset[seg]/2); 645 ubyte[] pslice = cast(ubyte[])(p[0..1]); 646 glyphIndex = popBE!ushort(pslice); 647 648 if (glyphIndex == 0) // missing glyph 649 continue; 650 glyphIndex += idDelta[seg]; 651 } 652 653 if (glyphIndex >= _glyphs.length) 654 { 655 throw new Exception("Non existing glyph index"); 656 } 657 _charToGlyphMapping[ch] = glyphIndex; 658 659 if (ch > _maxCodepoint) 660 _maxCodepoint = ch; 661 } 662 } 663 } 664 else 665 throw new Exception("Unsupported 'cmap' format"); 666 break; 667 } 668 } 669 } 670 671 /// Returns: an index in the file, where that table start for this particular font. 672 const(ubyte)[] findTable(uint fourCC) 673 { 674 int offsetToOffsetTable = _file.offsetForFont(_fontIndex); 675 const(ubyte)[] offsetTable = _wholeFileData[offsetToOffsetTable..$]; 676 677 uint firstTag = popBE!uint(offsetTable); 678 679 if (firstTag != 0x00010000 && firstTag != 0x4F54544F /* 'OTTO' */) 680 throw new Exception("Unrecognized tag in Offset Table"); 681 682 int numTables = popBE!ushort(offsetTable); 683 skipBytes(offsetTable, 6); 684 685 const(uint)[] tableRecordEntries = cast(uint[])(offsetTable[0..16*numTables]); 686 687 // Binary search following 688 // https://en.wikipedia.org/wiki/Binary_search_algorithm#Algorithm 689 int L = 0; 690 int R = numTables - 1; 691 while (L <= R) 692 { 693 int m = (L+R)/2; 694 uint tag = forceBigEndian(tableRecordEntries[m * 4]); 695 if (tag < fourCC) 696 L = m + 1; 697 else if (tag > fourCC) 698 R = m - 1; 699 else 700 { 701 // found 702 assert (tag == fourCC); 703 uint checkSum = forceBigEndian(tableRecordEntries[m*4+1]); 704 uint offset = forceBigEndian(tableRecordEntries[m*4+2]); 705 uint len = forceBigEndian(tableRecordEntries[m*4+3]); 706 return _wholeFileData[offset..offset+len]; 707 } 708 } 709 return null; // not found 710 } 711 712 /// Ditto, but throw if not found. 713 const(ubyte)[] getTable(uint fourCC) 714 { 715 const(ubyte)[] result = findTable(fourCC); 716 if (result is null) 717 throw new Exception(format("Table not found: %s", fourCC)); 718 return result; 719 } 720 721 /// Returns: that "name" information, in UTF-8 722 string getName(NameID requestedNameID) 723 { 724 const(ubyte)[] nameTable = getTable(0x6e616d65 /* 'name' */); 725 const(ubyte)[] nameTableParsed = nameTable; 726 727 ushort format = popBE!ushort(nameTableParsed); 728 if (format > 1) 729 throw new Exception("Unrecognized format in 'name' table"); 730 731 ushort numNameRecords = popBE!ushort(nameTableParsed); 732 ushort stringOffset = popBE!ushort(nameTableParsed); 733 734 const(ubyte)[] stringDataStorage = nameTable[stringOffset..$]; 735 736 foreach(i; 0..numNameRecords) 737 { 738 PlatformID platformID = cast(PlatformID) popBE!ushort(nameTableParsed); 739 ushort encodingID = popBE!ushort(nameTableParsed); 740 ushort languageID = popBE!ushort(nameTableParsed); 741 ushort nameID = popBE!ushort(nameTableParsed); 742 ushort length = popBE!ushort(nameTableParsed); 743 ushort offset = popBE!ushort(nameTableParsed); // String offset from start of storage area (in bytes) 744 if (nameID == requestedNameID) 745 { 746 // found 747 const(ubyte)[] stringSlice = stringDataStorage[offset..offset+length]; 748 string name; 749 750 if (platformID == PlatformID.macintosh && encodingID == 0) 751 { 752 // MacRoman encoding 753 name = decodeMacRoman(stringSlice); 754 } 755 else 756 { 757 // Most of the time it's UTF16-BE 758 name = decodeUTF16BE(stringSlice); 759 } 760 return name; 761 } 762 } 763 764 return null; // not found 765 } 766 767 enum PlatformID : ushort 768 { 769 unicode, 770 macintosh, 771 iso, 772 windows, 773 custom, 774 } 775 776 enum NameID : ushort 777 { 778 copyrightNotice = 0, 779 fontFamily = 1, 780 fontSubFamily = 2, 781 uniqueFontIdentifier = 3, 782 fullFontName = 4, 783 versionString = 5, 784 postscriptName = 6, 785 trademark = 7, 786 manufacturer = 8, 787 designer = 9, 788 description = 10, 789 preferredFamily = 16, 790 preferredSubFamily = 17, 791 } 792 } 793 794 795 private: 796 797 uint forceBigEndian(ref const(uint) x) pure nothrow @nogc 798 { 799 version(BigEndian) 800 return x; 801 else 802 { 803 import core.bitop: bswap; 804 return bswap(x); 805 } 806 } 807 808 string decodeMacRoman(const(ubyte)[] input) pure 809 { 810 static immutable dchar[128] CONVERT_TABLE = 811 [ 812 'Ä', 'Å', 'Ç', 'É', 'Ñ', 'Ö', 'Ü', 'á', 'à', 'â', 'ä' , 'ã', 'å', 'ç', 'é', 'è', 813 'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ñ', 'ó', 'ò', 'ô', 'ö' , 'õ', 'ú', 'ù', 'û', 'ü', 814 '†', '°', '¢', '£', '§', '•', '¶', 'ß', '®', '©', '™' , '´', '¨', '≠', 'Æ', 'Ø', 815 '∞', '±', '≤', '≥', '¥', 'µ', '∂', '∑', '∏', 'π', '∫' , 'ª', 'º', 'Ω', 'æ', 'ø', 816 '¿', '¡', '¬', '√', 'ƒ', '≈', '∆', '«', '»', '…', '\xA0', 'À', 'Ã', 'Õ', 'Œ', 'œ', 817 '–', '—', '“', '”', '‘', '’', '÷', '◊', 'ÿ', 'Ÿ', '⁄' , '€', '‹', '›', 'fi', 'fl', 818 '‡', '·', '‚', '„', '‰', 'Â', 'Ê', 'Á', 'Ë', 'È', 'Í' , 'Î', 'Ï', 'Ì', 'Ó', 'Ô', 819 '?', 'Ò', 'Ú', 'Û', 'Ù', 'ı', 'ˆ', '˜', '¯', '˘', '˙' , '˚', '¸', '˝', '˛', 'ˇ', 820 ]; 821 822 string textTranslated = ""; 823 foreach(i; 0..input.length) 824 { 825 char c = input[i]; 826 dchar ch; 827 if (c < 128) 828 ch = c; 829 else 830 ch = CONVERT_TABLE[c - 128]; 831 textTranslated ~= ch; 832 } 833 return textTranslated; 834 } 835 836 string decodeUTF16BE(const(ubyte)[] input) pure 837 { 838 wstring utf16 = ""; 839 840 if ((input.length % 2) != 0) 841 throw new Exception("Couldn't decode UTF-16 string"); 842 843 int numCodepoints = cast(int)(input.length)/2; 844 for (int i = 0; i < numCodepoints; ++i) 845 { 846 wchar ch = popBE!ushort(input); 847 utf16 ~= ch; 848 } 849 return to!string(utf16); 850 }