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