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 import std.conv;
12 import std.math;
13 import std.algorithm.comparison;
14 import std.utf;
15 
16 import dplug.math.vector;
17 import dplug.math.box;
18 
19 import dplug.core.sync;
20 import dplug.core.nogc;
21 import dplug.core.map;
22 
23 import dplug.graphics.image;
24 import dplug.graphics.stb_truetype;
25 
26 final class Font
27 {
28 public:
29 nothrow:
30 @nogc:
31 
32     /// Loads a TTF file.
33     /// fontData should be the content of that file.
34     this(ubyte[] fontData)
35     {
36         _fontData = fontData;
37         if (0 == stbtt_InitFont(&_font, _fontData.ptr, stbtt_GetFontOffsetForIndex(_fontData.ptr, 0)))
38             assert(false, "Coudln't load font");
39 
40         stbtt_GetFontVMetrics(&_font, &_fontAscent, &_fontDescent, &_fontLineGap);
41 
42         _mutex = makeMutex();
43 
44         _glyphCache.initialize(&_font);
45     }
46 
47     ~this()
48     {
49         stbtt_FreeFont(&_font);
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(const(char)[] 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(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(const(char)[] 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             const float fxpos = floorf(xpos);
138             const float fypos = floorf(ypos);
139 
140             int ixpos = cast(int) fxpos;
141             int iypos = cast(int) fypos;
142 
143             float xShift = xpos - fxpos;
144             float yShift = ypos - fypos;
145 
146             // Round position sub-pixel to 1/4th of pixels, to make more use of the glyph cache.
147             // That means for a codepoint at a particular size, up to 16 different glyph can potentially
148             // exist in the cache.
149             float roundDivide = 8.0f;
150             xShift = cast(int)(round(roundDivide * xShift)) / roundDivide;
151             yShift = cast(int)(round(roundDivide * yShift)) / roundDivide;
152 
153             stbtt_GetCodepointHMetrics(&_font, ch, &advance, &lsb);
154             stbtt_GetCodepointBitmapBoxSubpixel(&_font, ch, scale, scale, xShift, yShift, &x0, &y0, &x1, &y1);
155             box2i position = box2i(x0 + ixpos, y0 + iypos, x1 + ixpos, y1 + iypos);
156             doSomethingWithPosition(numCh, ch, position, scale, xShift, yShift);
157             xpos += (advance * scale);
158 
159             // add a user-provided constant letter spacing
160             xpos += (letterSpacingPx);
161 
162             lastCh = ch;
163             numCh++;
164         }
165     }
166 
167     GlyphCache _glyphCache;
168 
169     UncheckedMutex _mutex;
170 
171     ImageRef!L8 getGlyphCoverage(dchar codepoint, float scale, int w, int h, float xShift, float yShift) nothrow @nogc
172     {
173         GlyphKey key = GlyphKey(codepoint, scale, xShift, yShift);
174 
175         return _glyphCache.requestGlyph(key, w, h);
176     }
177 }
178 
179 
180 enum HorizontalAlignment
181 {
182     left,    // positionX in surface corresponds to the leftmost point of the first character 
183              //   (a bit incorrect, since some chars such as O could go lefter than that)
184     center   // positionX in surface corresponds to the center of the horizontal extent of the text
185 }
186 
187 enum VerticalAlignment
188 {
189     baseline, // positionY in surface corresponds to baseline coordinate in surface
190     center    // positionY in surface corresponds to the center of the vertical extent of the text
191 }
192 
193 /// Draw text centered on a point on a DirectView.
194 void fillText(ImageRef!RGBA surface, Font font, const(char)[] s, float fontSizePx, float letterSpacingPx,
195               RGBA textColor, float positionX, float positionY,
196               HorizontalAlignment horzAlign = HorizontalAlignment.center,
197               VerticalAlignment   vertAlign = VerticalAlignment.center) nothrow @nogc
198 {
199     font._mutex.lock();
200     scope(exit) font._mutex.unlock();
201 
202     // Decompose in fractional and integer position
203     int ipositionx = cast(int)floorf(positionX);
204     int ipositiony = cast(int)floorf(positionY);
205     float fractionalPosX = positionX - ipositionx;
206     float fractionalPosY = positionY - ipositiony;
207 
208     box2i area = font.measureText(s, fontSizePx, letterSpacingPx);
209 
210     // For clipping outside characters
211     box2i surfaceArea = box2i(0, 0, surface.w, surface.h);
212 
213     vec2i offset = vec2i(ipositionx, ipositiony);
214     
215     if (horzAlign == HorizontalAlignment.center)
216         offset.x -= area.center.x;
217 
218     if (vertAlign == VerticalAlignment.center)
219         offset.y -= area.center.y;
220 
221     void drawCharacter(int numCh, dchar ch, box2i position, float scale, float xShift, float yShift) nothrow @nogc
222     {
223         vec2i offsetPos = position.min + offset;
224 
225         // make room in temp buffer
226         int w = position.width;
227         int h = position.height;
228 
229         ImageRef!L8 coverageBuffer = font.getGlyphCoverage(ch, scale, w, h, xShift, yShift);
230 
231         // follows the cropping limitations of crop()
232         int cropX0 = clamp!int(offsetPos.x, 0, surface.w);
233         int cropY0 = clamp!int(offsetPos.y, 0, surface.h);
234         int cropX1 = clamp!int(offsetPos.x + w, 0, surface.w);
235         int cropY1 = clamp!int(offsetPos.y + h, 0, surface.h);
236         box2i where = box2i(cropX0, cropY0, cropX1, cropY1);
237  
238         // Note: it is possible for where to be empty here.
239 
240         // Early exit if out of scope
241         // For example the whole charater might be out of surface
242         if (!surfaceArea.intersects(where))
243             return;
244 
245         // The area where the glyph (part of it at least) is drawn.
246         auto outsurf = surface.cropImageRef(where);
247 
248         int croppedWidth = outsurf.w;
249 
250         RGBA fontColor = textColor;
251 
252         // Need to crop the coverage surface like the output surface.
253         // Get the margins introduced.
254         // This fixed garbled rendering (Issue #642).
255         int covx = cropX0 - offsetPos.x;
256         int covy = cropY0 - offsetPos.y;
257         int covw = cropX1 - cropX0;
258         int covh = cropY1 - cropY0;
259         assert(covw > 0 && covh > 0); // else would have exited above
260 
261         coverageBuffer = coverageBuffer.cropImageRef(covx, covy, covx + covw, covy + covh);
262 
263         assert(outsurf.w == coverageBuffer.w);
264         assert(outsurf.h == coverageBuffer.h);
265 
266         for (int y = 0; y < outsurf.h; ++y)
267         {
268             RGBA[] outscan = outsurf.scanline(y);
269 
270             L8[] inscan = coverageBuffer.scanline(y);
271             for (int x = 0; x < croppedWidth; ++x)
272             {
273                 blendFontPixel(outscan.ptr[x], fontColor, inscan.ptr[x].l);
274             }
275         }
276     }
277     font.iterateCharacterPositions(s, fontSizePx, letterSpacingPx, fractionalPosX, fractionalPosY, &drawCharacter);
278 }
279 
280 // PERF: perhaps this can be replaced by blendColor, but beware of alpha
281 // this can be breaking
282 private void blendFontPixel(ref RGBA bg, RGBA fontColor, int alpha) nothrow @nogc
283 {
284 
285     int alpha2 = 255 - alpha;
286     int red =   (bg.r * alpha2 + fontColor.r * alpha + 128) >> 8;
287     int green = (bg.g * alpha2 + fontColor.g * alpha + 128) >> 8;
288     int blue =  (bg.b * alpha2 + fontColor.b * alpha + 128) >> 8;
289 
290     bg = RGBA(cast(ubyte)red, cast(ubyte)green, cast(ubyte)blue, fontColor.a);
291 }
292 
293 
294 private struct GlyphKey
295 {
296     dchar codepoint;
297     float scale;
298     float xShift;
299     float yShift;
300 
301     // PERF: that sounds a bit expensive, could be replaced by a hash I guess
302     int opCmp(const(GlyphKey) other) const nothrow @nogc
303     {
304         // Basically: group by scale then by yShift (likely yo be the same line)
305         //            then codepoint then xShift
306         if (scale < other.scale)
307             return -1;
308         else if (scale > other.scale)
309             return 1;
310         else
311         {
312             if (yShift < other.yShift)
313                 return -1;
314             else if (yShift > other.yShift)
315                 return 1;
316             else
317             {
318                 if (codepoint < other.codepoint)
319                     return -1;
320                 else if (codepoint > other.codepoint)
321                     return 1;
322                 else
323                 {
324                     if (xShift < other.xShift)
325                         return -1;
326                     else if (xShift > other.xShift)
327                         return 1;
328                     else
329                         return 0;
330                 }
331             }
332         }
333     }
334 }
335 
336 static assert(GlyphKey.sizeof == 16);
337 
338 private struct GlyphCache
339 {
340 public:
341 nothrow:
342 @nogc:
343     void initialize(stbtt_fontinfo* font)
344     {
345         _font = font;
346         _glyphs = makeMap!(GlyphKey, ubyte*); // TODO: Inspector says this map is leaking, investigate why
347     }
348 
349     @disable this(this);
350 
351     ~this()
352     {
353         // Free all glyphs
354         foreach(g; _glyphs.byValue)
355         {
356             free(g);
357         }
358     }
359 
360     ImageRef!L8 requestGlyph(GlyphKey key, int w, int h)
361     {
362         ubyte** p = key in _glyphs;
363 
364         if (p !is null)
365         {
366             // Found glyph in cache, return this glyph
367             ImageRef!L8 result;
368             result.w = w;
369             result.h = h;
370             result.pitch = w;
371             result.pixels = cast(L8*)(*p);
372             return result;
373         }
374 
375         //  Not existing, creates the glyph and add them to the cache
376         {
377             int stride = w;
378             ubyte* buf = cast(ubyte*) malloc(w * h);
379             stbtt_MakeCodepointBitmapSubpixel(_font, buf, w, h, stride, key.scale, key.scale, key.xShift, key.yShift, key.codepoint);
380             _glyphs[key] = buf;
381             
382             ImageRef!L8 result;
383             result.w = w;
384             result.h = h;
385             result.pitch = w;
386             result.pixels = cast(L8*)buf;
387             return result;
388         }
389     }
390 private:
391     Map!(GlyphKey, ubyte*) _glyphs;
392     stbtt_fontinfo* _font;
393 }