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 }