Ken Van Hoeylandt 7ea8c346b0
Various improvements (#673)
Graphics & rendering

- Implement `binfont-module` to generate bitmap fonts on the device itself
- Remove lodepng support from LVGL library and create `Modules/lodepng-module` to make the feature re-usable in non-LVGL apps too. Add binding functions for LVGL support.
- Implement large icons for boot screen (Tactility and USB logos)
- Remove unused asset files in `Data/`

Boot

- Split up `Tactility.cpp` and its `run()` function into various `Init*.cpp` files.
- Re-implement `Boot.cpp` app as non-LVGL app
- Boot app now has a `BootSequence.cpp` for the init code.
- LVGL is now started when the Boot app finishes
- Move more init logic into Boot app

BLE

- Don't allocate scan result buffers unless BLE is on
- Allocate scan results in external memory if possible

UX & UI

- USB Settings app: remove border from the button container
2026-10-04 18:09:21 +02:00

341 lines
13 KiB
C++

#include "doctest.h"
#include <binfont/binfont.h>
#include <binfont_private.h>
#include <binfont/generator.h>
#include <binfont/render.h>
#include <graphics/pixel_buffer.h>
#include <tactility/memory.h>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
namespace {
constexpr uint32_t ICON_CODEPOINTS[] = { 0xE5C3, 0xE8B8, 0xE2C7, 0xE63E, 0xE1A7 };
std::string fixture(const char* name) {
return std::string(BINFONT_TEST_FIXTURES_DIR) + "/" + name;
}
/** Loads a file into memory allocated with memory_alloc() */
uint8_t* load_file(const std::string& path, size_t& size) {
FILE* file = fopen(path.c_str(), "rb");
REQUIRE(file != nullptr);
fseek(file, 0, SEEK_END);
size = static_cast<size_t>(ftell(file));
fseek(file, 0, SEEK_SET);
auto* data = static_cast<uint8_t*>(memory_alloc(size));
REQUIRE(fread(data, 1, size, file) == size);
fclose(file);
return data;
}
BinFont* open_fixture_in_memory(const char* name) {
size_t size;
uint8_t* data = load_file(fixture(name), size);
BinFont* font = nullptr;
REQUIRE(binfont_open_memory(data, size, true, &font) == ERROR_NONE);
return font;
}
std::vector<uint8_t> decode(BinFont* font, const BinFontGlyph& glyph) {
std::vector<uint8_t> bitmap(static_cast<size_t>(glyph.width) * glyph.height);
REQUIRE(binfont_get_glyph_bitmap(font, &glyph, bitmap.data(), glyph.width) == ERROR_NONE);
return bitmap;
}
uint32_t sum(const std::vector<uint8_t>& bitmap) {
uint32_t total = 0;
for (uint8_t value : bitmap) {
total += value;
}
return total;
}
void check_same_glyph(const BinFontGlyph& a, const BinFontGlyph& b) {
CHECK_EQ(a.glyph_id, b.glyph_id);
CHECK_EQ(a.advance_x16, b.advance_x16);
CHECK_EQ(a.x, b.x);
CHECK_EQ(a.y, b.y);
CHECK_EQ(a.width, b.width);
CHECK_EQ(a.height, b.height);
}
} // namespace
TEST_CASE("binfont_open_file returns ERROR_NOT_FOUND for a missing file") {
BinFont* font = nullptr;
CHECK_EQ(binfont_open_file(fixture("missing.bin").c_str(), &font), ERROR_NOT_FOUND);
}
TEST_CASE("binfont_open_memory rejects data that is not a font") {
const uint8_t data[64] = { 1, 2, 3, 4 };
BinFont* font = nullptr;
CHECK_EQ(binfont_open_memory(data, sizeof(data), false, &font), ERROR_NOT_SUPPORTED);
}
TEST_CASE("binfont reads metrics, glyphs and bitmaps that match lv_font_conv's C output") {
BinFont* font = open_fixture_in_memory("text_kern_pairs.bin");
BinFontMetrics metrics;
binfont_get_metrics(font, &metrics);
CHECK_EQ(metrics.size, 20);
CHECK_EQ(metrics.line_height, 20);
CHECK_EQ(metrics.base_line, 5);
CHECK_EQ(metrics.ascent, 15);
CHECK_EQ(metrics.descent, -5);
CHECK_EQ(metrics.underline_position, 1);
CHECK_EQ(metrics.underline_thickness, 0);
CHECK_EQ(metrics.bpp, 4);
BinFontGlyph glyph;
REQUIRE(binfont_get_glyph(font, 'A', &glyph));
CHECK_EQ(glyph.glyph_id, 1);
CHECK_EQ(glyph.advance_x16, 195);
CHECK_EQ(glyph.x, 0);
CHECK_EQ(glyph.y, 0);
CHECK_EQ(glyph.width, 13);
CHECK_EQ(glyph.height, 14);
const auto bitmap = decode(font, glyph);
const std::vector<uint8_t> first_row = { 0, 0, 0, 0, 0, 238, 255, 17, 0, 0, 0, 0, 0 };
const std::vector<uint8_t> last_row = { 221, 255, 0, 0, 0, 0, 0, 0, 0, 0, 238, 238, 0 };
CHECK(std::vector<uint8_t>(bitmap.begin(), bitmap.begin() + 13) == first_row);
CHECK(std::vector<uint8_t>(bitmap.end() - 13, bitmap.end()) == last_row);
CHECK_EQ(sum(bitmap), 13974);
REQUIRE(binfont_get_glyph(font, 'V', &glyph));
CHECK_EQ(glyph.glyph_id, 22);
REQUIRE(binfont_get_glyph(font, 'z', &glyph));
CHECK_EQ(glyph.glyph_id, 52);
CHECK_FALSE(binfont_get_glyph(font, '0', &glyph));
CHECK_FALSE(binfont_get_glyph(font, 0x5B, &glyph));
CHECK_FALSE(binfont_get_glyph(font, 0x10FFFF, &glyph));
binfont_close(font);
}
TEST_CASE("binfont kerning is the same for the pairs and the classes format") {
BinFont* pairs = open_fixture_in_memory("text_kern_pairs.bin");
BinFont* classes = open_fixture_in_memory("text_kern_classes.bin");
CHECK_EQ(binfont_get_kerning_x16(pairs, 1, 22), -10);
CHECK_EQ(binfont_get_kerning_x16(classes, 1, 22), -10);
int non_zero = 0;
for (uint32_t left = 1; left <= 52; left++) {
for (uint32_t right = 1; right <= 52; right++) {
const int32_t value = binfont_get_kerning_x16(pairs, left, right);
CHECK_EQ(value, binfont_get_kerning_x16(classes, left, right));
if (value != 0) {
non_zero++;
}
}
}
CHECK(non_zero > 0);
CHECK_EQ(binfont_get_kerning_x16(pairs, 1, 1000), 0);
CHECK_EQ(binfont_get_kerning_x16(classes, 1, 1000), 0);
binfont_close(pairs);
binfont_close(classes);
}
TEST_CASE("binfont decodes identical bitmaps for raw, RLE and RLE with prefilter") {
for (const char* bpp : { "2", "4" }) {
CAPTURE(bpp);
BinFont* raw = open_fixture_in_memory((std::string("icons_raw_") + bpp + ".bin").c_str());
BinFont* rle = open_fixture_in_memory((std::string("icons_rle_") + bpp + ".bin").c_str());
BinFont* prefiltered = open_fixture_in_memory((std::string("icons_rlepf_") + bpp + ".bin").c_str());
for (uint32_t codepoint : ICON_CODEPOINTS) {
CAPTURE(codepoint);
BinFontGlyph raw_glyph, rle_glyph, prefiltered_glyph;
REQUIRE(binfont_get_glyph(raw, codepoint, &raw_glyph));
REQUIRE(binfont_get_glyph(rle, codepoint, &rle_glyph));
REQUIRE(binfont_get_glyph(prefiltered, codepoint, &prefiltered_glyph));
check_same_glyph(raw_glyph, rle_glyph);
check_same_glyph(raw_glyph, prefiltered_glyph);
const auto raw_bitmap = decode(raw, raw_glyph);
CHECK(sum(raw_bitmap) > 0);
CHECK(decode(rle, rle_glyph) == raw_bitmap);
CHECK(decode(prefiltered, prefiltered_glyph) == raw_bitmap);
}
binfont_close(raw);
binfont_close(rle);
binfont_close(prefiltered);
}
}
TEST_CASE("binfont gives the same results when streaming from a file") {
BinFont* memory_font = open_fixture_in_memory("icons_rlepf_4.bin");
BinFont* file_font = nullptr;
REQUIRE(binfont_open_file_streaming(fixture("icons_rlepf_4.bin").c_str(), &file_font) == ERROR_NONE);
for (uint32_t codepoint : ICON_CODEPOINTS) {
BinFontGlyph memory_glyph, file_glyph;
REQUIRE(binfont_get_glyph(memory_font, codepoint, &memory_glyph));
REQUIRE(binfont_get_glyph(file_font, codepoint, &file_glyph));
check_same_glyph(memory_glyph, file_glyph);
CHECK(decode(memory_font, memory_glyph) == decode(file_font, file_glyph));
}
binfont_close(memory_font);
binfont_close(file_font);
}
TEST_CASE("binfont_open_file loads the font into memory") {
const std::string path = std::string(BINFONT_TEST_TEMP_DIR) + "/binfont_memory_test.bin";
size_t size;
uint8_t* data = load_file(fixture("icons_rlepf_4.bin"), size);
FILE* copy = fopen(path.c_str(), "wb");
REQUIRE(copy != nullptr);
REQUIRE(fwrite(data, 1, size, copy) == size);
fclose(copy);
memory_free(data);
BinFont* font = nullptr;
REQUIRE(binfont_open_file(path.c_str(), &font) == ERROR_NONE);
REQUIRE(remove(path.c_str()) == 0);
BinFontGlyph glyph;
CHECK(binfont_get_glyph(font, ICON_CODEPOINTS[0], &glyph));
CHECK(sum(decode(font, glyph)) > 0);
binfont_close(font);
}
TEST_CASE("binfont only keeps a streamed font file open during a session") {
// An open file stays readable after it's deleted, so deleting it shows whether it's kept open
const std::string path = std::string(BINFONT_TEST_TEMP_DIR) + "/binfont_session_test.bin";
size_t size;
uint8_t* data = load_file(fixture("icons_rlepf_4.bin"), size);
FILE* copy = fopen(path.c_str(), "wb");
REQUIRE(copy != nullptr);
REQUIRE(fwrite(data, 1, size, copy) == size);
fclose(copy);
memory_free(data);
BinFont* font = nullptr;
REQUIRE(binfont_open_file_streaming(path.c_str(), &font) == ERROR_NONE);
BinFontGlyph glyph;
REQUIRE(binfont_get_glyph(font, ICON_CODEPOINTS[0], &glyph));
REQUIRE(binfont_begin(font) == ERROR_NONE);
REQUIRE(binfont_begin(font) == ERROR_NONE);
REQUIRE(remove(path.c_str()) == 0);
binfont_end(font);
// Still open: one session remains
CHECK(binfont_get_glyph(font, ICON_CODEPOINTS[1], &glyph));
CHECK_FALSE(decode(font, glyph).empty());
binfont_end(font);
// Closed: the deleted file can't be opened again
CHECK_FALSE(binfont_get_glyph(font, ICON_CODEPOINTS[2], &glyph));
CHECK_EQ(binfont_begin(font), ERROR_NOT_FOUND);
binfont_close(font);
}
TEST_CASE("binfont_get_glyph_bitmap rejects a stride smaller than the glyph width") {
BinFont* font = open_fixture_in_memory("icons_raw_2.bin");
BinFontGlyph glyph;
REQUIRE(binfont_get_glyph(font, ICON_CODEPOINTS[0], &glyph));
std::vector<uint8_t> bitmap(static_cast<size_t>(glyph.width) * glyph.height);
CHECK_EQ(binfont_get_glyph_bitmap(font, &glyph, bitmap.data(), glyph.width - 1), ERROR_INVALID_ARGUMENT);
binfont_close(font);
}
TEST_CASE("binfont_generate rasterizes a TTF close to lv_font_conv") {
uint32_t codepoints[] = { 0xE8B8, 0xE5C3, 0x41, 0xE1A7, 0xE2C7, 0xE63E, 0xE5C3 }; // unsorted, duplicate and missing entries
const BinFontGeneratorConfig config = {
.ttf_path = BINFONT_TEST_ICON_TTF,
.size = 24,
.bpp = 4,
.codepoints = codepoints,
.codepoint_count = sizeof(codepoints) / sizeof(codepoints[0]),
};
uint8_t* data = nullptr;
size_t size = 0;
REQUIRE(binfont_generate(&config, &data, &size) == ERROR_NONE);
BinFont* generated = nullptr;
REQUIRE(binfont_open_memory(data, size, true, &generated) == ERROR_NONE);
BinFont* reference = open_fixture_in_memory("icons_raw_4.bin");
BinFontMetrics generated_metrics, reference_metrics;
binfont_get_metrics(generated, &generated_metrics);
binfont_get_metrics(reference, &reference_metrics);
CHECK_EQ(generated_metrics.size, 24);
CHECK_EQ(generated_metrics.bpp, 4);
CHECK_EQ(generated_metrics.ascent, reference_metrics.ascent);
CHECK_EQ(generated_metrics.descent, reference_metrics.descent);
BinFontGlyph glyph;
CHECK_FALSE(binfont_get_glyph(generated, 0x41, &glyph));
for (uint32_t codepoint : ICON_CODEPOINTS) {
CAPTURE(codepoint);
BinFontGlyph generated_glyph, reference_glyph;
REQUIRE(binfont_get_glyph(generated, codepoint, &generated_glyph));
REQUIRE(binfont_get_glyph(reference, codepoint, &reference_glyph));
CHECK(std::abs(static_cast<int>(generated_glyph.advance_x16) - static_cast<int>(reference_glyph.advance_x16)) <= 16);
CHECK(std::abs(generated_glyph.x - reference_glyph.x) <= 1);
CHECK(std::abs(generated_glyph.y - reference_glyph.y) <= 1);
CHECK(std::abs(generated_glyph.width - reference_glyph.width) <= 1);
CHECK(std::abs(generated_glyph.height - reference_glyph.height) <= 1);
const auto generated_sum = static_cast<double>(sum(decode(generated, generated_glyph)));
const auto reference_sum = static_cast<double>(sum(decode(reference, reference_glyph)));
CHECK(generated_sum > reference_sum * 0.9);
CHECK(generated_sum < reference_sum * 1.1);
}
binfont_close(generated);
binfont_close(reference);
}
TEST_CASE("binfont_generate fails for a missing TTF") {
const uint32_t codepoint = 0xE5C3;
const BinFontGeneratorConfig config = {
.ttf_path = BINFONT_TEST_FIXTURES_DIR "/missing.ttf",
.size = 24,
.bpp = 2,
.codepoints = &codepoint,
.codepoint_count = 1,
};
uint8_t* data = nullptr;
size_t size = 0;
CHECK_EQ(binfont_generate(&config, &data, &size), ERROR_NOT_FOUND);
}
TEST_CASE("binfont draws glyphs and text into a PixelBuffer") {
BinFont* font = open_fixture_in_memory("text_kern_pairs.bin");
PixelBuffer* buffer = pixel_buffer_create(DISPLAY_COLOR_FORMAT_RGB565, 64, 24);
REQUIRE(buffer != nullptr);
pixel_buffer_clear(buffer);
const int advance = binfont_draw_glyph_rgb565(buffer, 0, 18, font, 'A', 0xFFFF, PIXEL_BUFFER_CONVERSION_EXACT_BLACK);
CHECK_EQ(advance, (195 + 8) >> 4);
// Top of 'A' (row 0 of the glyph, column 6 is fully opaque) sits 14 rows above the baseline
CHECK_EQ(pixel_buffer_get_pixel_rgb565(buffer, 6, 18 - 14), 0xFFFF);
CHECK_EQ(pixel_buffer_get_pixel_rgb565(buffer, 0, 0), 0x0000);
CHECK_EQ(binfont_draw_glyph_rgb565(buffer, 0, 18, font, '0', 0xFFFF, PIXEL_BUFFER_CONVERSION_EXACT_BLACK), 0);
// "AV" is kerned: 195 - 10 + V's advance
BinFontGlyph v;
REQUIRE(binfont_get_glyph(font, 'V', &v));
pixel_buffer_clear(buffer);
const int width = binfont_draw_text_rgb565(buffer, 0, 18, font, "AV", 0xFFFF, PIXEL_BUFFER_CONVERSION_EXACT_BLACK);
CHECK_EQ(width, static_cast<int>((195 - 10 + v.advance_x16 + 8) >> 4));
pixel_buffer_free(buffer);
binfont_close(font);
}