1 /**
2 * Copyright: Copyright Auburn Sounds 2015 and later.
3 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
4 * Authors:   Guillaume Piolat
5 */
6 module dplug.graphics.font;
7 
8 import core.stdc.stdlib;
9 import std.conv;
10 import std.math;
11 import std.algorithm.comparison;
12 import std.utf;
13 
14 import gfm.math.vector;
15 import gfm.math.box;
16 
17 import dplug.core.alignedbuffer;
18 import dplug.core.sync;
19 import dplug.core.nogc;
20 
21 import dplug.graphics.view;
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     }
49 
50     /// Returns: Where a line of text will be drawn if starting at position (0, 0).
51     box2i measureText(StringType)(StringType s, float fontSizePx, float letterSpacingPx) nothrow @nogc
52     {
53         box2i area;
54         void extendArea(int numCh, dchar ch, box2i position, float scale, float xShift, float yShift) nothrow @nogc
55         {
56             if (numCh == 0)
57                 area = position;
58             else
59                 area = area.expand(position.min).expand(position.max);
60         }
61 
62         // Note: when measuring the size of the text, we do not account for sub-pixel shifts
63         // this is because it would make the size of the text vary which does movement jitter
64         // for moving text
65         iterateCharacterPositions!StringType(s, fontSizePx, letterSpacingPx, 0, 0, &extendArea);
66         return area;
67     }
68 
69 private:
70 
71     stbtt_fontinfo _font;
72     const(ubyte)[] _fontData;
73     int _fontAscent, _fontDescent, _fontLineGap;
74 
75     /// Iterates on character and call the delegate with their subpixel position
76     /// Only support one line of text.
77     /// Use kerning.
78     /// No hinting.
79     void iterateCharacterPositions(StringType)(StringType text, float fontSizePx, float letterSpacingPx, float fractionalPosX, float fractionalPosY,
80         scope void delegate(int numCh, dchar ch, box2i position, float scale, float xShift, float yShift) nothrow @nogc doSomethingWithPosition) nothrow @nogc
81     {
82         assert(0 <= fractionalPosX && fractionalPosX <= 1.0f);
83         assert(0 <= fractionalPosY && fractionalPosY <= 1.0f);
84         float scale = stbtt_ScaleForPixelHeight(&_font, fontSizePx);
85         float xpos = fractionalPosX;
86         float ypos = fractionalPosY;
87 
88         float lastxpos = 0;
89         dchar lastCh;
90         int maxHeight = 0;
91         box2i area;
92         int numCh = 0;
93         foreach(dchar ch; text.byDchar)
94         {
95             if (numCh > 0)
96                 xpos += scale * stbtt_GetCodepointKernAdvance(&_font, lastCh, ch);
97 
98             int advance,lsb,x0,y0,x1,y1;
99 
100             int ixpos = cast(int) floor(xpos);
101             int iypos = cast(int) floor(ypos);
102 
103 
104             float xShift = xpos - floor(xpos);
105             float yShift = ypos - floor(ypos);
106 
107             // Round position sub-pixel to 1/4th of pixels, to make more use of the glyph cache.
108             // That means for a codepoint at a particular size, up to 16 different glyph can potentially
109             // exist in the cache.
110             float roundDivide = 8.0f;
111             xShift = cast(int)(round(roundDivide * xShift)) / roundDivide;
112             yShift = cast(int)(round(roundDivide * yShift)) / roundDivide;
113 
114             stbtt_GetCodepointHMetrics(&_font, ch, &advance, &lsb);
115             stbtt_GetCodepointBitmapBoxSubpixel(&_font, ch, scale, scale, xShift, yShift, &x0, &y0, &x1, &y1);
116             box2i position = box2i(x0 + ixpos, y0 + iypos, x1 + ixpos, y1 + iypos);
117             doSomethingWithPosition(numCh, ch, position, scale, xShift, yShift);
118             xpos += (advance * scale);
119 
120             // add a user-provided constant letter spacing
121             xpos += (letterSpacingPx);
122 
123             lastCh = ch;
124             numCh++;
125         }
126     }
127 
128     GlyphCache _glyphCache;
129 
130     UncheckedMutex _mutex;
131 
132     ImageRef!L8 getGlyphCoverage(dchar codepoint, float scale, int w, int h, float xShift, float yShift) nothrow @nogc
133     {
134         GlyphKey key = GlyphKey(codepoint, scale, xShift, yShift);
135 
136         return _glyphCache.requestGlyph(key, w, h);
137     }
138 }
139 
140 /// Draw text centered on a point on a DirectView.
141 
142 void fillText(V, StringType)(auto ref V surface, Font font, StringType s, float fontSizePx, float letterSpacingPx,
143                              RGBA textColor, float positionx, float positiony) nothrow @nogc
144     if (isWritableView!V && is(ViewColor!V == RGBA))
145 {
146     font._mutex.lock();
147     scope(exit) font._mutex.unlock();
148 
149     // Decompose in fractional and integer position
150     int ipositionx = cast(int)floor(positionx);
151     int ipositiony = cast(int)floor(positiony);
152     float fractionalPosX = positionx - ipositionx;
153     float fractionalPosY = positiony - ipositiony;
154 
155     box2i area = font.measureText(s, fontSizePx, letterSpacingPx);
156 
157     // Early exit if out of scope
158     box2i surfaceArea = box2i(0, 0, surface.w, surface.h);
159     if (!surfaceArea.intersects(area))
160         return;
161 
162     vec2i offset = vec2i(ipositionx, ipositiony) - area.center; // MAYDO: support other alignment modes
163 
164     void drawCharacter(int numCh, dchar ch, box2i position, float scale, float xShift, float yShift) nothrow @nogc
165     {
166         vec2i offsetPos = position.min + offset;
167 
168         // make room in temp buffer
169         int w = position.width;
170         int h = position.height;
171 
172         ImageRef!L8 coverageBuffer = font.getGlyphCoverage(ch, scale, w, h, xShift, yShift);
173 
174         // follows the cropping limitations of crop()
175         int cropX0 = clamp!int(offsetPos.x, 0, surface.w);
176         int cropY0 = clamp!int(offsetPos.y, 0, surface.h);
177         int cropX1 = clamp!int(offsetPos.x + w, 0, surface.w);
178         int cropY1 = clamp!int(offsetPos.y + h, 0, surface.h);
179         auto outsurf = surface.crop(cropX0, cropY0, cropX1, cropY1);
180 
181         int croppedWidth = outsurf.w;
182 
183         RGBA fontColor = textColor;
184 
185         for (int y = 0; y < outsurf.h; ++y)
186         {
187             static if (isDirectView!V)
188                 RGBA[] outscan = outsurf.scanline(y);
189 
190             L8[] inscan = coverageBuffer.scanline(y);
191             for (int x = 0; x < croppedWidth; ++x)
192             {
193                 static if (isDirectView!V)
194                 {
195                     blendFontPixel(outscan.ptr[x], fontColor, inscan.ptr[x].l);
196                 }
197                 else
198                 {
199                     blendFontPixel(outscan.ptr[x], fontColor, inscan.ptr[x].l);
200                 }
201             }
202         }
203     }
204     font.iterateCharacterPositions!StringType(s, fontSizePx, letterSpacingPx, fractionalPosX, fractionalPosY, &drawCharacter);
205 }
206 
207 
208 private void blendFontPixel(ref RGBA bg, RGBA fontColor, int alpha) nothrow @nogc
209 {
210 
211     int alpha2 = 255 - alpha;
212     int red =   (bg.r * alpha2 + fontColor.r * alpha + 128) >> 8;
213     int green = (bg.g * alpha2 + fontColor.g * alpha + 128) >> 8;
214     int blue =  (bg.b * alpha2 + fontColor.b * alpha + 128) >> 8;
215 
216     bg = RGBA(cast(ubyte)red, cast(ubyte)green, cast(ubyte)blue, fontColor.a);
217 }
218 
219 
220 private struct GlyphKey
221 {
222     dchar codepoint;
223     float scale;
224     float xShift;
225     float yShift;
226 }
227 
228 static assert(GlyphKey.sizeof == 16);
229 
230 private struct GlyphCache
231 {
232 public:
233 nothrow:
234 @nogc:
235     void initialize(stbtt_fontinfo* font)
236     {
237         _font = font;
238         keys = makeAlignedBuffer!GlyphKey;
239         glyphs = makeAlignedBuffer!(ubyte*);
240     }
241 
242     @disable this(this);
243 
244     ~this()
245     {
246         // Free all glyphs
247         foreach(g; glyphs)
248         {
249             free(g);
250         }
251     }
252 
253     ImageRef!L8 requestGlyph(GlyphKey key, int w, int h)
254     {
255         // TODO
256         // Just a linear search for now. Obviously this
257         // will be a problem for larger text
258         assert(keys.length == glyphs.length);
259 
260         for(int i = 0; i < glyphs.length; ++i)
261         {
262             GlyphKey k = keys[i];
263             if (k.codepoint == key.codepoint
264                 && k.scale == key.scale
265                 && k.xShift == key.xShift
266                 && k.yShift == key.yShift)
267             {
268                 // Found, return this glyph
269                 ImageRef!L8 result;
270                 result.w = w;
271                 result.h = h;
272                 result.pitch = w;
273                 result.pixels = cast(L8*)(glyphs[i]);
274                 return result;
275             }
276         }
277 
278         //  Not existing, creates the glyph and add them to the cache
279         {
280             int stride = w;
281             ubyte* buf = cast(ubyte*) malloc(w * h);
282             keys.pushBack(key);
283             glyphs.pushBack(buf);
284 
285             stbtt_MakeCodepointBitmapSubpixel(_font, buf, w, h, stride, key.scale, key.scale, key.xShift, key.yShift, key.codepoint);
286             
287             ImageRef!L8 result;
288             result.w = w;
289             result.h = h;
290             result.pitch = w;
291             result.pixels = cast(L8*)buf;
292             return result;
293         }
294     }
295 private:
296     AlignedBuffer!GlyphKey keys;
297     AlignedBuffer!(ubyte*) glyphs;
298     stbtt_fontinfo* _font;
299 }