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 }