feat(text): optional vendored stb_truetype backend for runtime external fonts
This commit is contained in:
1 parent
daff60e3f0
commit
14d664a246
12 files changed
+6511
-19
No files matched your search
@@ -0,0 +1,58 @@
|
||||
# The optional runtime-font target: plot::text::TrueTypeFont, which bakes a real
|
||||
# TrueType/OpenType file into a fixed-cell coverage atlas at run time. It is the
|
||||
# only target in the project that reaches the vendored third_party/stb_truetype.h,
|
||||
# and the top-level CMakeLists.txt adds this directory only when PLOT_BUILD_TTF_FONT
|
||||
# is on -- so plot_core, its public headers and every other target stay
|
||||
# third-party-free, and a build with the option off neither compiles nor links any
|
||||
# of it.
|
||||
#
|
||||
# Two targets, not one, and for the reason the whole task exists: stb_truetype is
|
||||
# third-party source, so it is compiled in its own object library to which
|
||||
# plot_apply_warnings() is *not* applied. The project's warning set stays on our
|
||||
# own code (and stays clean under it) without a single per-file suppression to
|
||||
# quiet someone else's header -- see cmake/plot_options.cmake, whose warning
|
||||
# policy is "never suppressed per file".
|
||||
add_library(plot_stb_truetype OBJECT stb_truetype_impl.cpp)
|
||||
# The vendored header's directory is PRIVATE and on this object library alone:
|
||||
# nothing else in the build can reach stb_truetype.h even by accident, and a
|
||||
# consumer of the installed target needs none of it (the parser is compiled into
|
||||
# the archive, so only our own header is on the interface).
|
||||
target_include_directories(plot_stb_truetype PRIVATE "${CMAKE_SOURCE_DIR}/third_party")
|
||||
target_compile_features(plot_stb_truetype PRIVATE cxx_std_20)
|
||||
|
||||
# The backend itself. Its objects come from the object library above, so the
|
||||
# archive is self-contained: a consumer links plot::font_ttf and nothing else, and
|
||||
# no non-exported target leaks into the install set's link interface.
|
||||
add_library(plot_font_ttf STATIC
|
||||
true_type_font.cpp
|
||||
$<TARGET_OBJECTS:plot_stb_truetype>)
|
||||
add_library(plot::font_ttf ALIAS plot_font_ttf)
|
||||
# The installed tree names it plot::font_ttf, like the build tree (see
|
||||
# plot_core's EXPORT_NAME).
|
||||
set_target_properties(plot_font_ttf PROPERTIES EXPORT_NAME font_ttf)
|
||||
|
||||
# The public include root is the project's own `include/`, because that is where
|
||||
# the target's header is reached from: <plot/text/true_type_font.hpp>. The build
|
||||
# and install interfaces are the same pair plot_core declares, spelled here
|
||||
# because this target is the one that owns the header.
|
||||
target_include_directories(plot_font_ttf PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>)
|
||||
|
||||
# PRIVATE, like the object library's: this target's own source includes the
|
||||
# vendored header for its declarations (the implementation is the object library
|
||||
# above), and nothing that links plot::font_ttf inherits the path -- a consumer
|
||||
# needs none of it, because stb_truetype is compiled into the archive.
|
||||
target_include_directories(plot_font_ttf PRIVATE "${CMAKE_SOURCE_DIR}/third_party")
|
||||
target_compile_features(plot_font_ttf PUBLIC cxx_std_20)
|
||||
target_link_libraries(plot_font_ttf PUBLIC plot::core)
|
||||
plot_apply_warnings(plot_font_ttf)
|
||||
|
||||
# It joins the install and export set: it has no dependency a consumer would have
|
||||
# to re-find (the vendored header is compiled in, and third_party/ is not
|
||||
# installed at all), so find_package(plot) + target_link_libraries(consumer
|
||||
# PRIVATE plot::font_ttf) works from an installed prefix whenever the option was
|
||||
# on -- and plotConfig.cmake then needs no find_dependency() for it. The reason
|
||||
# string is what the configure log prints.
|
||||
plot_record_adapter_export(plot_font_ttf TRUE
|
||||
"its only third-party dependency is compiled in from third_party/" "")
|
||||
@@ -0,0 +1,16 @@
|
||||
// The vendored rasteriser's single implementation unit: the one translation unit
|
||||
// in this project that defines STB_TRUETYPE_IMPLEMENTATION, so the ~5000-line
|
||||
// stb_truetype.h is compiled exactly once, into the optional plot_font_ttf
|
||||
// target, and never into plot_core. It is a separate translation unit -- and its
|
||||
// own object-library target in src/text/CMakeLists.txt -- so that the project's
|
||||
// warning set (-Wall -Wextra -Wpedantic -Wconversion -Wshadow) covers our own
|
||||
// sources without covering third-party code, and so that no warning is ever
|
||||
// suppressed per file to accommodate it (see plot_apply_warnings in
|
||||
// cmake/plot_options.cmake).
|
||||
//
|
||||
// stb_truetype v1.26 -- public domain, Sean Barrett / RAD Game Tools. The
|
||||
// vendored copy at third_party/stb_truetype.h is pinned by the sha256 recorded in
|
||||
// the wrapper header include/plot/text/true_type_font.hpp, which is also where
|
||||
// the source URL and the retrieval date live.
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <stb_truetype.h>
|
||||
@@ -0,0 +1,529 @@
|
||||
// The optional TrueType backend: the one place in the project that parses a font
|
||||
// file at run time, and the only consumer of the vendored third_party/stb_truetype.h
|
||||
// (see src/text/stb_truetype_impl.cpp for its single implementation unit, and
|
||||
// include/plot/text/true_type_font.hpp for its provenance). Built only by the
|
||||
// PLOT_BUILD_TTF_FONT target, so nothing here is compiled or linked into
|
||||
// plot_core.
|
||||
//
|
||||
// The bake follows tools/fonts/gen_atlas.py's cell scheme, deliberately: a
|
||||
// proportional face is drawn into a uniform cell rather than trimmed to its ink,
|
||||
// because the text pipeline stages one rect origin per glyph and carries the
|
||||
// rect's size once per draw (see render::TextBackend::cell_uv). The cell is as
|
||||
// wide as the widest advance *or* the widest ink's right edge -- the pen must
|
||||
// never leave the cell, and neither may a glyph's ink -- and as tall as the
|
||||
// face's ascent-to-descent extent, extended when a baked glyph's own bitmap box
|
||||
// needs more room than the face declares. The pen starts at the cell's left edge
|
||||
// and the baseline sits `ascent()` below the cell's top, so the ink is blitted at
|
||||
// the offset its bitmap box reports from the pen and every rect stays one size.
|
||||
|
||||
#include <plot/text/true_type_font.hpp>
|
||||
|
||||
#include <stb_truetype.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <new>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
namespace plot::text {
|
||||
namespace {
|
||||
|
||||
/// The fallback box, in font units relative to the baseline: the same outline
|
||||
/// `tools/fonts/gen_atlas.py` bakes for the embedded face (its FALLBACK_OUTER and
|
||||
/// FALLBACK_INNER), so a codepoint neither backend can spell shows the same mark.
|
||||
/// It is drawn from geometry rather than from the face, because the face may have
|
||||
/// no usable `.notdef` and a fallback that depends on one is not a fallback.
|
||||
constexpr f32 kFallbackOuterX0 = 60.0f;
|
||||
constexpr f32 kFallbackOuterY0 = -100.0f;
|
||||
constexpr f32 kFallbackOuterX1 = 580.0f;
|
||||
constexpr f32 kFallbackOuterY1 = 700.0f;
|
||||
constexpr f32 kFallbackInnerX0 = 160.0f;
|
||||
constexpr f32 kFallbackInnerY0 = 0.0f;
|
||||
constexpr f32 kFallbackInnerX1 = 480.0f;
|
||||
constexpr f32 kFallbackInnerY1 = 600.0f;
|
||||
|
||||
/// A codepoint the face can draw, with the advance the pen moves by: pass one of
|
||||
/// a bake, whose only job is to size the cell every one of them is drawn into.
|
||||
struct Drawable {
|
||||
u32 codepoint = 0;
|
||||
f32 advance = 0.0f;
|
||||
};
|
||||
|
||||
/// One bake's complete output, so `from_file()` and `set_coverage()` can build a
|
||||
/// new one and swap it in only when it succeeded: a failed bake leaves the
|
||||
/// previous atlas -- and every glyph pointer a caller may be holding -- exactly
|
||||
/// where it was.
|
||||
struct Bake {
|
||||
u32 cell_w = 0;
|
||||
u32 cell_h = 0;
|
||||
i32 baseline = 0;
|
||||
std::vector<u32> codepoints;
|
||||
std::vector<render::Glyph> glyphs;
|
||||
render::Glyph fallback{};
|
||||
std::vector<u8> atlas;
|
||||
u32 atlas_w = 0;
|
||||
u32 atlas_h = 0;
|
||||
};
|
||||
|
||||
bool fail(Status* st, ErrorCode code, const char* message) noexcept {
|
||||
if (st != nullptr) {
|
||||
*st = Status{code, message};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
u32 read_be16(std::span<const u8> bytes, std::size_t at) noexcept {
|
||||
return (static_cast<u32>(bytes[at]) << 8) | static_cast<u32>(bytes[at + 1]);
|
||||
}
|
||||
|
||||
u32 read_be32(std::span<const u8> bytes, std::size_t at) noexcept {
|
||||
return (static_cast<u32>(bytes[at]) << 24) | (static_cast<u32>(bytes[at + 1]) << 16) |
|
||||
(static_cast<u32>(bytes[at + 2]) << 8) | static_cast<u32>(bytes[at + 3]);
|
||||
}
|
||||
|
||||
/// True when `bytes` opens with a font signature *and* the table directory that
|
||||
/// follows fits inside it.
|
||||
///
|
||||
/// stb_truetype does no range checking of a font file's offsets -- its own header
|
||||
/// says so, in capitals -- and its table scan reads `numTables` 16-byte records
|
||||
/// from offset 12 whatever the file's length is. Handing it a text file would
|
||||
/// therefore read past the buffer: an immediate error under the address
|
||||
/// sanitizer, and a plain out-of-bounds read everywhere else. So the directory the
|
||||
/// scan walks is validated here, before the parser sees a byte. What is inside
|
||||
/// those tables stays stb's business: a *random* file is rejected here, and a file
|
||||
/// *crafted* to be hostile remains out of scope, as the vendored header warns.
|
||||
bool has_sfnt_directory(std::span<const u8> bytes) noexcept {
|
||||
if (bytes.size() < 12u) {
|
||||
return false;
|
||||
}
|
||||
const u32 version = read_be32(bytes, 0u);
|
||||
constexpr u32 kTrueType = 0x00010000u;
|
||||
constexpr u32 kAppleTrue = 0x74727565u; // 'true'
|
||||
constexpr u32 kOpenTypeCff = 0x4F54544Fu; // 'OTTO'
|
||||
constexpr u32 kTypeOne = 0x74797031u; // 'typ1'
|
||||
constexpr u32 kCollection = 0x74746366u; // 'ttcf'
|
||||
if (version == kCollection) {
|
||||
// stb reads a collection's first font offset at byte 12, so the header has to
|
||||
// be that long before it is asked for one.
|
||||
return bytes.size() >= 16u;
|
||||
}
|
||||
if (version != kTrueType && version != kAppleTrue && version != kOpenTypeCff &&
|
||||
version != kTypeOne) {
|
||||
return false;
|
||||
}
|
||||
const u64 directory_end = 12u + 16u * static_cast<u64>(read_be16(bytes, 4u));
|
||||
return directory_end <= static_cast<u64>(bytes.size());
|
||||
}
|
||||
|
||||
/// Blits `bitmap` into `slot`'s cell, `x`/`y` being the ink's top-left in cell
|
||||
/// coordinates. Ink outside the cell is dropped rather than written into the
|
||||
/// neighbouring cell: a face whose ink overhangs its advance would otherwise
|
||||
/// corrupt whichever glyph owns the next column, and clipping is what the
|
||||
/// embedded generator does at the same boundary. Returns the texels written.
|
||||
u32 blit_cell(std::vector<u8>& atlas, const Bake& bake, u32 slot, const u8* bitmap, i32 width,
|
||||
i32 height, i32 x, i32 y) noexcept {
|
||||
u32 written = 0;
|
||||
for (i32 row = 0; row < height; ++row) {
|
||||
const i32 cell_y = y + row;
|
||||
if (cell_y < 0 || cell_y >= static_cast<i32>(bake.cell_h)) {
|
||||
continue;
|
||||
}
|
||||
for (i32 column = 0; column < width; ++column) {
|
||||
const u8 value = bitmap[static_cast<std::size_t>(row) * static_cast<std::size_t>(width) +
|
||||
static_cast<std::size_t>(column)];
|
||||
if (value == 0u) {
|
||||
continue;
|
||||
}
|
||||
const i32 cell_x = x + column;
|
||||
if (cell_x < 0 || cell_x >= static_cast<i32>(bake.cell_w)) {
|
||||
continue;
|
||||
}
|
||||
atlas[static_cast<std::size_t>(cell_y) * bake.atlas_w +
|
||||
static_cast<std::size_t>(slot) * bake.cell_w + static_cast<std::size_t>(cell_x)] = value;
|
||||
++written;
|
||||
}
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
/// Draws the fallback box into `slot`'s cell: the outline between the outer and
|
||||
/// inner rectangles, roughly one texel thick at the bake's scale. It is geometry,
|
||||
/// not a rasterised glyph, so the box is there for a face with no usable
|
||||
/// `.notdef` too.
|
||||
void blit_fallback_box(std::vector<u8>& atlas, const Bake& bake, u32 slot, f32 scale) noexcept {
|
||||
const auto to_x = [scale](f32 units) { return static_cast<i32>(std::lround(units * scale)); };
|
||||
const auto to_y = [&bake, scale](f32 units) {
|
||||
return bake.baseline - static_cast<i32>(std::lround(units * scale));
|
||||
};
|
||||
const i32 outer_left = to_x(kFallbackOuterX0);
|
||||
const i32 outer_right = to_x(kFallbackOuterX1);
|
||||
const i32 outer_top = to_y(kFallbackOuterY1);
|
||||
const i32 outer_bottom = to_y(kFallbackOuterY0);
|
||||
const i32 inner_left = to_x(kFallbackInnerX0);
|
||||
const i32 inner_right = to_x(kFallbackInnerX1);
|
||||
const i32 inner_top = to_y(kFallbackInnerY1);
|
||||
const i32 inner_bottom = to_y(kFallbackInnerY0);
|
||||
for (i32 y = outer_top; y < outer_bottom; ++y) {
|
||||
if (y < 0 || y >= static_cast<i32>(bake.cell_h)) {
|
||||
continue;
|
||||
}
|
||||
for (i32 x = outer_left; x < outer_right; ++x) {
|
||||
if (x < 0 || x >= static_cast<i32>(bake.cell_w)) {
|
||||
continue;
|
||||
}
|
||||
if (x >= inner_left && x < inner_right && y >= inner_top && y < inner_bottom) {
|
||||
continue; // the hole the box is drawn around
|
||||
}
|
||||
atlas[static_cast<std::size_t>(y) * bake.atlas_w +
|
||||
static_cast<std::size_t>(slot) * bake.cell_w + static_cast<std::size_t>(x)] = 255u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bakes `coverage` at `scale` into `out`: the cell, the glyph table and the
|
||||
/// single-row atlas, in two passes over the face. False, with `*st` filled in,
|
||||
/// when no requested codepoint produced ink -- an atlas whose only mark is the
|
||||
/// fallback box is the "everything the caller asked for came out blank" failure
|
||||
/// this backend exists to refuse (see `TrueTypeFont::from_file`). `out` is only
|
||||
/// meaningful when this returns true.
|
||||
bool bake_face(const stbtt_fontinfo& info, f32 scale, std::span<const u32> coverage, Bake* out,
|
||||
Status* st) {
|
||||
i32 ascent_units = 0;
|
||||
i32 descent_units = 0;
|
||||
i32 line_gap = 0;
|
||||
stbtt_GetFontVMetrics(&info, &ascent_units, &descent_units, &line_gap);
|
||||
const f32 ascent_f = static_cast<f32>(ascent_units) * scale;
|
||||
const f32 descent_f = -static_cast<f32>(descent_units) * scale;
|
||||
|
||||
std::vector<u32> requested(coverage.begin(), coverage.end());
|
||||
std::sort(requested.begin(), requested.end());
|
||||
requested.erase(std::unique(requested.begin(), requested.end()), requested.end());
|
||||
|
||||
// Pass one: what every drawable codepoint would need. A codepoint the face has
|
||||
// no glyph for is dropped here rather than given a blank cell -- the draw then
|
||||
// falls back to the box, which is visible, where a blank cell renders nothing
|
||||
// and says so to nobody.
|
||||
std::vector<Drawable> drawable;
|
||||
drawable.reserve(requested.size());
|
||||
f32 widest_advance = 0.0f;
|
||||
i32 highest = 0;
|
||||
i32 lowest = 0;
|
||||
i32 rightmost = 0;
|
||||
for (const u32 codepoint : requested) {
|
||||
if (codepoint > 0x10FFFFu) {
|
||||
continue; // kInvalidCodepoint, and anything else that is not a scalar
|
||||
}
|
||||
if (stbtt_FindGlyphIndex(&info, static_cast<i32>(codepoint)) == 0) {
|
||||
continue;
|
||||
}
|
||||
i32 x0 = 0;
|
||||
i32 y0 = 0;
|
||||
i32 x1 = 0;
|
||||
i32 y1 = 0;
|
||||
stbtt_GetCodepointBitmapBox(&info, static_cast<i32>(codepoint), scale, scale, &x0, &y0, &x1,
|
||||
&y1);
|
||||
i32 advance_units = 0;
|
||||
i32 bearing_units = 0;
|
||||
stbtt_GetCodepointHMetrics(&info, static_cast<i32>(codepoint), &advance_units, &bearing_units);
|
||||
const f32 advance = static_cast<f32>(advance_units) * scale;
|
||||
widest_advance = std::max(widest_advance, advance);
|
||||
highest = std::min(highest, y0);
|
||||
lowest = std::max(lowest, y1);
|
||||
rightmost = std::max(rightmost, x1);
|
||||
drawable.push_back(Drawable{codepoint, advance});
|
||||
}
|
||||
|
||||
// The cell: wide enough for the widest advance *and* the widest ink (the
|
||||
// generator's rule -- an overhanging glyph stays inside its own cell instead of
|
||||
// reaching into the next one), as tall as the face's ascent-to-descent extent,
|
||||
// and never so short that a baked bitmap box hangs out of it.
|
||||
const u32 cell_w = std::max<u32>(
|
||||
1u,
|
||||
static_cast<u32>(std::ceil(std::max(widest_advance, static_cast<f32>(rightmost)) - 1e-4f)));
|
||||
i32 baseline = static_cast<i32>(std::floor(ascent_f));
|
||||
baseline = std::max(baseline, -highest); // room for the tallest ink above the pen
|
||||
baseline = std::max(baseline, 0);
|
||||
const u32 extent = static_cast<u32>(std::ceil(ascent_f + descent_f - 1e-4f));
|
||||
const u32 below = static_cast<u32>(std::max(0, baseline + lowest));
|
||||
const u32 cell_h = std::max<u32>(1u, std::max(extent, below));
|
||||
|
||||
out->cell_w = cell_w;
|
||||
out->cell_h = cell_h;
|
||||
out->baseline = baseline;
|
||||
const u32 slots = static_cast<u32>(drawable.size()) + 1u; // the fallback box is the last slot
|
||||
out->atlas_w = cell_w * slots;
|
||||
out->atlas_h = cell_h;
|
||||
out->atlas.assign(static_cast<std::size_t>(out->atlas_w) * out->atlas_h, 0u);
|
||||
out->codepoints.clear();
|
||||
out->codepoints.reserve(drawable.size());
|
||||
out->glyphs.clear();
|
||||
out->glyphs.reserve(slots);
|
||||
|
||||
// Every glyph's rect is the same one cell, so the rect is built once per slot
|
||||
// and only its `u0` moves. The v range is the whole atlas height, which is one
|
||||
// cell: the atlas is a single row, so `cell_uv()` is `(cell_w / atlas_w, 1)`.
|
||||
const f32 du = static_cast<f32>(cell_w) / static_cast<f32>(out->atlas_w);
|
||||
const f32 dv = static_cast<f32>(cell_h) / static_cast<f32>(out->atlas_h);
|
||||
const auto glyph_for_slot = [&](u32 slot, f32 advance) {
|
||||
const f32 u0 = static_cast<f32>(slot * cell_w) / static_cast<f32>(out->atlas_w);
|
||||
return render::Glyph{u0, 0.0f, u0 + du, dv, 0.0f, 0.0f, advance};
|
||||
};
|
||||
|
||||
// Pass two: rasterise and blit. The offset `stbtt_GetCodepointBitmap` reports is
|
||||
// the box pass one measured -- stb derives both from the same glyph box -- which
|
||||
// is why placing the ink at it is what keeps the bearing.
|
||||
u32 inked = 0;
|
||||
for (std::size_t i = 0; i < drawable.size(); ++i) {
|
||||
const Drawable& glyph = drawable[i];
|
||||
i32 width = 0;
|
||||
i32 height = 0;
|
||||
i32 x = 0;
|
||||
i32 y = 0;
|
||||
u8* bitmap = stbtt_GetCodepointBitmap(&info, scale, scale, static_cast<i32>(glyph.codepoint),
|
||||
&width, &height, &x, &y);
|
||||
if (bitmap != nullptr) {
|
||||
inked += blit_cell(out->atlas, *out, static_cast<u32>(i), bitmap, width, height, x,
|
||||
baseline + y);
|
||||
stbtt_FreeBitmap(bitmap, nullptr);
|
||||
}
|
||||
out->codepoints.push_back(glyph.codepoint);
|
||||
out->glyphs.push_back(glyph_for_slot(static_cast<u32>(i), glyph.advance));
|
||||
}
|
||||
|
||||
if (inked == 0u) {
|
||||
return fail(st, ErrorCode::InvalidArgument,
|
||||
"the bake produced no ink: none of the requested codepoints drew anything at this "
|
||||
"em size");
|
||||
}
|
||||
|
||||
// The fallback slot, last: the embedded face's box, with the widest advance so a
|
||||
// run of missing glyphs takes the room of a real one.
|
||||
const u32 fallback_slot = static_cast<u32>(drawable.size());
|
||||
blit_fallback_box(out->atlas, *out, fallback_slot, scale);
|
||||
out->fallback = glyph_for_slot(fallback_slot, widest_advance);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct TrueTypeFont::Impl {
|
||||
/// The whole file, because `stbtt_fontinfo` holds pointers into it and a font
|
||||
/// is not readable piecemeal: the parser walks tables wherever they are.
|
||||
std::vector<u8> file;
|
||||
stbtt_fontinfo info{};
|
||||
f32 px = 0.0f;
|
||||
f32 scale = 0.0f;
|
||||
/// The coverage a re-point at another file inherits, so a host's choice of
|
||||
/// codepoints survives a `from_file()`.
|
||||
std::vector<u32> coverage;
|
||||
Bake bake;
|
||||
/// Counts the bakes this object has performed, so a face installed over one the
|
||||
/// renderer has already uploaded never carries the number it uploaded.
|
||||
u32 revision = 0;
|
||||
};
|
||||
|
||||
TrueTypeFont::TrueTypeFont() noexcept = default;
|
||||
TrueTypeFont::~TrueTypeFont() = default;
|
||||
|
||||
std::span<const u32> TrueTypeFont::default_coverage() noexcept {
|
||||
// The embedded font's coverage: printable ASCII, the codepoints
|
||||
// `plot::format_si()` emits, and the two dashes a plotting label uses. The
|
||||
// fallback box is not a codepoint, so it is not in the list -- it is what every
|
||||
// codepoint *outside* the list draws.
|
||||
static const std::vector<u32> kCoverage = [] {
|
||||
std::vector<u32> codepoints;
|
||||
codepoints.reserve(100u);
|
||||
for (u32 codepoint = 0x20u; codepoint <= 0x7Eu; ++codepoint) {
|
||||
codepoints.push_back(codepoint);
|
||||
}
|
||||
codepoints.push_back(0x00B0u); // degree
|
||||
codepoints.push_back(0x00B5u); // micro
|
||||
codepoints.push_back(0x03A9u); // ohm
|
||||
codepoints.push_back(0x2013u); // en dash
|
||||
codepoints.push_back(0x2014u); // em dash
|
||||
return codepoints;
|
||||
}();
|
||||
return kCoverage;
|
||||
}
|
||||
|
||||
bool TrueTypeFont::from_file(const char* utf8_path, f32 px, TrueTypeFont* out, Status* st) {
|
||||
if (out == nullptr) {
|
||||
return fail(st, ErrorCode::InvalidArgument,
|
||||
"TrueTypeFont::from_file() needs somewhere to put the font");
|
||||
}
|
||||
if (utf8_path == nullptr || utf8_path[0] == '\0') {
|
||||
return fail(st, ErrorCode::InvalidArgument, "TrueTypeFont::from_file() needs a font file path");
|
||||
}
|
||||
if (!std::isfinite(px) || !(px > 0.0f)) {
|
||||
return fail(st, ErrorCode::InvalidArgument,
|
||||
"a font em size must be a positive, finite number of pixels");
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path{utf8_path};
|
||||
const std::filesystem::file_status path_status = std::filesystem::status(path, ec);
|
||||
if (ec || !std::filesystem::exists(path_status)) {
|
||||
return fail(st, ErrorCode::InvalidArgument, "no such font file");
|
||||
}
|
||||
if (!std::filesystem::is_regular_file(path_status)) {
|
||||
return fail(st, ErrorCode::InvalidArgument, "the font path is not a regular file");
|
||||
}
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
if (ec || size == 0u) {
|
||||
return fail(st, ErrorCode::InvalidArgument, "the font file is empty or its size is unreadable");
|
||||
}
|
||||
|
||||
try {
|
||||
// Everything is built in a candidate and swapped in at the end, so a failure
|
||||
// at any step leaves `*out` exactly as it was -- including the atlas and the
|
||||
// glyph pointers a caller may still be drawing from.
|
||||
Impl candidate;
|
||||
candidate.px = px;
|
||||
if (out->impl_ != nullptr) {
|
||||
candidate.coverage = out->impl_->coverage;
|
||||
candidate.revision = out->impl_->revision;
|
||||
} else {
|
||||
candidate.coverage.assign(default_coverage().begin(), default_coverage().end());
|
||||
}
|
||||
|
||||
candidate.file.resize(static_cast<std::size_t>(size));
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) {
|
||||
return fail(st, ErrorCode::InvalidArgument, "the font file cannot be opened");
|
||||
}
|
||||
in.read(reinterpret_cast<char*>(candidate.file.data()),
|
||||
static_cast<std::streamsize>(candidate.file.size()));
|
||||
if (!in) {
|
||||
return fail(st, ErrorCode::InvalidArgument, "the font file cannot be read");
|
||||
}
|
||||
|
||||
const std::span<const u8> bytes{candidate.file};
|
||||
// The directory is validated before stb sees the file at all -- and again at
|
||||
// the font the collection header points at, if this is a collection, because
|
||||
// that offset comes out of the file too (see has_sfnt_directory).
|
||||
int offset = -1;
|
||||
if (has_sfnt_directory(bytes)) {
|
||||
offset = stbtt_GetFontOffsetForIndex(candidate.file.data(), 0);
|
||||
}
|
||||
if (offset < 0 || static_cast<std::uintmax_t>(offset) >= size ||
|
||||
!has_sfnt_directory(bytes.subspan(static_cast<std::size_t>(offset)))) {
|
||||
return fail(st, ErrorCode::InvalidArgument, "the file is not a font");
|
||||
}
|
||||
if (stbtt_InitFont(&candidate.info, candidate.file.data(), offset) == 0) {
|
||||
return fail(st, ErrorCode::InvalidArgument,
|
||||
"the font cannot be parsed: a TrueType outline needs a table this file lacks");
|
||||
}
|
||||
candidate.scale = stbtt_ScaleForPixelHeight(&candidate.info, px);
|
||||
if (!(candidate.scale > 0.0f)) {
|
||||
return fail(st, ErrorCode::InvalidArgument, "the face declares no size to scale from");
|
||||
}
|
||||
if (!bake_face(candidate.info, candidate.scale, candidate.coverage, &candidate.bake, st)) {
|
||||
return false;
|
||||
}
|
||||
candidate.revision += 1u;
|
||||
out->impl_ = std::make_unique<Impl>(std::move(candidate));
|
||||
} catch (const std::bad_alloc&) {
|
||||
return fail(st, ErrorCode::OutOfMemory, "the font atlas could not be allocated");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void TrueTypeFont::set_coverage(std::span<const u32> codepoints) {
|
||||
Impl* impl = impl_.get();
|
||||
if (impl == nullptr) {
|
||||
return; // no face: nothing to bake, and nothing to re-bake
|
||||
}
|
||||
std::vector<u32> coverage(codepoints.begin(), codepoints.end());
|
||||
try {
|
||||
Bake candidate;
|
||||
if (!bake_face(impl->info, impl->scale, coverage, &candidate, nullptr)) {
|
||||
// A re-bake that produced no ink cannot be reported through this interface
|
||||
// -- there is no Status to fill in -- so the previous atlas stays. Losing a
|
||||
// working font to an argument this backend cannot answer for is worse than
|
||||
// ignoring the request.
|
||||
return;
|
||||
}
|
||||
impl->coverage = std::move(coverage);
|
||||
impl->bake = std::move(candidate);
|
||||
impl->revision += 1u;
|
||||
} catch (const std::bad_alloc&) {
|
||||
// Same reasoning: the atlas that is already there is a font that works.
|
||||
}
|
||||
}
|
||||
|
||||
f32 TrueTypeFont::measure(const char* utf8) const {
|
||||
if (utf8 == nullptr || impl_ == nullptr) {
|
||||
return 0.0f;
|
||||
}
|
||||
const Bake& bake = impl_->bake;
|
||||
f32 width = 0.0f;
|
||||
for (const char* p = utf8; *p != '\0';) {
|
||||
const u32 codepoint = render::next_codepoint(p);
|
||||
const auto found = std::lower_bound(bake.codepoints.begin(), bake.codepoints.end(), codepoint);
|
||||
width += (found != bake.codepoints.end() && *found == codepoint)
|
||||
? bake.glyphs[static_cast<std::size_t>(found - bake.codepoints.begin())].advance
|
||||
: bake.fallback.advance;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
f32 TrueTypeFont::height() const {
|
||||
return impl_ != nullptr ? static_cast<f32>(impl_->bake.cell_h) : 0.0f;
|
||||
}
|
||||
|
||||
f32 TrueTypeFont::ascent() const {
|
||||
return impl_ != nullptr ? static_cast<f32>(impl_->bake.baseline) : 0.0f;
|
||||
}
|
||||
|
||||
f32 TrueTypeFont::em_px() const { return impl_ != nullptr ? impl_->px : 0.0f; }
|
||||
|
||||
const render::Glyph* TrueTypeFont::glyph(u32 codepoint) const {
|
||||
if (impl_ == nullptr) {
|
||||
return nullptr; // no face: the legal "no text" backend, drawn as nothing
|
||||
}
|
||||
const Bake& bake = impl_->bake;
|
||||
const auto found = std::lower_bound(bake.codepoints.begin(), bake.codepoints.end(), codepoint);
|
||||
if (found != bake.codepoints.end() && *found == codepoint) {
|
||||
return &bake.glyphs[static_cast<std::size_t>(found - bake.codepoints.begin())];
|
||||
}
|
||||
return &bake.fallback;
|
||||
}
|
||||
|
||||
Vec2 TrueTypeFont::cell_uv() const {
|
||||
if (impl_ == nullptr || impl_->bake.atlas_w == 0u) {
|
||||
return Vec2{};
|
||||
}
|
||||
return Vec2{static_cast<f32>(impl_->bake.cell_w) / static_cast<f32>(impl_->bake.atlas_w),
|
||||
static_cast<f32>(impl_->bake.cell_h) / static_cast<f32>(impl_->bake.atlas_h)};
|
||||
}
|
||||
|
||||
const u8* TrueTypeFont::atlas_pixels() const {
|
||||
if (impl_ == nullptr || impl_->bake.atlas.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
return impl_->bake.atlas.data();
|
||||
}
|
||||
|
||||
u32 TrueTypeFont::atlas_width() const { return impl_ != nullptr ? impl_->bake.atlas_w : 0u; }
|
||||
|
||||
u32 TrueTypeFont::atlas_height() const { return impl_ != nullptr ? impl_->bake.atlas_h : 0u; }
|
||||
|
||||
u32 TrueTypeFont::revision() const { return impl_ != nullptr ? impl_->revision : 0u; }
|
||||
|
||||
u32 TrueTypeFont::glyph_count() const noexcept {
|
||||
return impl_ != nullptr ? static_cast<u32>(impl_->bake.glyphs.size()) + 1u : 0u;
|
||||
}
|
||||
|
||||
u32 TrueTypeFont::cell_width() const noexcept {
|
||||
return impl_ != nullptr ? impl_->bake.cell_w : 0u;
|
||||
}
|
||||
|
||||
u32 TrueTypeFont::cell_height() const noexcept {
|
||||
return impl_ != nullptr ? impl_->bake.cell_h : 0u;
|
||||
}
|
||||
|
||||
} // namespace plot::text
|
||||
Reference in new issue
Block a user