1 /**
2 Font high-level interface.
3 
4 Copyright: Guillaume Piolat 2015.
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 */
7 module dplug.graphics.font;
8 
9 import core.stdc.math: floorf;
10 import core.stdc.stdlib;
11 
12 import std.math;
13 import std.utf: byDchar;
14 
15 import dplug.math.vector;
16 import dplug.math.box;
17 
18 import dplug.core.sync;
19 import dplug.core.nogc;
20 import dplug.core.map;
21 
22 import dplug.graphics.image;
23 import dplug.graphics.stb_truetype;
24 
25 final class Font
26 {
27 public:
28 nothrow:
29 @nogc:
30 
31     /// Loads a TTF file.
32     /// fontData should be the content of that file.
33     this(ubyte[] fontData)
34     {
35         _fontData = fontData;
36         if (0 == stbtt_InitFont(&_font, _fontData.ptr, stbtt_GetFontOffsetForIndex(_fontData.ptr, 0)))
37             assert(false, "Coudln't load font");
38 
39         stbtt_GetFontVMetrics(&_font, &_fontAscent, &_fontDescent, &_fontLineGap);
40 
41         _mutex = makeMutex();
42 
43         _glyphCache.initialize(&_font);
44     }
45 
46     ~this()
47     {
48         stbtt_FreeFont(&_font);
49     }
50 
51     /// Returns: font ascent in pixels (aka the size of "A").
52     float getAscent(float fontSizePx)
53     {
54         float scale = stbtt_ScaleForPixelHeight(&_font, fontSizePx);
55         return scale * _fontAscent;
56     }
57 
58     /// Returns: font descent in pixels (aka the size of "A").
59     /// `ascent - descent` gives the extent of characters.
60     float getDescent(float fontSizePx)
61     {
62         float scale = stbtt_ScaleForPixelHeight(&_font, fontSizePx);
63         return scale * _fontDescent;
64     }
65 
66     /// Returns: size of little 'x' in pixels. Useful for vertical alignment.
67     float getHeightOfx(float fontSizePx)
68     {
69         float scale = stbtt_ScaleForPixelHeight(&_font, fontSizePx);
70         int xIndex = stbtt_FindGlyphIndex(&_font, 'x'); // TODO optimize this function
71         if (xIndex == 0)
72             return getAscent(fontSizePx); // No 'x', return a likely height
73 
74         int x0, y0, x1, y1;
75         if (stbtt_GetGlyphBox(&_font, xIndex, &x0, &y0, &x1, &y1))
76         {
77             return scale * y1;
78         }
79         else
80             return getAscent(fontSizePx); // No 'x', return a likely height        
81     }
82 
83 
84     /// Returns: Where a line of text will be drawn if starting at position (0, 0).
85     /// Note: aligning vertically with this information is dangerous, since different characters
86     ///       may affect vertical extent differently. Prefer the use of `getHeightOfx()`.
87     box2i measureText(const(char)[] s, float fontSizePx, float letterSpacingPx) nothrow @nogc
88     {
89         box2i area;
90         void extendArea(int numCh, dchar ch, box2i position, float scale, float xShift, float yShift) nothrow @nogc
91         {
92             if (numCh == 0)
93                 area = position;
94             else
95                 area = area.expand(position.min).expand(position.max);
96         }
97 
98         // Note: when measuring the size of the text, we do not account for sub-pixel shifts
99         // this is because it would make the size of the text vary which does movement jitter
100         // for moving text
101         iterateCharacterPositions(s, fontSizePx, letterSpacingPx, 0, 0, &extendArea);
102         return area;
103     }
104 
105 private:
106 
107     stbtt_fontinfo _font;
108     const(ubyte)[] _fontData;
109     int _fontAscent, _fontDescent, _fontLineGap;
110 
111     /// Iterates on character and call the delegate with their subpixel position
112     /// Only support one line of text.
113     /// Use kerning.
114     /// No hinting.
115     void iterateCharacterPositions(const(char)[] text, float fontSizePx, float letterSpacingPx, float fractionalPosX, float fractionalPosY,
116         scope void delegate(int numCh, dchar ch, box2i position, float scale, float xShift, float yShift) nothrow @nogc doSomethingWithPosition) nothrow @nogc
117     {
118         assert(0 <= fractionalPosX && fractionalPosX <= 1.0f);
119         assert(0 <= fractionalPosY && fractionalPosY <= 1.0f);
120         float scale = stbtt_ScaleForPixelHeight(&_font, fontSizePx);
121         float xpos = fractionalPosX;
122         float ypos = fractionalPosY;
123 
124         float lastxpos = 0;
125         dchar lastCh;
126         int maxHeight = 0;
127         box2i area;
128         int numCh = 0;
129         foreach(dchar ch; text.byDchar)
130         {
131             if (numCh > 0)
132                 xpos += scale * stbtt_GetCodepointKernAdvance(&_font, lastCh, ch);
133 
134             int advance,lsb,x0,y0,x1,y1;
135 
136             const float fxpos = floorf(xpos);
137             const float fypos = floorf(ypos);
138 
139             int ixpos = cast(int) fxpos;
140             int iypos = cast(int) fypos;
141 
142             float xShift = xpos - fxpos;
143             float yShift = ypos - fypos;
144 
145             // Round position sub-pixel to 1/4th of pixels, to make more use of the glyph cache.
146             // That means for a codepoint at a particular size, up to 16 different glyph can potentially
147             // exist in the cache.
148             float roundDivide = 8.0f;
149             xShift = cast(int)(round(roundDivide * xShift)) / roundDivide;
150             yShift = cast(int)(round(roundDivide * yShift)) / roundDivide;
151 
152             stbtt_GetCodepointHMetrics(&_font, ch, &advance, &lsb);
153             stbtt_GetCodepointBitmapBoxSubpixel(&_font, ch, scale, scale, xShift, yShift, &x0, &y0, &x1, &y1);
154             box2i position = box2i(x0 + ixpos, y0 + iypos, x1 + ixpos, y1 + iypos);
155             doSomethingWithPosition(numCh, ch, position, scale, xShift, yShift);
156             xpos += (advance * scale);
157 
158             // add a user-provided constant letter spacing
159             xpos += (letterSpacingPx);
160 
161             lastCh = ch;
162             numCh++;
163         }
164     }
165 
166     GlyphCache _glyphCache;
167 
168     UncheckedMutex _mutex;
169 
170     ImageRef!L8 getGlyphCoverage(dchar codepoint, float scale, int w, int h, float xShift, float yShift) nothrow @nogc
171     {
172         GlyphKey key = GlyphKey(codepoint, scale, xShift, yShift);
173 
174         return _glyphCache.requestGlyph(key, w, h);
175     }
176 }
177 
178 
179 enum HorizontalAlignment
180 {
181     left,    // positionX in surface corresponds to the leftmost point of the first character 
182              //   (a bit incorrect, since some chars such as O could go lefter than that)
183     center   // positionX in surface corresponds to the center of the horizontal extent of the text
184 }
185 
186 enum VerticalAlignment
187 {
188     baseline, // positionY in surface corresponds to baseline coordinate in surface
189     center    // positionY in surface corresponds to the center of the vertical extent of the text
190 }
191 
192 /// Draw text centered on a point on a DirectView.
193 void fillText(ImageRef!RGBA surface, Font font, const(char)[] s, float fontSizePx, float letterSpacingPx,
194               RGBA textColor, float positionX, float positionY,
195               HorizontalAlignment horzAlign = HorizontalAlignment.center,
196               VerticalAlignment   vertAlign = VerticalAlignment.center) nothrow @nogc
197 {
198     font._mutex.lock();
199     scope(exit) font._mutex.unlock();
200 
201     // Decompose in fractional and integer position
202     int ipositionx = cast(int)floorf(positionX);
203     int ipositiony = cast(int)floorf(positionY);
204     float fractionalPosX = positionX - ipositionx;
205     float fractionalPosY = positionY - ipositiony;
206 
207     box2i area = font.measureText(s, fontSizePx, letterSpacingPx);
208 
209     // For clipping outside characters
210     box2i surfaceArea = box2i(0, 0, surface.w, surface.h);
211 
212     vec2i offset = vec2i(ipositionx, ipositiony);
213     
214     if (horzAlign == HorizontalAlignment.center)
215         offset.x -= area.center.x;
216 
217     if (vertAlign == VerticalAlignment.center)
218         offset.y -= area.center.y;
219 
220     void drawCharacter(int numCh, dchar ch, box2i position, float scale, float xShift, float yShift) nothrow @nogc
221     {
222         vec2i offsetPos = position.min + offset;
223 
224         // make room in temp buffer
225         int w = position.width;
226         int h = position.height;
227 
228         ImageRef!L8 coverageBuffer = font.getGlyphCoverage(ch, scale, w, h, xShift, yShift);
229 
230         static int clamp_int(int x, int m, int M)
231         {
232             if (x < m) x = m;
233             if (x > M) x = M;
234             return x;
235         }
236 
237         // follows the cropping limitations of crop()
238         int cropX0 = clamp_int(offsetPos.x, 0, surface.w);
239         int cropY0 = clamp_int(offsetPos.y, 0, surface.h);
240         int cropX1 = clamp_int(offsetPos.x + w, 0, surface.w);
241         int cropY1 = clamp_int(offsetPos.y + h, 0, surface.h);
242         box2i where = box2i(cropX0, cropY0, cropX1, cropY1);
243  
244         // Note: it is possible for where to be empty here.
245 
246         // Early exit if out of scope
247         // For example the whole charater might be out of surface
248         if (!surfaceArea.intersects(where))
249             return;
250 
251         // The area where the glyph (part of it at least) is drawn.
252         auto outsurf = surface.cropImageRef(where);
253 
254         int croppedWidth = outsurf.w;
255 
256         RGBA fontColor = textColor;
257 
258         // Need to crop the coverage surface like the output surface.
259         // Get the margins introduced.
260         // This fixed garbled rendering (Issue #642).
261         int covx = cropX0 - offsetPos.x;
262         int covy = cropY0 - offsetPos.y;
263         int covw = cropX1 - cropX0;
264         int covh = cropY1 - cropY0;
265         assert(covw > 0 && covh > 0); // else would have exited above
266 
267         coverageBuffer = coverageBuffer.cropImageRef(covx, covy, covx + covw, covy + covh);
268 
269         assert(outsurf.w == coverageBuffer.w);
270         assert(outsurf.h == coverageBuffer.h);
271 
272         for (int y = 0; y < outsurf.h; ++y)
273         {
274             RGBA[] outscan = outsurf.scanline(y);
275 
276             L8[] inscan = coverageBuffer.scanline(y);
277             for (int x = 0; x < croppedWidth; ++x)
278             {
279                 blendFontPixel(outscan.ptr[x], fontColor, inscan.ptr[x].l);
280             }
281         }
282     }
283     font.iterateCharacterPositions(s, fontSizePx, letterSpacingPx, fractionalPosX, fractionalPosY, &drawCharacter);
284 }
285 
286 // PERF: perhaps this can be replaced by blendColor, but beware of alpha
287 // this can be breaking
288 private void blendFontPixel(ref RGBA bg, RGBA fontColor, int alpha) nothrow @nogc
289 {
290 
291     int alpha2 = 255 - alpha;
292     int red =   (bg.r * alpha2 + fontColor.r * alpha + 128) >> 8;
293     int green = (bg.g * alpha2 + fontColor.g * alpha + 128) >> 8;
294     int blue =  (bg.b * alpha2 + fontColor.b * alpha + 128) >> 8;
295 
296     bg = RGBA(cast(ubyte)red, cast(ubyte)green, cast(ubyte)blue, fontColor.a);
297 }
298 
299 
300 private struct GlyphKey
301 {
302     dchar codepoint;
303     float scale;
304     float xShift;
305     float yShift;
306 
307     // PERF: that sounds a bit expensive, could be replaced by a hash I guess
308     int opCmp(const(GlyphKey) other) const nothrow @nogc
309     {
310         // Basically: group by scale then by yShift (likely yo be the same line)
311         //            then codepoint then xShift
312         if (scale < other.scale)
313             return -1;
314         else if (scale > other.scale)
315             return 1;
316         else
317         {
318             if (yShift < other.yShift)
319                 return -1;
320             else if (yShift > other.yShift)
321                 return 1;
322             else
323             {
324                 if (codepoint < other.codepoint)
325                     return -1;
326                 else if (codepoint > other.codepoint)
327                     return 1;
328                 else
329                 {
330                     if (xShift < other.xShift)
331                         return -1;
332                     else if (xShift > other.xShift)
333                         return 1;
334                     else
335                         return 0;
336                 }
337             }
338         }
339     }
340 }
341 
342 static assert(GlyphKey.sizeof == 16);
343 
344 private struct GlyphCache
345 {
346 public:
347 nothrow:
348 @nogc:
349     void initialize(stbtt_fontinfo* font)
350     {
351         _font = font;
352         _glyphs = makeMap!(GlyphKey, ubyte*); // TODO: Inspector says this map is leaking, investigate why
353     }
354 
355     @disable this(this);
356 
357     ~this()
358     {
359         // Free all glyphs
360         foreach(g; _glyphs.byValue)
361         {
362             free(g);
363         }
364     }
365 
366     ImageRef!L8 requestGlyph(GlyphKey key, int w, int h)
367     {
368         ubyte** p = key in _glyphs;
369 
370         if (p !is null)
371         {
372             // Found glyph in cache, return this glyph
373             ImageRef!L8 result;
374             result.w = w;
375             result.h = h;
376             result.pitch = w;
377             result.pixels = cast(L8*)(*p);
378             return result;
379         }
380 
381         //  Not existing, creates the glyph and add them to the cache
382         {
383             int stride = w;
384             ubyte* buf = cast(ubyte*) malloc(w * h);
385             stbtt_MakeCodepointBitmapSubpixel(_font, buf, w, h, stride, key.scale, key.scale, key.xShift, key.yShift, key.codepoint);
386             _glyphs[key] = buf;
387             
388             ImageRef!L8 result;
389             result.w = w;
390             result.h = h;
391             result.pitch = w;
392             result.pixels = cast(L8*)buf;
393             return result;
394         }
395     }
396 private:
397     Map!(GlyphKey, ubyte*) _glyphs;
398     stbtt_fontinfo* _font;
399 }