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 }