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 }