fix(text): carry a glyph's negative bearing and harden the bake's extents
This commit is contained in:
1 parent
b0f9fbbb35
commit
d853db62ae
5 files changed
+470
-60
No files matched your search
+63
-20
@@ -10,11 +10,15 @@
|
||||
// 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.
|
||||
// never leave the cell, and neither may a glyph's ink, in *either* direction: the
|
||||
// cell also steps right of the pen by the deepest backward overhang any covered
|
||||
// glyph has, because an italic glyph's ink starts left of its pen, and a cell
|
||||
// whose left edge was the pen would clip that overhang away without saying so.
|
||||
// The cell is 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, and the
|
||||
// baseline sits `ascent()` below its top, so every glyph's ink is blitted at the
|
||||
// offset its bitmap box reports from the pen, into a cell it cannot leave, and
|
||||
// every rect stays one size.
|
||||
|
||||
#include <plot/text/true_type_font.hpp>
|
||||
|
||||
@@ -46,6 +50,21 @@ constexpr f32 kFallbackInnerY0 = 0.0f;
|
||||
constexpr f32 kFallbackInnerX1 = 480.0f;
|
||||
constexpr f32 kFallbackInnerY1 = 600.0f;
|
||||
|
||||
/// The largest texel extent and the largest em this backend will compute with.
|
||||
///
|
||||
/// Both exist to keep a float-to-integer conversion inside its target's range --
|
||||
/// [conv.fpint] makes one outside it undefined, and `from_file()` accepts any
|
||||
/// positive `px`, so without a bound a caller could hand the bake a number whose
|
||||
/// floor or ceiling does not fit in the `int`/`u32` the cell is computed in. The
|
||||
/// em ceiling is the tighter one on purpose: it also keeps the *vendored*
|
||||
/// rasteriser's own pixel arithmetic inside `int`, since stb scales a glyph's
|
||||
/// outline by the same factor and narrows it to pixels itself. A million-pixel em
|
||||
/// is already a thousand times any legible bake, so nothing real is refused: an
|
||||
/// over-sized request still gets a `Status`, from the atlas-size check rather than
|
||||
/// from here (see `bake_face`).
|
||||
constexpr f32 kMaxTexels = 1.0e9f;
|
||||
constexpr f32 kMaxEmPx = 1.0e7f;
|
||||
|
||||
/// 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 {
|
||||
@@ -153,9 +172,13 @@ u32 blit_cell(std::vector<u8>& atlas, const Bake& bake, u32 slot, const u8* bitm
|
||||
/// 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)); };
|
||||
/// `.notdef` too. It is placed in the same coordinates the glyphs are -- font
|
||||
/// units from the pen, into a cell that sits `shift` texels right of it.
|
||||
void blit_fallback_box(std::vector<u8>& atlas, const Bake& bake, u32 slot, f32 scale,
|
||||
u32 shift) noexcept {
|
||||
const auto to_x = [scale, shift](f32 units) {
|
||||
return static_cast<i32>(std::lround(units * scale)) + static_cast<i32>(shift);
|
||||
};
|
||||
const auto to_y = [&bake, scale](f32 units) {
|
||||
return bake.baseline - static_cast<i32>(std::lround(units * scale));
|
||||
};
|
||||
@@ -212,6 +235,7 @@ bool bake_face(const stbtt_fontinfo& info, f32 scale, std::span<const u32> cover
|
||||
f32 widest_advance = 0.0f;
|
||||
i32 highest = 0;
|
||||
i32 lowest = 0;
|
||||
i32 leftmost = 0;
|
||||
i32 rightmost = 0;
|
||||
for (const u32 codepoint : requested) {
|
||||
if (codepoint > 0x10FFFFu) {
|
||||
@@ -233,6 +257,7 @@ bool bake_face(const stbtt_fontinfo& info, f32 scale, std::span<const u32> cover
|
||||
widest_advance = std::max(widest_advance, advance);
|
||||
highest = std::min(highest, y0);
|
||||
lowest = std::max(lowest, y1);
|
||||
leftmost = std::min(leftmost, x0);
|
||||
rightmost = std::max(rightmost, x1);
|
||||
drawable.push_back(Drawable{codepoint, advance});
|
||||
}
|
||||
@@ -241,16 +266,26 @@ bool bake_face(const stbtt_fontinfo& info, f32 scale, std::span<const u32> cover
|
||||
// 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. Each extent is
|
||||
// clamped before it is narrowed to texels: a float-to-unsigned conversion of a
|
||||
// negative value is undefined, and a face whose metrics or ink are degenerate
|
||||
// enough to produce one must land on a one-texel cell, not on undefined
|
||||
// behaviour.
|
||||
// clamped at both ends before it is narrowed to texels: a float-to-integer
|
||||
// conversion outside the target type's range is undefined, and neither a
|
||||
// degenerate face nor an absurd em size may reach that -- they land on a
|
||||
// one-texel cell or on the atlas-size refusal below instead.
|
||||
const auto ceil_to_texels = [](f32 value) {
|
||||
return static_cast<u32>(std::ceil(std::max(0.0f, value - 1e-4f)));
|
||||
return static_cast<u32>(std::ceil(std::min(std::max(0.0f, value - 1e-4f), kMaxTexels)));
|
||||
};
|
||||
const u32 cell_w =
|
||||
std::max<u32>(1u, ceil_to_texels(std::max(widest_advance, static_cast<f32>(rightmost))));
|
||||
i32 baseline = static_cast<i32>(std::floor(ascent_f));
|
||||
// The cell sits `shift` texels right of the pen, because the pen is where the
|
||||
// *advance* starts and not where the ink does: an italic glyph reaches left of
|
||||
// its pen (`U+006A` in half the italic faces on a stock Linux box), and a cell
|
||||
// whose left edge was the pen would drop that overhang -- silently, since the
|
||||
// trimmed bitmap stb returns is exactly what would be clipped. Widening the
|
||||
// same way the right edge is widened is what makes the rect uniform *and*
|
||||
// lossless; `-shift` is then the non-positive bearing `glyph()` reports, and
|
||||
// the pipeline places the cell corner at the pen plus that.
|
||||
const u32 shift =
|
||||
leftmost < 0 ? static_cast<u32>(-static_cast<std::int64_t>(leftmost)) : 0u;
|
||||
const u32 cell_w = std::max<u32>(
|
||||
1u, shift + ceil_to_texels(std::max(widest_advance, static_cast<f32>(rightmost))));
|
||||
i32 baseline = static_cast<i32>(std::floor(std::min(ascent_f, kMaxTexels)));
|
||||
baseline = std::max(baseline, -highest); // room for the tallest ink above the pen
|
||||
baseline = std::max(baseline, 0);
|
||||
const u32 below = static_cast<u32>(std::max(0, baseline + lowest));
|
||||
@@ -288,7 +323,7 @@ bool bake_face(const stbtt_fontinfo& info, f32 scale, std::span<const u32> cover
|
||||
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};
|
||||
return render::Glyph{u0, 0.0f, u0 + du, dv, -static_cast<f32>(shift), 0.0f, advance};
|
||||
};
|
||||
|
||||
// Pass two: rasterise and blit. The offset `stbtt_GetCodepointBitmap` reports is
|
||||
@@ -304,8 +339,8 @@ bool bake_face(const stbtt_fontinfo& info, f32 scale, std::span<const u32> cover
|
||||
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);
|
||||
inked += blit_cell(out->atlas, *out, static_cast<u32>(i), bitmap, width, height,
|
||||
x + static_cast<i32>(shift), baseline + y);
|
||||
stbtt_FreeBitmap(bitmap, nullptr);
|
||||
}
|
||||
out->codepoints.push_back(glyph.codepoint);
|
||||
@@ -321,7 +356,7 @@ bool bake_face(const stbtt_fontinfo& info, f32 scale, std::span<const u32> cover
|
||||
// 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);
|
||||
blit_fallback_box(out->atlas, *out, fallback_slot, scale, shift);
|
||||
out->fallback = glyph_for_slot(fallback_slot, widest_advance);
|
||||
return true;
|
||||
}
|
||||
@@ -380,6 +415,14 @@ bool TrueTypeFont::from_file(const char* utf8_path, f32 px, TrueTypeFont* out, S
|
||||
return fail(st, ErrorCode::InvalidArgument,
|
||||
"a font em size must be a positive, finite number of pixels");
|
||||
}
|
||||
if (px > kMaxEmPx) {
|
||||
// Not merely pedantry: the rasteriser scales outlines by this number and
|
||||
// narrows them to pixels itself, so an em past this bound would overflow
|
||||
// arithmetic this backend does not control. A size this large is not a font
|
||||
// size, and saying so beats undefined behaviour (see kMaxEmPx).
|
||||
return fail(st, ErrorCode::InvalidArgument,
|
||||
"the font em size is larger than this backend will rasterise");
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path{utf8_path};
|
||||
|
||||
Reference in new issue
Block a user