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
@@ -91,6 +91,16 @@ if(TARGET plot::font_ttf)
|
||||
target_compile_definitions(plot_tests PRIVATE
|
||||
PLOT_CORE_ARCHIVE="$<TARGET_FILE:plot_core>"
|
||||
PLOT_CMAKE_COMMAND="${CMAKE_COMMAND}")
|
||||
# The cases compare a bake against the rasteriser's own glyph bitmaps, which is
|
||||
# the only reference that can tell a complete cell from a clipped one, and for
|
||||
# that the test needs the vendored header's *declarations* (its implementation is
|
||||
# already in the binary through plot::font_ttf). This is the optional target's
|
||||
# own test, compiled only when that target exists, and it is the one target
|
||||
# outside src/text that sees third_party/: the isolation this project promises --
|
||||
# plot_core's archive and every public header under include/plot/ -- is what
|
||||
# plot_core_stays_free_of_stb_truetype keeps asserting, and it is unaffected by
|
||||
# which include paths a test binary has.
|
||||
target_include_directories(plot_tests PRIVATE "${CMAKE_SOURCE_DIR}/third_party")
|
||||
endif()
|
||||
|
||||
# Same switches the library is built with: the cost tests read the counters
|
||||
|
||||
+353
-23
@@ -24,6 +24,13 @@
|
||||
#include <plot/render/renderer.hpp>
|
||||
#include <plot/text/true_type_font.hpp>
|
||||
|
||||
// Declarations only, for the reference the bake is measured against: the vendored
|
||||
// rasteriser's own glyph bitmaps and boxes. Its implementation is already in this
|
||||
// binary -- plot_tests links plot::font_ttf, which compiles it in -- so this is a
|
||||
// call, not a second copy of third-party code (see tests/CMakeLists.txt for why
|
||||
// this one target has third_party/ on its include path).
|
||||
#include <stb_truetype.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
@@ -45,6 +52,21 @@ namespace {
|
||||
/// equal terms.
|
||||
constexpr const char* kAdwaitaMono = "/usr/share/fonts/Adwaita/AdwaitaMono-Regular.ttf";
|
||||
|
||||
/// Italic faces to look for, in the order the case prefers them: every one of
|
||||
/// these leaves some glyph's ink *left* of its pen (an oblique or italic design
|
||||
/// slants the outline over the advance that starts at the pen), which is the shape
|
||||
/// the cell's left edge has to accommodate. Distro paths differ, so the list names
|
||||
/// the layouts of both the Debian-style and the Arch-style font trees.
|
||||
constexpr const char* kItalicCandidates[] = {
|
||||
"/usr/share/fonts/TTF/DejaVuSerif-Italic.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSerif-Italic.ttf",
|
||||
"/usr/share/fonts/TTF/TypesetterSlab-Italic.ttf",
|
||||
"/usr/share/fonts/gsfonts/NimbusRoman-Italic.otf",
|
||||
"/usr/share/fonts/opentype/urw-base35/NimbusRoman-Italic.otf",
|
||||
"/usr/share/fonts/liberation/LiberationSerif-Italic.ttf",
|
||||
"/usr/share/fonts/TTF/LiberationSerif-Italic.ttf",
|
||||
};
|
||||
|
||||
/// The viewport the renderer case draws into. Smaller than the smoke suite's 640
|
||||
/// x 360 because every assertion there scans pixels one `glReadPixels` at a time.
|
||||
constexpr plot::u32 kW = 360;
|
||||
@@ -92,6 +114,161 @@ plot::u32 slot_of(const plot::text::TrueTypeFont& font, const plot::render::Glyp
|
||||
return static_cast<plot::u32>(std::lround(cells));
|
||||
}
|
||||
|
||||
/// The vendored rasteriser, driven by the test the way the backend drives it, so
|
||||
/// a bake can be compared against the glyphs it was *supposed* to produce.
|
||||
///
|
||||
/// This is the only reference that can tell a complete cell from a clipped one:
|
||||
/// the atlas is self-consistent either way, so a cell that quietly lost its
|
||||
/// leftmost ink column looks exactly like a cell that never had one. Nothing about
|
||||
/// the cell's size or placement is recomputed here -- that would only repeat the
|
||||
/// arithmetic under test -- the reference is stb_truetype's own bitmap and box.
|
||||
class ReferenceRasteriser {
|
||||
public:
|
||||
/// Reads `path` and parses it. False (with the case's skip reason printed by the
|
||||
/// caller) when the file is missing or the parser refuses it.
|
||||
bool open(const char* path) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) {
|
||||
return false;
|
||||
}
|
||||
bytes_.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
||||
if (bytes_.empty()) {
|
||||
return false;
|
||||
}
|
||||
const int offset = stbtt_GetFontOffsetForIndex(bytes_.data(), 0);
|
||||
return offset >= 0 && stbtt_InitFont(&info_, bytes_.data(), offset) != 0;
|
||||
}
|
||||
|
||||
/// The scale the backend uses for `px` em, which is the rasteriser's own.
|
||||
float scale(f32 px) const { return stbtt_ScaleForPixelHeight(&info_, px); }
|
||||
|
||||
/// The ink bitmap and box of `codepoint` at `scale`: the bytes the cell must
|
||||
/// hold, and where, in pixels from the pen. The caller frees it.
|
||||
plot::u8* bitmap(u32 codepoint, float scale, plot::i32* width, plot::i32* height, plot::i32* x, plot::i32* y) const {
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
int bx = 0;
|
||||
int by = 0;
|
||||
plot::u8* pixels = stbtt_GetCodepointBitmap(&info_, scale, scale, static_cast<int>(codepoint), &w, &h,
|
||||
&bx, &by);
|
||||
*width = w;
|
||||
*height = h;
|
||||
*x = bx;
|
||||
*y = by;
|
||||
return pixels;
|
||||
}
|
||||
|
||||
/// The ink box stb reports for `codepoint`: the *geometric* box, which is what
|
||||
/// the bake sizes its cells from and is worth more than the bitmap here -- a
|
||||
/// bitmap cannot say whether a codepoint has ink left of its pen.
|
||||
void box(u32 codepoint, float scale, plot::i32* x0, plot::i32* y0, plot::i32* x1, plot::i32* y1) const {
|
||||
int ix0 = 0;
|
||||
int iy0 = 0;
|
||||
int ix1 = 0;
|
||||
int iy1 = 0;
|
||||
stbtt_GetCodepointBitmapBox(&info_, static_cast<int>(codepoint), scale, scale, &ix0, &iy0,
|
||||
&ix1, &iy1);
|
||||
*x0 = ix0;
|
||||
*y0 = iy0;
|
||||
*x1 = ix1;
|
||||
*y1 = iy1;
|
||||
}
|
||||
|
||||
bool has_glyph(u32 codepoint) const {
|
||||
return stbtt_FindGlyphIndex(&info_, static_cast<int>(codepoint)) != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<plot::u8> bytes_;
|
||||
stbtt_fontinfo info_{};
|
||||
};
|
||||
|
||||
/// False, with `why` filled in, unless `codepoint`'s cell holds *exactly* the
|
||||
/// bitmap the rasteriser produced for it, placed where the glyph's bearing puts
|
||||
/// the pen: bit for bit, texel for texel, with no texel of the bitmap left outside
|
||||
/// the cell. A column dropped because the cell could not hold it (the negative
|
||||
/// bearing an italic glyph has), ink placed at the wrong offset, or a bake that
|
||||
/// lost its shape, all fail here, and none of them is visible to an assertion
|
||||
/// about ink being *present*.
|
||||
bool cell_holds_reference(const plot::text::TrueTypeFont& font, const ReferenceRasteriser& face,
|
||||
float scale, plot::u32 codepoint, std::string* why) {
|
||||
const plot::render::Glyph* glyph = font.glyph(codepoint);
|
||||
if (glyph == nullptr) {
|
||||
*why = "no glyph";
|
||||
return false;
|
||||
}
|
||||
plot::i32 width = 0;
|
||||
plot::i32 height = 0;
|
||||
plot::i32 ink_x = 0;
|
||||
plot::i32 ink_y = 0;
|
||||
plot::u8* bitmap = face.bitmap(codepoint, scale, &width, &height, &ink_x, &ink_y);
|
||||
if (bitmap == nullptr) {
|
||||
return true; // no ink to compare: a blank cell is the whole truth here
|
||||
}
|
||||
const plot::u8* atlas = font.atlas_pixels();
|
||||
const plot::u32 cell_w = font.cell_width();
|
||||
const plot::u32 cell_h = font.cell_height();
|
||||
const plot::u32 atlas_w = font.atlas_width();
|
||||
const plot::u32 slot = slot_of(font, *glyph);
|
||||
// Where the cell's own coordinates put the pen: the bearing is the cell corner's
|
||||
// offset from the pen, so the pen is that much *into* the cell.
|
||||
const plot::i32 pen_x = static_cast<plot::i32>(std::lround(-static_cast<double>(glyph->xoff)));
|
||||
const plot::i32 pen_y = static_cast<plot::i32>(std::lround(static_cast<double>(font.ascent())));
|
||||
bool ok = true;
|
||||
for (plot::i32 row = 0; row < height && ok; ++row) {
|
||||
for (plot::i32 column = 0; column < width; ++column) {
|
||||
const plot::i32 cell_x = pen_x + ink_x + column;
|
||||
const plot::i32 cell_y = pen_y + ink_y + row;
|
||||
plot::u8 held = 0;
|
||||
if (cell_x >= 0 && cell_y >= 0 && cell_x < static_cast<plot::i32>(cell_w) &&
|
||||
cell_y < static_cast<plot::i32>(cell_h)) {
|
||||
held = atlas[static_cast<std::size_t>(cell_y) * atlas_w +
|
||||
static_cast<std::size_t>(slot) * cell_w + static_cast<std::size_t>(cell_x)];
|
||||
}
|
||||
const plot::u8 want =
|
||||
bitmap[static_cast<std::size_t>(row) * static_cast<std::size_t>(width) +
|
||||
static_cast<std::size_t>(column)];
|
||||
if (held == want) {
|
||||
continue;
|
||||
}
|
||||
*why = "U+" + std::to_string(codepoint) + " bitmap texel (column " + std::to_string(column) +
|
||||
", row " + std::to_string(row) + ") is " + std::to_string(want) +
|
||||
" but its cell holds " + std::to_string(held) +
|
||||
(held == 0 && want != 0 ? " -- the ink was dropped" : " -- the ink is misplaced");
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
stbtt_FreeBitmap(bitmap, nullptr);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// `cell_holds_reference`, as a case asserts it: the diagnostic the reference
|
||||
/// produced is the failure message, because "which texel of which codepoint" is
|
||||
/// the whole value of the check.
|
||||
void expect_cell_holds_reference(const plot::text::TrueTypeFont& font,
|
||||
const ReferenceRasteriser& face, float scale,
|
||||
plot::u32 codepoint) {
|
||||
std::string why;
|
||||
if (!cell_holds_reference(font, face, scale, codepoint, &why)) {
|
||||
throw ::plottest::Failure{why};
|
||||
}
|
||||
}
|
||||
|
||||
/// The first italic face from `kItalicCandidates` that is installed and parses,
|
||||
/// with `face` opened on it. Null when none is: the case that wants one is about a
|
||||
/// shape only an italic face has, and says so rather than passing on a face that
|
||||
/// does not.
|
||||
const char* open_an_italic_face(ReferenceRasteriser* face) {
|
||||
for (const char* candidate : kItalicCandidates) {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::is_regular_file(candidate, ec) && face->open(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// A cheap order-sensitive signature of the ink in `area`: two frames that drew
|
||||
/// the same thing hash the same, and a frame whose glyphs came from another atlas
|
||||
/// -- or from another cell of the same atlas -- does not. The position is folded
|
||||
@@ -242,6 +419,18 @@ PLOT_TEST(ttf_backend_bakes_the_requested_coverage) {
|
||||
}
|
||||
}
|
||||
|
||||
// And the cells hold the glyphs the rasteriser produced, whole: for every
|
||||
// codepoint, the bitmap stb_truetype hands back -- itself trimmed to its ink, so
|
||||
// it is the one artefact that knows what the cell must contain -- must be in the
|
||||
// cell texel for texel, at the offset the glyph's bearing names. This is the
|
||||
// assertion that catches ink a cell boundary dropped, which no assertion about
|
||||
// ink being *present* can see.
|
||||
ReferenceRasteriser face;
|
||||
PLOT_CHECK(face.open(kAdwaitaMono));
|
||||
for (const plot::u32 codepoint : coverage) {
|
||||
expect_cell_holds_reference(font, face, face.scale(px), codepoint);
|
||||
}
|
||||
|
||||
// A codepoint outside the coverage is not blank: it draws the fallback box, and
|
||||
// it draws it as the same cell as every other glyph.
|
||||
const plot::render::Glyph* fallback = font.glyph(kUncovered);
|
||||
@@ -260,13 +449,62 @@ PLOT_TEST(ttf_backend_bakes_the_requested_coverage) {
|
||||
}
|
||||
|
||||
// Every way a font can fail to arrive: a path that is not there, one that is a
|
||||
// directory, a file that is not a font, an em size that is not positive, and a
|
||||
// bake whose coverage came out blank. Each reports through a `Status`, each
|
||||
// leaves the object it was handed exactly as it was -- the atlas, the cell, the
|
||||
// revision and every glyph pointer -- and none of them takes the process with it.
|
||||
// directory, a file that is not a font, an em size that is not positive or is
|
||||
// absurd, and a bake whose coverage came out blank. Each reports through a
|
||||
// `Status`, each leaves the object it was handed exactly as it was, and none of
|
||||
// them takes the process with it.
|
||||
//
|
||||
// It has two halves on purpose. The rejections that need no face run against a
|
||||
// default-constructed backend, whose untouched state is the fontless one -- so
|
||||
// they are exercised on a machine with no system face at all, which is a machine
|
||||
// this project supports. The stronger half -- a *loaded* font, with an atlas, a
|
||||
// cell and glyph pointers that must survive a refused re-point -- needs a face, and
|
||||
// says so by itself rather than taking the other half's coverage down with it.
|
||||
PLOT_TEST(ttf_failure_paths_report_status) {
|
||||
// --- without a face: what "untouched" means for an object with nothing to lose
|
||||
plot::text::TrueTypeFont fontless;
|
||||
const auto still_fontless = [&] {
|
||||
return fontless.glyph('A') == nullptr && fontless.glyph(plot::render::kInvalidCodepoint) ==
|
||||
nullptr &&
|
||||
fontless.atlas_pixels() == nullptr && fontless.atlas_width() == 0u &&
|
||||
fontless.atlas_height() == 0u && fontless.cell_width() == 0u &&
|
||||
fontless.cell_height() == 0u && fontless.glyph_count() == 0u &&
|
||||
fontless.revision() == 0u && fontless.em_px() == 0.0f &&
|
||||
fontless.measure("A") == 0.0f;
|
||||
};
|
||||
PLOT_CHECK(still_fontless());
|
||||
const auto refuses = [&](const char* what, const char* path, plot::f32 px) {
|
||||
plot::Status failure;
|
||||
PLOT_CHECK(!plot::text::TrueTypeFont::from_file(path, px, &fontless, &failure));
|
||||
PLOT_CHECK(!static_cast<bool>(failure));
|
||||
PLOT_CHECK(failure.code != plot::ErrorCode::Ok);
|
||||
PLOT_CHECK(failure.message != nullptr && failure.message[0] != '\0');
|
||||
PLOT_CHECK(still_fontless());
|
||||
std::printf("[fail-%s] %s\n", what, failure.message);
|
||||
};
|
||||
|
||||
refuses("missing", "/nonexistent/plot/no-such-font.ttf", 20.0f);
|
||||
refuses("directory", "/usr/share/fonts", 20.0f);
|
||||
// A real text file from this repository: it exists, it reads, and it is not a
|
||||
// font -- the case for a file the parser cannot accept.
|
||||
refuses("not-a-font", PLOT_SOURCE_DIR "/CMakeLists.txt", 20.0f);
|
||||
refuses("zero-px", kAdwaitaMono, 0.0f);
|
||||
refuses("negative-px", kAdwaitaMono, -3.0f);
|
||||
// Past the em ceiling the rasteriser's own pixel arithmetic would leave `int`,
|
||||
// so it is refused as out of range before it is scaled by anything.
|
||||
refuses("beyond-the-em-ceiling", kAdwaitaMono, 1.0e8f);
|
||||
// Nowhere to put the font is the caller's own mistake, refused before the path is
|
||||
// even looked at -- which is why a path that does not exist still serves here.
|
||||
plot::Status no_destination;
|
||||
PLOT_CHECK(!plot::text::TrueTypeFont::from_file("/nonexistent/plot/no-such-font.ttf", 20.0f,
|
||||
nullptr, &no_destination));
|
||||
PLOT_CHECK(!static_cast<bool>(no_destination));
|
||||
PLOT_CHECK(still_fontless());
|
||||
|
||||
// --- with a face: the state that must survive is a real one
|
||||
if (!have_adwaita_mono()) {
|
||||
skip("ttf_failure_paths_report_status", "no system Adwaita Mono to bake");
|
||||
skip("ttf_failure_paths_report_status",
|
||||
"no system Adwaita Mono, so the loaded-state half did not run");
|
||||
return;
|
||||
}
|
||||
plot::text::TrueTypeFont font;
|
||||
@@ -282,35 +520,127 @@ PLOT_TEST(ttf_failure_paths_report_status) {
|
||||
font.atlas_pixels() == atlas && font.cell_width() == cell_w &&
|
||||
font.glyph('A') == glyph;
|
||||
};
|
||||
|
||||
const auto rejects = [&](const char* what, const char* path, plot::f32 px) {
|
||||
const auto survives = [&](const char* what, const char* path, plot::f32 px) {
|
||||
plot::Status failure;
|
||||
PLOT_CHECK(!plot::text::TrueTypeFont::from_file(path, px, &font, &failure));
|
||||
PLOT_CHECK(!static_cast<bool>(failure));
|
||||
PLOT_CHECK(failure.code != plot::ErrorCode::Ok);
|
||||
PLOT_CHECK(failure.message != nullptr && failure.message[0] != '\0');
|
||||
PLOT_CHECK(unchanged());
|
||||
std::printf("[fail-%s] %s\n", what, failure.message);
|
||||
std::printf("[fail-with-a-face-%s] %s\n", what, failure.message);
|
||||
};
|
||||
|
||||
rejects("missing", "/nonexistent/plot/no-such-font.ttf", 20.0f);
|
||||
rejects("directory", "/usr/share/fonts", 20.0f);
|
||||
// A real text file from this repository: it exists, it reads, and it is not a
|
||||
// font -- the case for a file the parser cannot accept.
|
||||
rejects("not-a-font", PLOT_SOURCE_DIR "/CMakeLists.txt", 20.0f);
|
||||
rejects("zero-px", kAdwaitaMono, 0.0f);
|
||||
rejects("negative-px", kAdwaitaMono, -3.0f);
|
||||
survives("missing", "/nonexistent/plot/no-such-font.ttf", 20.0f);
|
||||
survives("directory", "/usr/share/fonts", 20.0f);
|
||||
survives("not-a-font", PLOT_SOURCE_DIR "/CMakeLists.txt", 20.0f);
|
||||
survives("zero-px", kAdwaitaMono, 0.0f);
|
||||
survives("negative-px", kAdwaitaMono, -3.0f);
|
||||
// Positive, but far below one texel of ink: the bake itself is what fails here,
|
||||
// which is the "a bake that produced no ink" path rather than a bad argument.
|
||||
rejects("blank-bake", kAdwaitaMono, 0.1f);
|
||||
|
||||
// And the one failure that is the caller's own: nowhere to put the font.
|
||||
plot::Status no_destination;
|
||||
PLOT_CHECK(!plot::text::TrueTypeFont::from_file(kAdwaitaMono, 20.0f, nullptr, &no_destination));
|
||||
PLOT_CHECK(!static_cast<bool>(no_destination));
|
||||
survives("blank-bake", kAdwaitaMono, 0.1f);
|
||||
PLOT_CHECK(!plot::text::TrueTypeFont::from_file(kAdwaitaMono, 20.0f, nullptr, &st));
|
||||
PLOT_CHECK(unchanged());
|
||||
}
|
||||
|
||||
// A face whose ink reaches *left* of the pen is the case a cell that started at
|
||||
// the pen cannot hold: an italic or oblique design slants its outlines over the
|
||||
// advance, so some glyphs' bitmap boxes begin at a negative x, and a bake that
|
||||
// placed such a bitmap at the pen would drop the columns that fell outside --
|
||||
// silently, because the bitmap the rasteriser hands over is already trimmed to its
|
||||
// ink, so a lost column looks exactly like a column that was never there.
|
||||
//
|
||||
// Two things are asserted, and both fail against a bake without the left widening:
|
||||
// every cell holds its glyph whole (bit for bit, against the rasteriser's own
|
||||
// bitmap), and the bearings say so -- a negative `xoff` is how the pipeline knows
|
||||
// to place the cell corner right of the pen. Which codepoints count as "left of
|
||||
// the pen" is decided by the *rasteriser's* boxes, not by the bake's bearings:
|
||||
// asking the code under test whether there was ink to clip would let a bake that
|
||||
// clipped it also claim there was nothing to clip.
|
||||
PLOT_TEST(ttf_negative_bearing_glyphs_keep_their_ink) {
|
||||
constexpr const char* kCase = "ttf_negative_bearing_glyphs_keep_their_ink";
|
||||
ReferenceRasteriser face;
|
||||
const char* path = open_an_italic_face(&face);
|
||||
if (path == nullptr) {
|
||||
skip(kCase, "no italic face from the candidate list is installed");
|
||||
return;
|
||||
}
|
||||
const plot::f32 px = 20.0f;
|
||||
plot::text::TrueTypeFont font;
|
||||
plot::Status st;
|
||||
PLOT_CHECK(plot::text::TrueTypeFont::from_file(path, px, &font, &st));
|
||||
const float scale = face.scale(px);
|
||||
|
||||
// Every covered codepoint, whole -- the same reference check the other cases
|
||||
// make, on a face where a cell boundary has something to drop.
|
||||
for (const plot::u32 codepoint : plot::text::TrueTypeFont::default_coverage()) {
|
||||
expect_cell_holds_reference(font, face, scale, codepoint);
|
||||
}
|
||||
|
||||
plot::u32 left_of_the_pen = 0;
|
||||
for (const plot::u32 codepoint : plot::text::TrueTypeFont::default_coverage()) {
|
||||
if (!face.has_glyph(codepoint)) {
|
||||
continue;
|
||||
}
|
||||
plot::i32 x0 = 0;
|
||||
plot::i32 y0 = 0;
|
||||
plot::i32 x1 = 0;
|
||||
plot::i32 y1 = 0;
|
||||
face.box(codepoint, scale, &x0, &y0, &x1, &y1);
|
||||
if (x0 >= 0) {
|
||||
continue;
|
||||
}
|
||||
++left_of_the_pen;
|
||||
const plot::render::Glyph* glyph = font.glyph(codepoint);
|
||||
PLOT_CHECK(glyph != nullptr);
|
||||
PLOT_CHECK(glyph->xoff < 0.0f); // the cell corner is right of the pen
|
||||
// ... by at least the overhang the rasteriser reports, so no ink is left outside.
|
||||
PLOT_CHECK(static_cast<plot::i32>(std::lround(-static_cast<double>(glyph->xoff))) >= -x0);
|
||||
// And the cell still reaches past the advance, not into it: the pen's own step
|
||||
// must stay inside the cell the rect declares.
|
||||
PLOT_CHECK(static_cast<plot::f32>(font.cell_width()) + glyph->xoff >= glyph->advance);
|
||||
}
|
||||
if (left_of_the_pen == 0u) {
|
||||
skip(kCase, "this italic face draws no covered glyph left of its pen");
|
||||
return;
|
||||
}
|
||||
std::printf("[italic] %s: %u covered codepoints have ink left of the pen\n", path,
|
||||
left_of_the_pen);
|
||||
}
|
||||
|
||||
// The two bounds on a bake's size, both of which refuse *before* allocating: an
|
||||
// atlas that would not fit is a bad argument rather than an allocation failure,
|
||||
// and an em past what this backend will rasterise is refused rather than narrowed
|
||||
// into an integer it does not fit (see kMaxEmPx in src/text/true_type_font.cpp).
|
||||
// The `InvalidArgument` code is what pins the order -- a bake that got as far as
|
||||
// allocating would report `OutOfMemory` instead.
|
||||
PLOT_TEST(ttf_refuses_a_bake_it_cannot_hold) {
|
||||
constexpr const char* kCase = "ttf_refuses_a_bake_it_cannot_hold";
|
||||
if (!have_adwaita_mono()) {
|
||||
skip(kCase, "no system Adwaita Mono to bake");
|
||||
return;
|
||||
}
|
||||
plot::text::TrueTypeFont font;
|
||||
plot::Status st;
|
||||
// A million-pixel em is a legal size and an impossible atlas: 101 cells of about
|
||||
// 5.1e5 x 1e6 texels. Refused as an argument, not as 5.1e13 bytes that failed to
|
||||
// allocate.
|
||||
PLOT_CHECK(!plot::text::TrueTypeFont::from_file(kAdwaitaMono, 1.0e6f, &font, &st));
|
||||
PLOT_CHECK(st.code == plot::ErrorCode::InvalidArgument);
|
||||
PLOT_CHECK(!static_cast<bool>(st));
|
||||
PLOT_CHECK(font.atlas_pixels() == nullptr);
|
||||
PLOT_CHECK(font.glyph_count() == 0u);
|
||||
std::printf("[bounds-1000000px] %s\n", st.message);
|
||||
// Past the em ceiling, before any of that arithmetic runs.
|
||||
PLOT_CHECK(!plot::text::TrueTypeFont::from_file(kAdwaitaMono, 1.0e8f, &font, &st));
|
||||
PLOT_CHECK(st.code == plot::ErrorCode::InvalidArgument);
|
||||
std::printf("[bounds-100000000px] %s\n", st.message);
|
||||
// And neither bound is over-eager: the sizes a plot really draws at still bake,
|
||||
// including one well past the embedded face's 13 px and the 20 px this suite
|
||||
// bakes elsewhere.
|
||||
PLOT_CHECK(plot::text::TrueTypeFont::from_file(kAdwaitaMono, 96.0f, &font, &st));
|
||||
PLOT_CHECK(font.cell_width() > 0u);
|
||||
PLOT_CHECK(font.glyph_count() == 101u);
|
||||
PLOT_CHECK(font.atlas_pixels() != nullptr);
|
||||
}
|
||||
|
||||
// A renderer draws with the backend it was given, and re-uploads when that
|
||||
// backend re-bakes. The same plot is drawn four times: with the embedded face,
|
||||
// with the TrueType bake at 20 px (a size the embedded bake does not have), with
|
||||
|
||||
Reference in new issue
Block a user