mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
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
804 lines
25 KiB
C++
804 lines
25 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
#include <binfont/binfont.h>
|
|
#include <binfont_private.h>
|
|
|
|
#include <tactility/concurrent/mutex.h>
|
|
#include <tactility/memory.h>
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <initializer_list>
|
|
|
|
namespace {
|
|
|
|
constexpr size_t RECORD_HEADER_SIZE = 8;
|
|
constexpr size_t HEAD_MIN_SIZE = 48;
|
|
constexpr size_t CMAP_SUBTABLE_HEADER_SIZE = 16;
|
|
constexpr size_t GLYPH_HEADER_MAX_BYTES = 16;
|
|
|
|
enum CmapFormat : uint8_t {
|
|
CMAP_FORMAT_0 = 0,
|
|
CMAP_FORMAT_SPARSE = 1,
|
|
CMAP_FORMAT_0_TINY = 2,
|
|
CMAP_FORMAT_SPARSE_TINY = 3,
|
|
};
|
|
|
|
enum Compression : uint8_t {
|
|
COMPRESSION_NONE = 0,
|
|
COMPRESSION_RLE_PREFILTER = 1,
|
|
COMPRESSION_RLE = 2,
|
|
};
|
|
|
|
uint16_t read_u16(const uint8_t* p) {
|
|
return static_cast<uint16_t>(p[0] | (p[1] << 8));
|
|
}
|
|
|
|
int16_t read_i16(const uint8_t* p) {
|
|
return static_cast<int16_t>(read_u16(p));
|
|
}
|
|
|
|
uint32_t read_u32(const uint8_t* p) {
|
|
return static_cast<uint32_t>(p[0]) | (static_cast<uint32_t>(p[1]) << 8) | (static_cast<uint32_t>(p[2]) << 16) | (static_cast<uint32_t>(p[3]) << 24);
|
|
}
|
|
|
|
bool has_marker(const uint8_t* record, const char* marker) {
|
|
return memcmp(record + 4, marker, 4) == 0;
|
|
}
|
|
|
|
/** MSB-first bit reader. Bits past the end read as 0. */
|
|
class BitReader {
|
|
const uint8_t* data;
|
|
size_t sizeBits;
|
|
size_t position;
|
|
|
|
public:
|
|
BitReader(const uint8_t* data, size_t size, size_t bitOffset = 0) : data(data), sizeBits(size * 8), position(bitOffset) {}
|
|
|
|
uint32_t read(uint8_t bits) {
|
|
uint32_t value = 0;
|
|
for (uint8_t i = 0; i < bits; i++) {
|
|
value <<= 1;
|
|
if (position < sizeBits) {
|
|
value |= (data[position >> 3] >> (7 - (position & 7))) & 1U;
|
|
}
|
|
position++;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
int32_t readSigned(uint8_t bits) {
|
|
if (bits == 0) {
|
|
return 0;
|
|
}
|
|
const uint32_t value = read(bits);
|
|
if (value & (1U << (bits - 1))) {
|
|
return static_cast<int32_t>(value | (~0U << bits));
|
|
}
|
|
return static_cast<int32_t>(value);
|
|
}
|
|
};
|
|
|
|
/** Decoder for the modified I3BN RLE stream of lv_font_conv. */
|
|
class RleReader {
|
|
enum class State { Single, Repeated, Counter };
|
|
|
|
BitReader& bits;
|
|
uint8_t bpp;
|
|
State state = State::Single;
|
|
bool first = true;
|
|
uint8_t previous = 0;
|
|
uint32_t count = 0;
|
|
|
|
uint8_t readSingle() {
|
|
previous = static_cast<uint8_t>(bits.read(bpp));
|
|
state = State::Single;
|
|
return previous;
|
|
}
|
|
|
|
public:
|
|
RleReader(BitReader& bits, uint8_t bpp) : bits(bits), bpp(bpp) {}
|
|
|
|
uint8_t next() {
|
|
switch (state) {
|
|
case State::Single: {
|
|
const auto value = static_cast<uint8_t>(bits.read(bpp));
|
|
if (!first && value == previous) {
|
|
count = 0;
|
|
state = State::Repeated;
|
|
}
|
|
first = false;
|
|
previous = value;
|
|
return value;
|
|
}
|
|
case State::Repeated: {
|
|
count++;
|
|
if (bits.read(1) == 0) {
|
|
return readSingle();
|
|
}
|
|
if (count == 11) {
|
|
count = bits.read(6);
|
|
if (count != 0) {
|
|
state = State::Counter;
|
|
} else {
|
|
return readSingle();
|
|
}
|
|
}
|
|
return previous;
|
|
}
|
|
case State::Counter:
|
|
count--;
|
|
if (count == 0) {
|
|
return readSingle();
|
|
}
|
|
return previous;
|
|
}
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
struct Header {
|
|
uint16_t fontSize;
|
|
int16_t ascent;
|
|
int16_t descent;
|
|
uint16_t defaultAdvanceWidth;
|
|
uint16_t kerningScale;
|
|
uint8_t indexToLocFormat;
|
|
uint8_t glyphIdFormat;
|
|
uint8_t advanceWidthFormat;
|
|
uint8_t bpp;
|
|
uint8_t xyBits;
|
|
uint8_t whBits;
|
|
uint8_t advanceWidthBits;
|
|
uint8_t compression;
|
|
uint8_t subpixelMode;
|
|
int16_t underlinePosition;
|
|
uint16_t underlineThickness;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
struct BinFont {
|
|
Header header;
|
|
/** The whole file in memory mode, nullptr in stream mode */
|
|
const uint8_t* data;
|
|
bool ownsData;
|
|
/** Stream mode only: the file path, and the file while it's open (guarded by mutex) */
|
|
char* path;
|
|
FILE* file;
|
|
uint32_t sessionCount;
|
|
Mutex mutex;
|
|
/** Owned copy of the cmap/loca/kern records in stream mode */
|
|
uint8_t* tables;
|
|
/** Each record pointer includes its 8-byte record header */
|
|
const uint8_t* cmap;
|
|
size_t cmapSize;
|
|
const uint8_t* loca;
|
|
size_t locaSize;
|
|
uint32_t locaCount;
|
|
const uint8_t* kern;
|
|
size_t kernSize;
|
|
size_t glyfOffset;
|
|
size_t glyfSize;
|
|
};
|
|
|
|
namespace {
|
|
|
|
bool read_at(BinFont* font, size_t offset, uint8_t* out, size_t size) {
|
|
if (fseek(font->file, static_cast<long>(offset), SEEK_SET) != 0) {
|
|
return false;
|
|
}
|
|
return fread(out, 1, size, font->file) == size;
|
|
}
|
|
|
|
bool parse_header(const uint8_t* record, size_t size, Header& header) {
|
|
if (size < HEAD_MIN_SIZE || !has_marker(record, "head")) {
|
|
return false;
|
|
}
|
|
header.fontSize = read_u16(record + 14);
|
|
header.ascent = read_i16(record + 16);
|
|
header.descent = read_i16(record + 18);
|
|
header.defaultAdvanceWidth = read_u16(record + 30);
|
|
header.kerningScale = read_u16(record + 32);
|
|
header.indexToLocFormat = record[34];
|
|
header.glyphIdFormat = record[35];
|
|
header.advanceWidthFormat = record[36];
|
|
header.bpp = record[37];
|
|
header.xyBits = record[38];
|
|
header.whBits = record[39];
|
|
header.advanceWidthBits = record[40];
|
|
header.compression = record[41];
|
|
header.subpixelMode = record[42];
|
|
header.underlinePosition = read_i16(record + 44);
|
|
header.underlineThickness = read_u16(record + 46);
|
|
|
|
return header.bpp >= 1 && header.bpp <= 4 &&
|
|
header.compression <= COMPRESSION_RLE &&
|
|
header.subpixelMode == 0 &&
|
|
header.indexToLocFormat <= 1 &&
|
|
header.xyBits <= 16 && header.whBits <= 16 && header.advanceWidthBits <= 32;
|
|
}
|
|
|
|
bool validate_cmap(const uint8_t* cmap, size_t size) {
|
|
if (size < 12) {
|
|
return false;
|
|
}
|
|
const uint32_t count = read_u32(cmap + 8);
|
|
if (count > (size - 12) / CMAP_SUBTABLE_HEADER_SIZE) {
|
|
return false;
|
|
}
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
const uint8_t* subtable = cmap + 12 + i * CMAP_SUBTABLE_HEADER_SIZE;
|
|
const size_t data_offset = read_u32(subtable);
|
|
const size_t range_length = read_u16(subtable + 8);
|
|
const size_t entries = read_u16(subtable + 12);
|
|
size_t data_size;
|
|
switch (subtable[14]) {
|
|
case CMAP_FORMAT_0: data_size = range_length; break;
|
|
case CMAP_FORMAT_SPARSE: data_size = entries * 4; break;
|
|
case CMAP_FORMAT_0_TINY: data_size = 0; break;
|
|
case CMAP_FORMAT_SPARSE_TINY: data_size = entries * 2; break;
|
|
default: return false;
|
|
}
|
|
if (data_size > 0 && (data_offset > size || data_size > size - data_offset)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool validate_kern(const uint8_t* kern, size_t size, uint8_t glyphIdFormat) {
|
|
if (size < 16) {
|
|
return false;
|
|
}
|
|
const uint8_t format = kern[8];
|
|
if (format == 0) {
|
|
const size_t count = read_u32(kern + 12);
|
|
const size_t entry_size = (glyphIdFormat ? 4 : 2) + 1;
|
|
return count <= (size - 16) / entry_size;
|
|
} else if (format == 3) {
|
|
const size_t glyph_count = read_u16(kern + 12);
|
|
const size_t left_count = kern[14];
|
|
const size_t right_count = kern[15];
|
|
return 16 + glyph_count * 2 + left_count * right_count <= size;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Locates all tables. In memory mode the table pointers point into font->data, in stream mode
|
|
* the lookup tables are copied into font->tables.
|
|
*/
|
|
error_t parse_layout(BinFont* font, size_t fileSize) {
|
|
uint8_t record_header[RECORD_HEADER_SIZE];
|
|
uint8_t head[64];
|
|
|
|
auto read = [font](size_t offset, uint8_t* out, size_t size) {
|
|
if (font->data != nullptr) {
|
|
memcpy(out, font->data + offset, size);
|
|
return true;
|
|
}
|
|
return read_at(font, offset, out, size);
|
|
};
|
|
|
|
// head
|
|
if (fileSize < RECORD_HEADER_SIZE || !read(0, record_header, RECORD_HEADER_SIZE)) {
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
const size_t head_size = read_u32(record_header);
|
|
if (head_size < HEAD_MIN_SIZE || head_size > fileSize) {
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
if (!read(0, head, HEAD_MIN_SIZE) || !parse_header(head, HEAD_MIN_SIZE, font->header)) {
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
|
|
// cmap, loca, glyf and optionally kern follow each other
|
|
size_t offsets[4] = {};
|
|
size_t sizes[4] = {};
|
|
const char* markers[4] = { "cmap", "loca", "glyf", "kern" };
|
|
size_t offset = head_size;
|
|
for (int i = 0; i < 4; i++) {
|
|
if (offset > fileSize || fileSize - offset < RECORD_HEADER_SIZE || !read(offset, record_header, RECORD_HEADER_SIZE)) {
|
|
if (i == 3) {
|
|
break; // kern is optional
|
|
}
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
const size_t size = read_u32(record_header);
|
|
if (!has_marker(record_header, markers[i]) || size < RECORD_HEADER_SIZE || size > fileSize - offset) {
|
|
if (i == 3) {
|
|
break;
|
|
}
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
offsets[i] = offset;
|
|
sizes[i] = size;
|
|
offset += size;
|
|
}
|
|
|
|
font->glyfOffset = offsets[2];
|
|
font->glyfSize = sizes[2];
|
|
|
|
if (font->data != nullptr) {
|
|
font->cmap = font->data + offsets[0];
|
|
font->loca = font->data + offsets[1];
|
|
font->kern = sizes[3] ? font->data + offsets[3] : nullptr;
|
|
} else {
|
|
font->tables = static_cast<uint8_t*>(memory_alloc(sizes[0] + sizes[1] + sizes[3]));
|
|
if (font->tables == nullptr) {
|
|
return ERROR_OUT_OF_MEMORY;
|
|
}
|
|
uint8_t* cursor = font->tables;
|
|
for (int i : { 0, 1, 3 }) {
|
|
if (sizes[i] > 0 && !read_at(font, offsets[i], cursor, sizes[i])) {
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
cursor += sizes[i];
|
|
}
|
|
font->cmap = font->tables;
|
|
font->loca = font->tables + sizes[0];
|
|
font->kern = sizes[3] ? font->tables + sizes[0] + sizes[1] : nullptr;
|
|
}
|
|
font->cmapSize = sizes[0];
|
|
font->locaSize = sizes[1];
|
|
font->kernSize = sizes[3];
|
|
|
|
if (!validate_cmap(font->cmap, font->cmapSize)) {
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
|
|
if (font->locaSize < 12) {
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
font->locaCount = read_u32(font->loca + 8);
|
|
const size_t loca_entry_size = font->header.indexToLocFormat ? 4 : 2;
|
|
if (font->locaCount > (font->locaSize - 12) / loca_entry_size) {
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
|
|
if (font->kern != nullptr && !validate_kern(font->kern, font->kernSize, font->header.glyphIdFormat)) {
|
|
font->kern = nullptr;
|
|
font->kernSize = 0;
|
|
}
|
|
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
BinFont* create_font() {
|
|
auto* font = static_cast<BinFont*>(memory_calloc(1, sizeof(BinFont)));
|
|
if (font != nullptr) {
|
|
mutex_construct(&font->mutex);
|
|
}
|
|
return font;
|
|
}
|
|
|
|
uint32_t find_glyph_id(const BinFont* font, uint32_t codepoint) {
|
|
const uint8_t* cmap = font->cmap;
|
|
const uint32_t count = read_u32(cmap + 8);
|
|
|
|
// Find the last subtable that starts at or before the codepoint
|
|
uint32_t low = 0;
|
|
uint32_t high = count;
|
|
while (low < high) {
|
|
const uint32_t mid = (low + high) / 2;
|
|
if (read_u32(cmap + 12 + mid * CMAP_SUBTABLE_HEADER_SIZE + 4) <= codepoint) {
|
|
low = mid + 1;
|
|
} else {
|
|
high = mid;
|
|
}
|
|
}
|
|
if (low == 0) {
|
|
return 0;
|
|
}
|
|
|
|
const uint8_t* subtable = cmap + 12 + (low - 1) * CMAP_SUBTABLE_HEADER_SIZE;
|
|
const uint32_t data_offset = read_u32(subtable);
|
|
const uint32_t range_start = read_u32(subtable + 4);
|
|
const uint16_t range_length = read_u16(subtable + 8);
|
|
const uint16_t glyph_id_start = read_u16(subtable + 10);
|
|
const uint16_t entries = read_u16(subtable + 12);
|
|
const uint32_t relative = codepoint - range_start;
|
|
if (relative >= range_length) {
|
|
return 0;
|
|
}
|
|
|
|
switch (subtable[14]) {
|
|
case CMAP_FORMAT_0:
|
|
return glyph_id_start + cmap[data_offset + relative];
|
|
case CMAP_FORMAT_0_TINY:
|
|
return glyph_id_start + relative;
|
|
case CMAP_FORMAT_SPARSE:
|
|
case CMAP_FORMAT_SPARSE_TINY: {
|
|
const uint8_t* list = cmap + data_offset;
|
|
uint32_t list_low = 0;
|
|
uint32_t list_high = entries;
|
|
while (list_low < list_high) {
|
|
const uint32_t mid = (list_low + list_high) / 2;
|
|
const uint16_t value = read_u16(list + mid * 2);
|
|
if (value == relative) {
|
|
if (subtable[14] == CMAP_FORMAT_SPARSE_TINY) {
|
|
return glyph_id_start + mid;
|
|
}
|
|
return glyph_id_start + read_u16(list + entries * 2 + mid * 2);
|
|
} else if (value < relative) {
|
|
list_low = mid + 1;
|
|
} else {
|
|
list_high = mid;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/** Gets the offset and size of a glyph record, relative to the glyf data (after its record header). */
|
|
bool get_glyph_location(const BinFont* font, uint32_t glyphId, size_t& offset, size_t& size) {
|
|
if (glyphId == 0 || glyphId >= font->locaCount) {
|
|
return false;
|
|
}
|
|
const uint8_t* entries = font->loca + 12;
|
|
auto entry = [&](uint32_t index) -> size_t {
|
|
return font->header.indexToLocFormat ? read_u32(entries + index * 4) : read_u16(entries + index * 2);
|
|
};
|
|
const size_t start = entry(glyphId);
|
|
const size_t end = (glyphId + 1 < font->locaCount) ? entry(glyphId + 1) : font->glyfSize;
|
|
if (start < RECORD_HEADER_SIZE || end < start || end > font->glyfSize) {
|
|
return false;
|
|
}
|
|
offset = start;
|
|
size = end - start;
|
|
return true;
|
|
}
|
|
|
|
/** Reads from the font file in stream mode. Outside of a session, the file is opened for this read only. */
|
|
bool read_glyph_data(BinFont* font, size_t offset, uint8_t* out, size_t size) {
|
|
mutex_lock(&font->mutex);
|
|
const bool temporary = font->file == nullptr;
|
|
if (temporary) {
|
|
font->file = fopen(font->path, "rb");
|
|
}
|
|
const bool success = font->file != nullptr && read_at(font, offset, out, size);
|
|
if (temporary && font->file != nullptr) {
|
|
fclose(font->file);
|
|
font->file = nullptr;
|
|
}
|
|
mutex_unlock(&font->mutex);
|
|
return success;
|
|
}
|
|
|
|
/**
|
|
* Provides the record of a glyph. In memory mode it points into the font data, in stream mode it
|
|
* is read into buffer (bufferSize bytes) or, when that is too small, into an allocation returned
|
|
* via allocated (to be freed by the caller).
|
|
*/
|
|
bool get_glyph_record(BinFont* font, uint32_t glyphId, size_t maxSize, uint8_t* buffer, size_t bufferSize, const uint8_t*& record, size_t& size, uint8_t*& allocated) {
|
|
size_t offset;
|
|
allocated = nullptr;
|
|
if (!get_glyph_location(font, glyphId, offset, size)) {
|
|
return false;
|
|
}
|
|
if (font->data != nullptr) {
|
|
record = font->data + font->glyfOffset + offset;
|
|
return true;
|
|
}
|
|
|
|
if (size > maxSize) {
|
|
size = maxSize;
|
|
}
|
|
uint8_t* target = buffer;
|
|
if (size > bufferSize) {
|
|
allocated = static_cast<uint8_t*>(memory_alloc(size));
|
|
if (allocated == nullptr) {
|
|
return false;
|
|
}
|
|
target = allocated;
|
|
}
|
|
|
|
const bool success = read_glyph_data(font, font->glyfOffset + offset, target, size);
|
|
|
|
if (!success) {
|
|
memory_free(allocated);
|
|
allocated = nullptr;
|
|
return false;
|
|
}
|
|
record = target;
|
|
return true;
|
|
}
|
|
|
|
void parse_glyph(const Header& header, BitReader& reader, uint32_t glyphId, BinFontGlyph* out) {
|
|
uint32_t advance = header.advanceWidthBits ? reader.read(header.advanceWidthBits) : header.defaultAdvanceWidth;
|
|
if (header.advanceWidthFormat == 0) {
|
|
advance *= 16;
|
|
}
|
|
out->glyph_id = glyphId;
|
|
out->advance_x16 = advance;
|
|
out->x = static_cast<int16_t>(reader.readSigned(header.xyBits));
|
|
out->y = static_cast<int16_t>(reader.readSigned(header.xyBits));
|
|
out->width = static_cast<uint16_t>(reader.read(header.whBits));
|
|
out->height = static_cast<uint16_t>(reader.read(header.whBits));
|
|
}
|
|
|
|
/** Opens a file for reading and determines its size */
|
|
error_t open_file(const char* path, FILE*& file, size_t& size) {
|
|
file = fopen(path, "rb");
|
|
if (file == nullptr) {
|
|
return ERROR_NOT_FOUND;
|
|
}
|
|
if (fseek(file, 0, SEEK_END) != 0) {
|
|
fclose(file);
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
const long file_size = ftell(file);
|
|
if (file_size <= 0 || fseek(file, 0, SEEK_SET) != 0) {
|
|
fclose(file);
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
size = static_cast<size_t>(file_size);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
/** Loads the lookup tables from an open file, then closes it. Glyph data is read on demand. */
|
|
error_t open_streaming(const char* path, FILE* file, size_t size, BinFont** out) {
|
|
BinFont* font = create_font();
|
|
if (font == nullptr) {
|
|
fclose(file);
|
|
return ERROR_OUT_OF_MEMORY;
|
|
}
|
|
font->file = file;
|
|
|
|
const size_t path_size = strlen(path) + 1;
|
|
font->path = static_cast<char*>(memory_alloc(path_size));
|
|
if (font->path == nullptr) {
|
|
binfont_close(font);
|
|
return ERROR_OUT_OF_MEMORY;
|
|
}
|
|
memcpy(font->path, path, path_size);
|
|
|
|
const error_t error = parse_layout(font, size);
|
|
fclose(font->file);
|
|
font->file = nullptr;
|
|
if (error != ERROR_NONE) {
|
|
binfont_close(font);
|
|
return error;
|
|
}
|
|
*out = font;
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" {
|
|
|
|
error_t binfont_open_memory(const void* data, size_t size, bool take_ownership, BinFont** out) {
|
|
BinFont* font = create_font();
|
|
if (font == nullptr) {
|
|
if (take_ownership) {
|
|
memory_free(const_cast<void*>(data));
|
|
}
|
|
return ERROR_OUT_OF_MEMORY;
|
|
}
|
|
font->data = static_cast<const uint8_t*>(data);
|
|
font->ownsData = take_ownership;
|
|
|
|
const error_t error = parse_layout(font, size);
|
|
if (error != ERROR_NONE) {
|
|
binfont_close(font);
|
|
return error;
|
|
}
|
|
*out = font;
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t binfont_open_file(const char* path, BinFont** out) {
|
|
FILE* file;
|
|
size_t size;
|
|
const error_t open_error = open_file(path, file, size);
|
|
if (open_error != ERROR_NONE) {
|
|
return open_error;
|
|
}
|
|
|
|
const uint16_t memory_capability = memory_external_total() > 0 ? MEMORY_CAPABILITY_EXTERNAL : MEMORY_CAPABILITY_INTERNAL;
|
|
const MemoryPolicy policy = { .required = memory_capability, .desired = 0, .alignment = 0 };
|
|
auto* data = static_cast<uint8_t*>(memory_alloc_with_policy(size, &policy));
|
|
if (data == nullptr) {
|
|
return open_streaming(path, file, size, out);
|
|
}
|
|
|
|
const bool success = fread(data, 1, size, file) == size;
|
|
fclose(file);
|
|
if (!success) {
|
|
memory_free(data);
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
return binfont_open_memory(data, size, true, out);
|
|
}
|
|
|
|
error_t binfont_open_file_streaming(const char* path, BinFont** out) {
|
|
FILE* file;
|
|
size_t size;
|
|
const error_t open_error = open_file(path, file, size);
|
|
if (open_error != ERROR_NONE) {
|
|
return open_error;
|
|
}
|
|
return open_streaming(path, file, size, out);
|
|
}
|
|
|
|
error_t binfont_begin(BinFont* font) {
|
|
if (font->data != nullptr) {
|
|
return ERROR_NONE;
|
|
}
|
|
mutex_lock(&font->mutex);
|
|
if (font->sessionCount == 0) {
|
|
font->file = fopen(font->path, "rb");
|
|
if (font->file == nullptr) {
|
|
mutex_unlock(&font->mutex);
|
|
return ERROR_NOT_FOUND;
|
|
}
|
|
}
|
|
font->sessionCount++;
|
|
mutex_unlock(&font->mutex);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
void binfont_end(BinFont* font) {
|
|
if (font->data != nullptr) {
|
|
return;
|
|
}
|
|
mutex_lock(&font->mutex);
|
|
if (font->sessionCount > 0) {
|
|
font->sessionCount--;
|
|
if (font->sessionCount == 0) {
|
|
fclose(font->file);
|
|
font->file = nullptr;
|
|
}
|
|
}
|
|
mutex_unlock(&font->mutex);
|
|
}
|
|
|
|
void binfont_close(BinFont* font) {
|
|
if (font->ownsData) {
|
|
memory_free(const_cast<uint8_t*>(font->data));
|
|
}
|
|
if (font->file != nullptr) {
|
|
fclose(font->file);
|
|
}
|
|
memory_free(font->path);
|
|
memory_free(font->tables);
|
|
mutex_destruct(&font->mutex);
|
|
memory_free(font);
|
|
}
|
|
|
|
void binfont_get_metrics(const BinFont* font, BinFontMetrics* out) {
|
|
const Header& header = font->header;
|
|
out->size = header.fontSize;
|
|
out->ascent = header.ascent;
|
|
out->descent = header.descent;
|
|
out->line_height = static_cast<uint16_t>(header.ascent - header.descent);
|
|
out->base_line = static_cast<int16_t>(-header.descent);
|
|
out->underline_position = header.underlinePosition;
|
|
out->underline_thickness = header.underlineThickness;
|
|
out->bpp = header.bpp;
|
|
}
|
|
|
|
bool binfont_get_glyph(BinFont* font, uint32_t codepoint, BinFontGlyph* out) {
|
|
const uint32_t glyph_id = find_glyph_id(font, codepoint);
|
|
uint8_t buffer[GLYPH_HEADER_MAX_BYTES];
|
|
const uint8_t* record;
|
|
size_t size;
|
|
uint8_t* allocated;
|
|
if (!get_glyph_record(font, glyph_id, sizeof(buffer), buffer, sizeof(buffer), record, size, allocated)) {
|
|
return false;
|
|
}
|
|
BitReader reader(record, size);
|
|
parse_glyph(font->header, reader, glyph_id, out);
|
|
return true;
|
|
}
|
|
|
|
int32_t binfont_get_kerning_x16(BinFont* font, uint32_t left_glyph_id, uint32_t right_glyph_id) {
|
|
const uint8_t* kern = font->kern;
|
|
if (kern == nullptr) {
|
|
return 0;
|
|
}
|
|
|
|
int8_t value = 0;
|
|
if (kern[8] == 0) {
|
|
const uint32_t count = read_u32(kern + 12);
|
|
const uint8_t* pairs = kern + 16;
|
|
const bool wide = font->header.glyphIdFormat != 0;
|
|
const size_t pair_size = wide ? 4 : 2;
|
|
const uint32_t key = (left_glyph_id << 16) | right_glyph_id;
|
|
uint32_t low = 0;
|
|
uint32_t high = count;
|
|
while (low < high) {
|
|
const uint32_t mid = (low + high) / 2;
|
|
const uint8_t* pair = pairs + mid * pair_size;
|
|
const uint32_t left = wide ? read_u16(pair) : pair[0];
|
|
const uint32_t right = wide ? read_u16(pair + 2) : pair[1];
|
|
const uint32_t mid_key = (left << 16) | right;
|
|
if (mid_key == key) {
|
|
value = static_cast<int8_t>(pairs[count * pair_size + mid]);
|
|
break;
|
|
} else if (mid_key < key) {
|
|
low = mid + 1;
|
|
} else {
|
|
high = mid;
|
|
}
|
|
}
|
|
} else {
|
|
const uint16_t glyph_count = read_u16(kern + 12);
|
|
const uint8_t right_count = kern[15];
|
|
if (left_glyph_id >= glyph_count || right_glyph_id >= glyph_count) {
|
|
return 0;
|
|
}
|
|
const uint8_t* left_classes = kern + 16;
|
|
const uint8_t* right_classes = left_classes + glyph_count;
|
|
const uint8_t* values = right_classes + glyph_count;
|
|
const uint8_t left_class = left_classes[left_glyph_id];
|
|
const uint8_t right_class = right_classes[right_glyph_id];
|
|
if (left_class == 0 || right_class == 0 || left_class > kern[14] || right_class > right_count) {
|
|
return 0;
|
|
}
|
|
value = static_cast<int8_t>(values[(left_class - 1) * right_count + (right_class - 1)]);
|
|
}
|
|
|
|
return (static_cast<int32_t>(value) * font->header.kerningScale) >> 4;
|
|
}
|
|
|
|
error_t binfont_get_glyph_bitmap(BinFont* font, const BinFontGlyph* glyph, uint8_t* out, size_t stride) {
|
|
if (stride < glyph->width) {
|
|
return ERROR_INVALID_ARGUMENT;
|
|
}
|
|
if (glyph->width == 0 || glyph->height == 0) {
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
const uint8_t* record;
|
|
size_t size;
|
|
uint8_t* allocated;
|
|
if (!get_glyph_record(font, glyph->glyph_id, SIZE_MAX, nullptr, 0, record, size, allocated)) {
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
const Header& header = font->header;
|
|
const uint8_t bpp = header.bpp;
|
|
const size_t header_bits = header.advanceWidthBits + 2U * header.xyBits + 2U * header.whBits;
|
|
BitReader reader(record, size, header_bits);
|
|
|
|
// Raw pixel values first, so the XOR prefilter can work on the previous line
|
|
if (header.compression == COMPRESSION_NONE) {
|
|
for (uint16_t y = 0; y < glyph->height; y++) {
|
|
uint8_t* line = out + y * stride;
|
|
for (uint16_t x = 0; x < glyph->width; x++) {
|
|
line[x] = static_cast<uint8_t>(reader.read(bpp));
|
|
}
|
|
}
|
|
} else {
|
|
RleReader rle(reader, bpp);
|
|
const bool prefilter = header.compression == COMPRESSION_RLE_PREFILTER;
|
|
for (uint16_t y = 0; y < glyph->height; y++) {
|
|
uint8_t* line = out + y * stride;
|
|
const uint8_t* previous = line - stride;
|
|
for (uint16_t x = 0; x < glyph->width; x++) {
|
|
const uint8_t value = rle.next();
|
|
line[x] = (prefilter && y > 0) ? (value ^ previous[x]) : value;
|
|
}
|
|
}
|
|
}
|
|
|
|
const uint32_t max_value = (1U << bpp) - 1;
|
|
for (uint16_t y = 0; y < glyph->height; y++) {
|
|
uint8_t* line = out + y * stride;
|
|
for (uint16_t x = 0; x < glyph->width; x++) {
|
|
line[x] = static_cast<uint8_t>(line[x] * 255U / max_value);
|
|
}
|
|
}
|
|
|
|
memory_free(allocated);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
}
|