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 }