scimesh 0.3.4
Headless CPU-only 3D software renderer for scientific mesh visualization
Loading...
Searching...
No Matches
font.cpp
Go to the documentation of this file.
1
7
8#include <scimesh/font.h>
9
10#include <algorithm>
11#include <cmath>
12#include <cstdlib>
13#include <fstream>
14#include <map>
15#include <mutex>
16#include <stdexcept>
17
18// stb_truetype is used with its "static" mode so that no stbtt_* symbol leaks
19// out of this translation unit, mirroring how image.cpp handles stb_image.
20// Asserts are turned into no-ops: dynamic libraries must not abort the host
21// process on malformed input (the same reasoning as STBI_ASSERT in image.cpp).
22#ifdef SCIMESH_STB_TRUETYPE_IMPL
23#ifndef STBTT_STATIC
24#define STBTT_STATIC
25#endif
26#ifndef STB_TRUETYPE_IMPLEMENTATION
27#define STB_TRUETYPE_IMPLEMENTATION
28#endif
29#endif
30#define STBTT_assert(x) ((void)0)
31#include "stb_truetype.h"
32
33namespace scimesh {
34
35namespace {
36
38bool file_readable(const std::string &path) {
39 if (path.empty()) {
40 return false;
41 }
42 std::ifstream in(path.c_str(), std::ios::binary);
43 return in.good();
44}
45
48std::vector<uint8_t> read_file_bytes(const std::string &path) {
49 std::ifstream in(path.c_str(), std::ios::binary);
50 if (!in.good()) {
51 throw std::runtime_error("scimesh: cannot open font file '" + path + "'");
52 }
53 in.seekg(0, std::ios::end);
54 const std::streamoff size = in.tellg();
55 if (size <= 0) {
56 throw std::runtime_error("scimesh: font file '" + path + "' is empty");
57 }
58 in.seekg(0, std::ios::beg);
59 std::vector<uint8_t> data(static_cast<size_t>(size));
60 in.read(reinterpret_cast<char *>(data.data()), size);
61 if (!in) {
62 throw std::runtime_error("scimesh: failed to read font file '" + path + "'");
63 }
64 return data;
65}
66
72std::string utf16be_to_utf8(const unsigned char *bytes, int length) {
73 std::string out;
74 for (int i = 0; i + 1 < length; i += 2) {
75 uint32_t code_unit =
76 static_cast<uint32_t>(bytes[i] << 8) | static_cast<uint32_t>(bytes[i + 1]);
77 uint32_t cp = code_unit;
78 if (code_unit >= 0xD800 && code_unit <= 0xDBFF && i + 3 < length) {
79 uint32_t low = static_cast<uint32_t>(bytes[i + 2] << 8) |
80 static_cast<uint32_t>(bytes[i + 3]);
81 if (low >= 0xDC00 && low <= 0xDFFF) {
82 cp = 0x10000 + ((code_unit - 0xD800) << 10) + (low - 0xDC00);
83 i += 2;
84 } else {
85 cp = 0xFFFD;
86 }
87 } else if (code_unit >= 0xDC00 && code_unit <= 0xDFFF) {
88 cp = 0xFFFD;
89 }
90
91 if (cp < 0x80) {
92 out += static_cast<char>(cp);
93 } else if (cp < 0x800) {
94 out += static_cast<char>(0xC0 | (cp >> 6));
95 out += static_cast<char>(0x80 | (cp & 0x3F));
96 } else if (cp < 0x10000) {
97 out += static_cast<char>(0xE0 | (cp >> 12));
98 out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
99 out += static_cast<char>(0x80 | (cp & 0x3F));
100 } else {
101 out += static_cast<char>(0xF0 | (cp >> 18));
102 out += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
103 out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
104 out += static_cast<char>(0x80 | (cp & 0x3F));
105 }
106 }
107 return out;
108}
109
111std::mutex g_font_cache_mutex;
112std::map<std::string, Font> g_font_cache;
113
114} // namespace
115
116// ---------------------------------------------------------------------------
117// Font::Impl
118// ---------------------------------------------------------------------------
119
126 Impl(std::shared_ptr<const std::vector<uint8_t>> bytes, float size, std::string path)
127 : data(std::move(bytes)), source_path(std::move(path)), pixel_size(size) {
128 if (!data || data->empty()) {
129 throw std::runtime_error("scimesh: empty font data");
130 }
131 const int offset = stbtt_GetFontOffsetForIndex(data->data(), 0);
132 if (offset < 0) {
133 throw std::runtime_error(
134 "scimesh: not a usable TrueType/OpenType font" +
135 (source_path.empty() ? std::string() : " '" + source_path + "'"));
136 }
137 if (!stbtt_InitFont(&info, data->data(), offset)) {
138 throw std::runtime_error(
139 "scimesh: failed to parse font" +
140 (source_path.empty() ? std::string() : " '" + source_path + "'"));
141 }
142 scale = stbtt_ScaleForPixelHeight(&info, pixel_size);
143 int ascent = 0, descent = 0, line_gap = 0;
144 stbtt_GetFontVMetrics(&info, &ascent, &descent, &line_gap);
145 metrics.ascent = static_cast<float>(ascent) * scale;
146 metrics.descent = std::fabs(static_cast<float>(descent) * scale);
147 metrics.line_gap = static_cast<float>(line_gap) * scale;
148 }
149
150 std::shared_ptr<const std::vector<uint8_t>> data;
151 std::string source_path;
152 stbtt_fontinfo info{};
153 float pixel_size = 0.0f;
154 float scale = 0.0f;
156
158 mutable std::mutex mutex;
159
163 mutable std::map<uint32_t, GlyphBitmap> cache;
164};
165
166// ---------------------------------------------------------------------------
167// Loading
168// ---------------------------------------------------------------------------
169
170Font Font::from_file(const std::string &path, float pixel_size) {
171 if (!(pixel_size > 0.0f)) {
172 throw std::invalid_argument("scimesh: font pixel size must be > 0");
173 }
174 auto bytes = std::make_shared<const std::vector<uint8_t>>(read_file_bytes(path));
175 return Font(std::make_shared<Impl>(std::move(bytes), pixel_size, path));
176}
177
178Font Font::from_memory(std::vector<uint8_t> data, float pixel_size) {
179 if (!(pixel_size > 0.0f)) {
180 throw std::invalid_argument("scimesh: font pixel size must be > 0");
181 }
182 auto bytes = std::make_shared<const std::vector<uint8_t>>(std::move(data));
183 return Font(std::make_shared<Impl>(std::move(bytes), pixel_size, std::string()));
184}
185
186Font Font::with_pixel_size(float new_pixel_size) const {
187 if (!impl_) {
188 return Font();
189 }
190 if (!(new_pixel_size > 0.0f)) {
191 throw std::invalid_argument("scimesh: font pixel size must be > 0");
192 }
193 // Shares the font bytes, so no file access and no copy of the font data.
194 return Font(std::make_shared<Impl>(impl_->data, new_pixel_size, impl_->source_path));
195}
196
197// ---------------------------------------------------------------------------
198// Metrics and measurement
199// ---------------------------------------------------------------------------
200
201float Font::pixel_size() const { return impl_ ? impl_->pixel_size : 0.0f; }
202
203std::string Font::source_path() const { return impl_ ? impl_->source_path : std::string(); }
204
205FontMetrics Font::metrics() const { return impl_ ? impl_->metrics : FontMetrics(); }
206
207float Font::advance(uint32_t codepoint) const {
208 if (!impl_) {
209 return 0.0f;
210 }
211 int advance_width = 0, left_side_bearing = 0;
212 stbtt_GetCodepointHMetrics(&impl_->info, static_cast<int>(codepoint),
213 &advance_width, &left_side_bearing);
214 return static_cast<float>(advance_width) * impl_->scale;
215}
216
217float Font::kerning(uint32_t first, uint32_t second) const {
218 if (!impl_) {
219 return 0.0f;
220 }
221 const int kern = stbtt_GetCodepointKernAdvance(&impl_->info,
222 static_cast<int>(first),
223 static_cast<int>(second));
224 return static_cast<float>(kern) * impl_->scale;
225}
226
227float Font::measure(const std::string &text) const {
228 if (!impl_) {
229 return 0.0f;
230 }
231 const std::vector<uint32_t> codepoints = decode_utf8(text);
232 float width = 0.0f;
233 uint32_t previous = 0;
234 bool have_previous = false;
235 for (uint32_t cp : codepoints) {
236 if (have_previous) {
237 width += kerning(previous, cp);
238 }
239 width += advance(cp);
240 previous = cp;
241 have_previous = true;
242 }
243 return width;
244}
245
246const GlyphBitmap *Font::glyph(uint32_t codepoint) const {
247 if (!impl_) {
248 return nullptr;
249 }
250 std::lock_guard<std::mutex> lock(impl_->mutex);
251 auto it = impl_->cache.find(codepoint);
252 if (it != impl_->cache.end()) {
253 return &it->second;
254 }
255
257 const int glyph_index = stbtt_FindGlyphIndex(&impl_->info, static_cast<int>(codepoint));
258 int advance_width = 0, left_side_bearing = 0;
259 stbtt_GetGlyphHMetrics(&impl_->info, glyph_index, &advance_width, &left_side_bearing);
260 glyph.advance = static_cast<float>(advance_width) * impl_->scale;
261
262 if (glyph_index != 0) {
263 int width = 0, height = 0, x_offset = 0, y_offset = 0;
264 unsigned char *bitmap = stbtt_GetGlyphBitmap(&impl_->info, impl_->scale, impl_->scale,
265 glyph_index, &width, &height,
266 &x_offset, &y_offset);
267 if (bitmap && width > 0 && height > 0) {
268 glyph.width = width;
269 glyph.height = height;
270 glyph.x_offset = x_offset;
271 glyph.y_offset = y_offset;
272 const size_t count = static_cast<size_t>(width) * static_cast<size_t>(height);
273 glyph.coverage.assign(bitmap, bitmap + count);
274 }
275 stbtt_FreeBitmap(bitmap, impl_->info.userdata);
276 }
277
278 // std::map never invalidates pointers to existing elements on insert.
279 auto inserted = impl_->cache.emplace(codepoint, std::move(glyph));
280 return &inserted.first->second;
281}
282
283std::string Font::family_name() const {
284 if (!impl_) {
285 return std::string();
286 }
287 // nameID 1 is the font family, 4 the full name; platform 3 = Microsoft,
288 // encoding 1 = Unicode BMP, language 0x0409 = US English.
289 const int name_ids[] = {1, 4};
290 for (int name_id : name_ids) {
291 int length = 0;
292 const char *raw = stbtt_GetFontNameString(&impl_->info, &length,
293 STBTT_PLATFORM_ID_MICROSOFT,
294 STBTT_MS_EID_UNICODE_BMP,
295 STBTT_MS_LANG_ENGLISH, name_id);
296 if (raw && length > 0) {
297 std::string name = utf16be_to_utf8(
298 reinterpret_cast<const unsigned char *>(raw), length);
299 if (!name.empty()) {
300 return name;
301 }
302 }
303 }
304 return std::string();
305}
306
307// ---------------------------------------------------------------------------
308// Text decoding helpers
309// ---------------------------------------------------------------------------
310
311std::vector<uint32_t> Font::decode_utf8(const std::string &text) {
312 std::vector<uint32_t> out;
313 out.reserve(text.size());
314 const size_t n = text.size();
315 size_t i = 0;
316 while (i < n) {
317 const unsigned char byte = static_cast<unsigned char>(text[i]);
318 uint32_t cp = 0;
319 size_t extra = 0;
320 if (byte < 0x80) {
321 cp = byte;
322 } else if ((byte & 0xE0) == 0xC0) {
323 cp = byte & 0x1Fu;
324 extra = 1;
325 } else if ((byte & 0xF0) == 0xE0) {
326 cp = byte & 0x0Fu;
327 extra = 2;
328 } else if ((byte & 0xF8) == 0xF0) {
329 cp = byte & 0x07u;
330 extra = 3;
331 } else {
332 out.push_back(0xFFFD); // stray continuation or invalid lead byte
333 ++i;
334 continue;
335 }
336
337 bool valid = (i + extra < n);
338 for (size_t k = 1; valid && k <= extra; ++k) {
339 const unsigned char cont = static_cast<unsigned char>(text[i + k]);
340 if ((cont & 0xC0) != 0x80) {
341 valid = false;
342 break;
343 }
344 cp = (cp << 6) | (cont & 0x3Fu);
345 }
346 if (!valid) {
347 out.push_back(0xFFFD);
348 ++i;
349 continue;
350 }
351 out.push_back(cp);
352 i += extra + 1;
353 }
354 return out;
355}
356
357size_t Font::line_count(const std::string &text) {
358 size_t lines = 1;
359 for (char c : text) {
360 if (c == '\n') {
361 ++lines;
362 }
363 }
364 return lines;
365}
366
367// ---------------------------------------------------------------------------
368// Font resolution and caching
369// ---------------------------------------------------------------------------
370
371std::string default_font_path() {
372 const char *env = std::getenv("SCIMESH_FONT");
373 if (env && *env && file_readable(env)) {
374 return std::string(env);
375 }
376#ifdef SCIMESH_DEFAULT_FONT
377 if (file_readable(SCIMESH_DEFAULT_FONT)) {
378 return std::string(SCIMESH_DEFAULT_FONT);
379 }
380#endif
381 // Paths relative to the current working directory, which covers running
382 // from the source tree or from a build directory below it.
383 static const char *candidates[] = {
384 "inst/extdata/Inter-Regular.ttf",
385 "../inst/extdata/Inter-Regular.ttf",
386 "../../inst/extdata/Inter-Regular.ttf",
387 "../../../inst/extdata/Inter-Regular.ttf",
388 "../../../../inst/extdata/Inter-Regular.ttf",
389 };
390 for (const char *candidate : candidates) {
391 if (file_readable(candidate)) {
392 return std::string(candidate);
393 }
394 }
395 return std::string();
396}
397
398std::string resolve_font_path(const std::string &path) {
399 if (!path.empty()) {
400 return path;
401 }
402 return default_font_path();
403}
404
405Font cached_font(const std::string &path, float pixel_size) {
406 const std::string resolved = resolve_font_path(path);
407 if (resolved.empty()) {
408 throw std::runtime_error(
409 "scimesh: no font available. Pass a font file, use the bundled "
410 "'inst/extdata/Inter-Regular.ttf', or set the SCIMESH_FONT "
411 "environment variable to a readable .ttf file.");
412 }
413 if (!(pixel_size > 0.0f)) {
414 throw std::invalid_argument("scimesh: font pixel size must be > 0");
415 }
416
417 const std::string key = resolved + '\x1f' + std::to_string(pixel_size);
418 std::lock_guard<std::mutex> lock(g_font_cache_mutex);
419 auto it = g_font_cache.find(key);
420 if (it != g_font_cache.end()) {
421 return it->second;
422 }
423 Font font = Font::from_file(resolved, pixel_size);
424 g_font_cache.emplace(key, font);
425 return font;
426}
427
429 std::lock_guard<std::mutex> lock(g_font_cache_mutex);
430 g_font_cache.clear();
431}
432
433} // namespace scimesh
A TrueType font, loaded at a fixed pixel size, with a glyph cache.
Definition font.h:143
static size_t line_count(const std::string &text)
Number of lines in a string (newline-separated, at least 1).
Definition font.cpp:357
std::string family_name() const
Family name of the font, e.g. "Inter" (empty if unavailable).
Definition font.cpp:283
static std::vector< uint32_t > decode_utf8(const std::string &text)
Decode a UTF-8 string into Unicode code points.
Definition font.cpp:311
static Font from_file(const std::string &path, float pixel_size)
Load a TrueType font from a file.
Definition font.cpp:170
FontMetrics metrics() const
Vertical metrics (ascent, descent, line gap) in pixels.
Definition font.cpp:205
Font with_pixel_size(float new_pixel_size) const
A font handle for the same typeface at a different pixel size.
Definition font.cpp:186
float advance(uint32_t codepoint) const
Pen advance of one code point in pixels.
Definition font.cpp:207
std::string source_path() const
Path this font was loaded from, or an empty string for from_memory() fonts.
Definition font.cpp:203
float pixel_size() const
Em size of this font in pixels (0 for an invalid font).
Definition font.cpp:201
Font()=default
Construct an empty (invalid) font. See valid().
float measure(const std::string &text) const
Width of a single line of text in pixels, including kerning.
Definition font.cpp:227
const GlyphBitmap * glyph(uint32_t codepoint) const
The rasterized glyph for a code point, rasterizing it on first use.
Definition font.cpp:246
bool valid() const
Whether this handle refers to a loaded font.
Definition font.h:166
float kerning(uint32_t first, uint32_t second) const
Kerning adjustment between two code points, in pixels.
Definition font.cpp:217
static Font from_memory(std::vector< uint8_t > data, float pixel_size)
Create a font from TTF data already in memory.
Definition font.cpp:178
TrueType font loading, glyph rasterization and text measurement.
std::string resolve_font_path(const std::string &path)
Turn a possibly empty font path into a path that exists.
Definition font.cpp:398
void clear_font_cache()
Drop all fonts from the process-wide font cache.
Definition font.cpp:428
std::string default_font_path()
Path of the bundled default font, or as overridden by the SCIMESH_FONT environment variable.
Definition font.cpp:371
Font cached_font(const std::string &path, float pixel_size)
Load a font, re-using an already loaded one when possible.
Definition font.cpp:405
float width
Definition renderer.cpp:36
Vertical metrics of a font at a given pixel size.
Definition font.h:59
float ascent
Distance from the baseline to the top of the line, in pixels.
Definition font.h:61
float line_gap
Recommended extra spacing between two lines, in pixels.
Definition font.h:69
float descent
Distance from the baseline to the bottom of the line, in pixels.
Definition font.h:66
Shared font state: the font bytes, the parsed font and the glyph cache.
Definition font.cpp:125
std::string source_path
Origin of the font, if any.
Definition font.cpp:151
stbtt_fontinfo info
Parsed font tables.
Definition font.cpp:152
float pixel_size
Em size in pixels.
Definition font.cpp:153
std::map< uint32_t, GlyphBitmap > cache
Rasterized glyphs, filled on demand.
Definition font.cpp:163
Impl(std::shared_ptr< const std::vector< uint8_t > > bytes, float size, std::string path)
Definition font.cpp:126
std::mutex mutex
Guards cache; makes a Font safe to share between threads.
Definition font.cpp:158
std::shared_ptr< const std::vector< uint8_t > > data
Raw font file bytes.
Definition font.cpp:150
FontMetrics metrics
Vertical metrics in pixels.
Definition font.cpp:155
float scale
Font units to pixel scale.
Definition font.cpp:154
One rasterized glyph: an 8-bit coverage bitmap plus placement info.
Definition font.h:91
float advance
Pen advance for this glyph, in pixels.
Definition font.h:108
int x_offset
Horizontal offset from the pen position to the bitmap's left edge.
Definition font.h:99
int y_offset
Vertical offset from the baseline to the bitmap's top edge.
Definition font.h:105
int height
Bitmap height in pixels.
Definition font.h:96
std::vector< uint8_t > coverage
Coverage values, row-major, width * height entries.
Definition font.h:111
int width
Bitmap width in pixels.
Definition font.h:93
const GlyphBitmap * bitmap
Definition text.cpp:222