1 /*
2   Copyright Chris Jones 2020.
3   Distributed under the Boost Software License, Version 1.0.
4   See accompanying file Licence.txt or copy at...
5   https://www.boost.org/LICENSE_1_0.txt
6 */
7 
8 module dg2d.truetype;
9 
10 import dg2d.path;
11 
12 /*
13   TrueType font loader.
14 
15   refs:
16   https://developer.apple.com/fonts/TrueType-Reference-Manual/
17   https://docs.microsoft.com/en-gb/typography/opentype/spec/
18   https://github.com/nothings/stb/blob/master/stb_truetype.h
19 */
20 
21 /*
22   errorOp is used as a mixin to make it esier to debug font file errors
23   you need to pass the "-mixin" flag to compiler for it to be any use
24 */
25 
26 //private enum errorOp = "{ return ttFail; }"; // this for normal use
27 private enum errorOp = "{ asm { int 3; }}";  // this for debugging font
28 
29 enum
30 {
31     ttFail = 0,
32     ttSuccess = 1
33 }
34 
35 // helpers for reading big endian values from array of bytes
36 
37 private:
38 
39 short read_short(ubyte* ptr)
40 {
41     return cast(short)(ptr[1] | (ptr[0]<<8));
42 }
43 
44 ushort read_ushort(ubyte* ptr)
45 {
46     return cast(ushort)(ptr[1] | (ptr[0]<<8));
47 }
48 
49 int read_int(ubyte* ptr)
50 {
51     return cast(int)(ptr[3] | (ptr[2]<<8)| (ptr[1]<<16)| (ptr[0]<<24));
52 }
53 
54 uint read_uint(ubyte* ptr)
55 {
56     return cast(uint)(ptr[3] | (ptr[2]<<8)| (ptr[1]<<16)| (ptr[0]<<24));
57 }
58 
59 uint read_tag(string str)
60 {
61     assert (str.length == 4);
62     return str[3] | (str[2]<<8)| (str[1]<<16)| (str[0]<<24);
63 }
64 
65 /*
66   ttFontInfo
67 */
68 
69 public:
70 
71 struct ttFontInfo
72 {
73     ubyte[]    glyf;
74     ubyte[]    loca;
75     ubyte[]    head; 
76     ubyte[]    kern;
77     ubyte[]    hhea;
78     ubyte[]    hmtx;
79     ubyte[]    cmap; // actually points to sub table
80     uint       sfntver;
81     bool       locaShortForm;
82     int        numGlyphs;
83     int        unitsPerEm;
84     int        numHMetrics;
85     int        lineGap;
86 }
87 
88 // cmap encoding flags
89 
90 private:
91 
92 enum {
93     PID_Unicode   = 0,
94     PID_Mac       = 1,
95     PID_Iso       = 2,
96     PID_Microsoft = 3,
97 
98     MSEID_Symbol      = 0,
99     MSEID_UnicodeBmp  = 1,
100     MSEID_Shiftjis    = 2,
101     MSEID_UnicodeFull = 10
102 }
103 
104 /*
105   Loads font from raw bytes and returns ttFontInfo struct
106 */
107 
108 public:
109 
110 int ttLoadFont(ref ttFontInfo info, ubyte[] rawfont)
111 {
112     // check room for header, read offset table
113     
114     if (rawfont.length < 12) mixin(errorOp);
115     info.sfntver = read_int(rawfont.ptr);
116     int numtbl = read_ushort(rawfont.ptr+4);
117 
118     // check room for table record entries
119 
120     if (rawfont.length < (12+numtbl*16)) mixin(errorOp);
121 
122     // find chunk
123 
124     uint findChunk(string tag, ref ubyte[] data)
125     {
126         foreach(i; 0..numtbl)
127         {
128             ubyte* cnkptr = rawfont.ptr+12+i*16;
129 
130             if (read_uint(cnkptr) == read_tag(tag))
131             {
132                 uint offset = read_uint(cnkptr+8);
133                 uint length = read_uint(cnkptr+12);
134                 if ((offset+length) > rawfont.length) return ttFail;
135                 data = rawfont[offset..offset+length];
136                 return ttSuccess;
137             }
138         }
139         return ttFail;
140     }
141 
142     // fill out chunks we're interested in
143 
144     if (findChunk("glyf",info.glyf) == ttFail) mixin(errorOp);
145     if (findChunk("loca",info.loca) == ttFail) mixin(errorOp);
146     if (findChunk("head",info.head) == ttFail) mixin(errorOp);
147     if (findChunk("hhea",info.hhea) == ttFail) mixin(errorOp);
148     if (findChunk("hmtx",info.hmtx) == ttFail) mixin(errorOp);
149     findChunk("kern",info.kern); // not essential
150 
151     // lookup number of glyphs
152 
153     ubyte[] tmp;
154     if (findChunk("maxp",tmp) == ttFail) mixin(errorOp);
155     if (tmp.length < 32) mixin(errorOp);
156     info.numGlyphs = read_ushort(tmp.ptr+4);
157 
158     // Number of hMetric entries in 'hmtx' table
159 
160     if (info.hhea.length < 36) mixin(errorOp);
161     info.numHMetrics = read_ushort(info.hhea.ptr+34);
162 
163     // lookup loca table format, and units per em
164 
165     if (info.head.length < 54) mixin(errorOp);
166     info.locaShortForm = (read_short(info.head.ptr+50) == 0);
167     info.unitsPerEm = read_ushort(info.hhea.ptr+18);
168     info.lineGap = read_short(info.hhea.ptr+8);
169 
170     // check kern table has enough for header info 
171 
172     if (info.kern.length < 4) mixin(errorOp);
173 
174     // find character mapping table, we lookup cmap table, and
175     // then determine which subtable to use and set the info.cmap to 
176     // that subtable.
177 
178     if (findChunk("cmap",tmp) == ttFail) mixin(errorOp);
179 
180     int subtabs = read_ushort(tmp.ptr+2);
181     if (tmp.length < 4+subtabs*8) mixin(errorOp);
182 
183     foreach (i; 0..subtabs)
184     {
185         ubyte* ptr = tmp.ptr+4+i*8;
186         ushort platId = read_ushort(ptr);
187         ushort ncodId = read_ushort(ptr+2);
188         uint offset =  read_uint(ptr+4);
189 
190         if ((platId == PID_Microsoft) && 
191            ((ncodId == MSEID_UnicodeBmp) || (ncodId == MSEID_UnicodeFull)))
192         {
193             if (tmp.length < offset+4) mixin(errorOp);
194             int len = read_ushort(tmp.ptr+offset+2);
195             if (tmp.length < offset+len) mixin(errorOp);
196             info.cmap = tmp[offset..offset+len];
197         }
198     }
199 
200     if (info.cmap.length < 4) mixin(errorOp); // check room for table header
201 
202     return ttSuccess;
203 }
204 
205 // convert char code to glyph index 
206 
207 uint ttCharToGlyph(ref ttFontInfo info, uint charCode)
208 {
209     ushort format = read_ushort(info.cmap.ptr);
210 
211     if (format == 0) // Byte encoding table
212     {
213         if (6+charCode < info.cmap.length) return info.cmap[6+charCode];
214         return 0;
215     }
216     else if (format == 4) // Segment mapping to delta values
217     {
218         if (charCode >= 0xFFFF) return 0;
219         int segcnt = read_ushort(info.cmap.ptr+6)/2;
220         if (segcnt == 0) return 0;
221         if (info.cmap.length < 16+segcnt*8) mixin(errorOp); // check room for arrays
222 
223         // offsets to arrays
224 
225         int endcopos = 14;
226         int startpos = 16+segcnt*2;
227         int deltapos = 16+segcnt*4;
228         int rangepos = 16+segcnt*6;     
229  
230         // binary search for first endcode > charCode
231 
232         int l = 0;
233         int r = segcnt-1;
234 
235         while (l != r)
236         {
237             int m = (l+r)/2;
238             int endcode = read_ushort(info.cmap.ptr+endcopos+m*2);
239             if (endcode < charCode) l = m+1; else r = m;
240         }
241 
242         // not found return missing glyph code
243 
244         uint start = read_ushort(info.cmap.ptr+startpos+r*2);
245         if (charCode < start) return 0;
246 
247         // calculte glyph index
248         
249         int idoff = read_ushort(info.cmap.ptr+rangepos+r*2);
250         
251         if (idoff == 0) return
252             (charCode + read_short(info.cmap.ptr+deltapos+r*2)) & 0xFFFF;
253         
254         // seriously who thought this obscure indexing trick was a good idea?
255         // glyphIndexAddress = idRangeOffset[i] + 2 * (c - startCode[i]) + (Ptr) &idRangeOffset[i]
256         // not sure if following is correct as not found font to test it
257 
258         int startcode = read_ushort(info.cmap.ptr+startpos+r*2);
259 
260         return read_ushort(info.cmap.ptr + idoff
261             + (charCode-startcode)*2 + rangepos + r*2);
262     }
263     else if (format == 6) // Trimmed table mapping
264     {
265         int first = read_ushort(info.cmap.ptr+6);
266         int count = read_ushort(info.cmap.ptr+8);
267         if ((charCode >= first) && (charCode < (first+count)))
268             return read_ushort(info.cmap.ptr+10+(charCode-first)*2);
269         return 0;
270     }
271     return 0;
272 }
273 
274 // outline flags
275 
276 private:
277 
278 enum
279 {
280     ON_CURVE_POINT = 1,
281     X_SHORT_VECTOR = 2,
282     Y_SHORT_VECTOR = 4,
283     REPEAT_FLAG    = 8,
284     X_SAME_OR_POS  = 16,
285     Y_SAME_OR_POS  = 32,
286     OVERLAP_SIMPLE = 64
287 }
288 
289 // glyph bounds struct
290 
291 public:
292 
293 struct ttGlyphBounds
294 {
295     int xmin,ymin,xmax,ymax;
296 }
297 
298 ttGlyphBounds ttGetGlyphBounds(ref ttFontInfo info, uint glyphId)
299 {
300     ttGlyphBounds bounds;
301     ubyte[] data = ttGetGlyphData(info,glyphId);
302     if (data.length == 0) return bounds;
303     bounds.xmin = read_short(data.ptr+2);
304     bounds.ymin = read_short(data.ptr+4);
305     bounds.xmax = read_short(data.ptr+6);
306     bounds.ymax = read_short(data.ptr+8);
307     return bounds;
308 }
309 
310 // get glyph data, returns empty array if index out of range, note that
311 // an empty array is valid anyway for an empty glyph, so empty doesnt
312 // nececarilly mean error
313 
314 ubyte[] ttGetGlyphData(ref ttFontInfo info, uint glyphId)
315 {
316     if (glyphId >= info.numGlyphs) return null;
317 
318     if (info.locaShortForm)
319     {
320         int start = read_ushort(info.loca.ptr+glyphId*2);
321         int end   = read_ushort(info.loca.ptr+glyphId*2+2);
322         return info.glyf[start*2..end*2];
323     }
324     else
325     {
326         int start = read_ushort(info.loca.ptr+glyphId*4);
327         int end   = read_ushort(info.loca.ptr+glyphId*4+4);
328         return info.glyf[start*4..end*4];
329     }
330 }
331 
332 // add glyph path, adds the glyph path to the another path,
333 // note: vertical orientation cartesian, rather that typical GUI of origin at top.
334 
335 int ttAddGlyphToPath(ref ttFontInfo info, uint glyphId, ref Path path,
336         float xoff, float yoff, float xscale, float yscale)
337 {
338     ubyte[] data = ttGetGlyphData(info, glyphId);
339 
340     // if empty nothing to do
341       
342     if (data.length == 0) return ttSuccess;
343 
344     // must at least have a header
345 
346     if (data.length < 10) mixin(errorOp);
347     int numctr = read_short(data.ptr);
348 
349     if (numctr > 0)
350     {
351         if (data.length < (10+numctr*2+2)) mixin(errorOp);
352         int numpts = read_ushort(data.ptr+10+numctr*2-2)+1;
353         int ilen = read_ushort(data.ptr+10+numctr*2);
354         if (data.length < (10+numctr*2+2+ilen)) mixin(errorOp);        
355         
356         // need to parse flags to locate x and y arrays
357         
358         int fpos = 10+numctr*2+2+ilen; 
359         int xlen,ylen,rep,flags;
360 
361         foreach(i; 0..numpts)
362         {
363             if (rep == 0)
364             {
365                 if (data.length < (fpos+2)) mixin(errorOp);        
366                 flags = data[fpos++];
367                 if (flags & REPEAT_FLAG) rep = data[fpos++];
368             }
369             else rep--;
370 
371             if (flags & X_SHORT_VECTOR) xlen++;
372             else if (!(flags & X_SAME_OR_POS)) xlen+=2;    
373             if (flags & Y_SHORT_VECTOR) ylen++;
374             else if (!(flags & Y_SAME_OR_POS)) ylen+=2;    
375         }
376 
377         if (data.length < (fpos+xlen+ylen)) mixin(errorOp);        
378 
379         // init stuff to itterate path
380 
381         int xpos = fpos;
382         int ypos = xpos+xlen;
383         fpos = 10+numctr*2+2+ilen;
384         int cpos = 10;
385 
386         struct Point
387         {
388             float x = 0, y = 0;
389             bool anchor; // true if anchor point, false if contorl point 
390         }
391 
392         int pidx;
393         Point p0,p1,firstpt;
394         flags = 0;
395         rep = 0;
396 
397         p1.x = xoff;
398         p1.y = yoff;
399 
400         // this iterates p0 and p1 along the path
401 
402         void nextPoint()
403         {
404             pidx++;
405             p0 = p1;
406 
407             if (rep == 0)
408             {
409                 flags = data[fpos++];
410                 if (flags & REPEAT_FLAG) rep = data[fpos++];
411             }
412             else rep--;
413 
414             p1.anchor = (flags & 1);
415 
416             if (flags & X_SHORT_VECTOR)
417             {
418                 p1.x += ((flags & X_SAME_OR_POS) 
419                     ? data[xpos++] : -cast(int) data[xpos++])*xscale;
420             }
421             else if (!(flags & X_SAME_OR_POS))
422             {
423                 p1.x += read_short(&data[xpos])*xscale;
424                 xpos += 2;
425             }
426             if (flags & Y_SHORT_VECTOR)
427             {
428                 p1.y += ((flags & Y_SAME_OR_POS)
429                     ? data[ypos++] : -cast(int) data[ypos++])*yscale;
430             }
431             else if (!(flags & Y_SAME_OR_POS))
432             {
433                 p1.y += read_short(&data[ypos])*yscale;
434                 ypos += 2;
435             }
436         }
437 
438         // parse path for real now 
439 
440         while (pidx < numpts)
441         {
442             // get end of sub path, make sure we have at least 3 points
443 
444             int endofsub = read_short(&data[cpos])+1;
445             cpos += 2;
446             if (endofsub > numpts) mixin(errorOp);
447             if ((endofsub-pidx) < 3) mixin(errorOp);
448 
449             // process first two points
450 
451             nextPoint();
452             nextPoint();
453             firstpt = p0; // save for later
454 
455             if (p0.anchor)
456             {
457                 path.moveTo(p0.x, p0.y);
458                 if (p1.anchor) path.lineTo(p1.x, p1.y);
459             }
460             else
461             {
462                 if (p1.anchor)
463                 {
464                     path.moveTo(p1.x, p1.y);
465                 }
466                 else
467                 {
468                     p0.x = (p0.x+p1.x)/2; // new achor point
469                     p0.y = (p0.y+p1.y)/2;
470                     path.moveTo(p0.x,p0.y);
471                 }
472             }
473 
474             // process the remaining path
475 
476             while(pidx < endofsub)
477             {
478                 nextPoint();
479 
480                 if (p0.anchor)
481                 {
482                     if (p1.anchor) path.lineTo(p1.x,p1.y);
483                 }
484                 else
485                 {
486                     if (p1.anchor)
487                     {
488                         path.quadTo(p0.x, p0.y, p1.x, p1.y);
489                     }
490                     else
491                     {
492                         float mx = (p0.x+p1.x)/2; // new achor point
493                         float my = (p0.y+p1.y)/2;
494                         path.quadTo(p0.x, p0.y, mx, my);
495                     }
496                 }
497             }
498 
499             // close the path, have to check first point and stick it onto the
500             // end if it was a control point
501 
502             auto end = path.lastMoveTo();
503 
504             if (firstpt.anchor)
505             {
506                 if (p1.anchor) path.lineTo(end.x, end.y);
507                 else path.quadTo(p1.x, p1.y, end.x, end.y);
508             }
509             else
510             {
511                 if (p1.anchor)
512                 {
513                     path.quadTo(firstpt.x, firstpt.y, end.x, end.y);
514                 }
515                 else // a new anchor point and a quad either side
516                 {
517                     float mx = (p0.x+p1.x)/2;
518                     float my = (p0.y+p1.y)/2;
519                     path.quadTo(p0.x, p0.y, mx, my);               
520                     path.quadTo(mx, my, end.x, end.y);               
521                 }
522             }
523         }
524     }
525 }
526 
527 // get advance width of glyph
528 
529 int ttGetAdvanceWidth(ref ttFontInfo info, uint glyphId)
530 {
531     assert(glyphId < info.numGlyphs);
532 
533     if (glyphId < info.numHMetrics)
534     {
535         return read_short(info.hmtx.ptr + glyphId*4);
536     }
537     else
538     {
539         return read_short(info.hmtx.ptr + info.numHMetrics*4-4);
540     }
541 }
542 
543 // get left side bearing of glyph
544 
545 int ttGetLeftSideBearing(ref ttFontInfo info, uint glyphId)
546 {
547     assert(glyphId < info.numGlyphs);
548 
549     if (glyphId < info.numHMetrics)
550     {
551         return read_short(info.hmtx.ptr + glyphId*4+2);
552     }
553     else
554     {
555         return read_short(info.hmtx.ptr + info.numHMetrics*4
556             + (glyphId-info.numHMetrics)*2);
557     }
558 }
559 
560 // get kerning info, advance with for glyph1 given that it is followed by glyph2
561 
562 int ttGetKerning(ref ttFontInfo info, uint glyphId1, uint glyphId2)
563 {
564     // check we have a subtable
565 
566     if (read_ushort(info.kern.ptr+2) < 1) mixin(errorOp);
567 
568     // use first table, must be horizontal
569 
570     if (!(read_ushort(info.kern.ptr+8) & 1))  mixin(errorOp);
571 
572     // binary search kerning pair
573 
574     int l = 0;
575     int r = read_ushort(info.kern.ptr+10) - 1;
576     uint lookfor = (glyphId2 << 16) | glyphId1;
577     while (l <= r)
578     {
579         int q = (l + r) / 2;
580         uint qval = read_uint(info.kern.ptr+18+q*6);
581         if (lookfor < qval) r = q-1;
582         else if (lookfor > qval) l = q+1;
583         else return read_short(info.kern.ptr+18+q*6+4);
584     }
585     return 0;
586 }
587 
588 int ttGetFontAscent(ref ttFontInfo info)
589 {
590     return read_short(info.hhea.ptr+4);
591 }
592 
593 int ttGetFontDescent(ref ttFontInfo info)
594 {
595     return read_short(info.hhea.ptr+6);
596 }
597 
598 int ttGetFontLineGap(ref ttFontInfo info)
599 {
600     return read_short(info.hhea.ptr+8);
601 }