Build and use 1-bit font, saves a good amount of space

This commit is contained in:
Dave Allie 2025-12-07 01:26:49 +11:00
parent eceffaa289
commit 79294f6b8f
No known key found for this signature in database
GPG Key ID: F2FDDB3AD8D0276F
10 changed files with 8482 additions and 25051 deletions

View File

@ -11,7 +11,7 @@ typedef struct {
uint8_t advanceX; ///< Distance to advance cursor (x axis) uint8_t advanceX; ///< Distance to advance cursor (x axis)
int16_t left; ///< X dist from cursor pos to UL corner int16_t left; ///< X dist from cursor pos to UL corner
int16_t top; ///< Y dist from cursor pos to UL corner int16_t top; ///< Y dist from cursor pos to UL corner
uint16_t compressedSize; ///< Size of the zlib-compressed font data. uint16_t dataLength; ///< Size of the font data.
uint32_t dataOffset; ///< Pointer into EpdFont->bitmap uint32_t dataOffset; ///< Pointer into EpdFont->bitmap
} EpdGlyph; } EpdGlyph;
@ -28,7 +28,6 @@ typedef struct {
const EpdGlyph* glyph; ///< Glyph array const EpdGlyph* glyph; ///< Glyph array
const EpdUnicodeInterval* intervals; ///< Valid unicode intervals for this font const EpdUnicodeInterval* intervals; ///< Valid unicode intervals for this font
uint32_t intervalCount; ///< Number of unicode intervals. uint32_t intervalCount; ///< Number of unicode intervals.
bool compressed; ///< Does this font use compressed glyph bitmaps?
uint8_t advanceY; ///< Newline distance (y axis) uint8_t advanceY; ///< Newline distance (y axis)
int ascender; ///< Maximal height of a glyph above the base line int ascender; ///< Maximal height of a glyph above the base line
int descender; ///< Maximal height of a glyph below the base line int descender; ///< Maximal height of a glyph below the base line

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,20 +7,18 @@ import math
import argparse import argparse
from collections import namedtuple from collections import namedtuple
# From: https://github.com/vroland/epdiy # Originally from https://github.com/vroland/epdiy
parser = argparse.ArgumentParser(description="Generate a header file from a font to be used with epdiy.") parser = argparse.ArgumentParser(description="Generate a header file from a font to be used with epdiy.")
parser.add_argument("name", action="store", help="name of the font.") parser.add_argument("name", action="store", help="name of the font.")
parser.add_argument("size", type=int, help="font size to use.") parser.add_argument("size", type=int, help="font size to use.")
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.") parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.")
parser.add_argument("--compress", dest="compress", action="store_true", help="compress glyph bitmaps.")
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.") parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
args = parser.parse_args() args = parser.parse_args()
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "compressed_size", "data_offset", "code_point"]) GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
font_stack = [freetype.Face(f) for f in args.fontstack] font_stack = [freetype.Face(f) for f in args.fontstack]
compress = args.compress
size = args.size size = args.size
font_name = args.name font_name = args.name
@ -148,19 +146,18 @@ for i_start, i_end in unvalidated_intervals:
intervals.append((start, i_end)) intervals.append((start, i_end))
for face in font_stack: for face in font_stack:
# shift by 6 bytes, because sizes are given as 6-bit fractions
# the display has about 150 dpi.
face.set_char_size(size << 6, size << 6, 150, 150) face.set_char_size(size << 6, size << 6, 150, 150)
total_size = 0 total_size = 0
total_packed = 0
all_glyphs = [] all_glyphs = []
for i_start, i_end in intervals: for i_start, i_end in intervals:
for code_point in range(i_start, i_end + 1): for code_point in range(i_start, i_end + 1):
face = load_glyph(code_point) face = load_glyph(code_point)
bitmap = face.glyph.bitmap bitmap = face.glyph.bitmap
pixels = []
# Build out 4-bit greyscale bitmap
pixels4g = []
px = 0 px = 0
for i, v in enumerate(bitmap.buffer): for i, v in enumerate(bitmap.buffer):
y = i / bitmap.width y = i / bitmap.width
@ -169,31 +166,49 @@ for i_start, i_end in intervals:
px = (v >> 4) px = (v >> 4)
else: else:
px = px | (v & 0xF0) px = px | (v & 0xF0)
pixels.append(px); pixels4g.append(px);
px = 0 px = 0
# eol # eol
if x == bitmap.width - 1 and bitmap.width % 2 > 0: if x == bitmap.width - 1 and bitmap.width % 2 > 0:
pixels.append(px) pixels4g.append(px)
px = 0 px = 0
packed = bytes(pixels); # Downsample to 1-bit bitmap - treat any non-zero as black
total_packed += len(packed) pixelsbw = []
compressed = packed px = 0
if compress: pitch = (bitmap.width // 2) + (bitmap.width % 2)
compressed = zlib.compress(packed) for localY in range(bitmap.rows):
for xx in range(bitmap.width):
px = px << 1
bm = pixels4g[localY * pitch + (xx // 2)]
if (xx & 1) == 0:
if bm & 0xF > 0:
px += 1
else:
if bm & 0xF0 > 0:
px += 1
if (localY * bitmap.width + xx) % 8 == 7:
pixelsbw.append(px)
px = 0
if (bitmap.width * bitmap.rows) % 8 != 0:
pixelsbw.append(px)
# Build output data
packed = bytes(pixelsbw)
glyph = GlyphProps( glyph = GlyphProps(
width = bitmap.width, width = bitmap.width,
height = bitmap.rows, height = bitmap.rows,
advance_x = norm_floor(face.glyph.advance.x), advance_x = norm_floor(face.glyph.advance.x),
left = face.glyph.bitmap_left, left = face.glyph.bitmap_left,
top = face.glyph.bitmap_top, top = face.glyph.bitmap_top,
compressed_size = len(compressed), data_length = len(packed),
data_offset = total_size, data_offset = total_size,
code_point = code_point, code_point = code_point,
) )
total_size += len(compressed) total_size += len(packed)
all_glyphs.append((glyph, compressed)) all_glyphs.append((glyph, packed))
# pipe seems to be a good heuristic for the "real" descender # pipe seems to be a good heuristic for the "real" descender
face = load_glyph(ord('|')) face = load_glyph(ord('|'))
@ -201,11 +216,11 @@ face = load_glyph(ord('|'))
glyph_data = [] glyph_data = []
glyph_props = [] glyph_props = []
for index, glyph in enumerate(all_glyphs): for index, glyph in enumerate(all_glyphs):
props, compressed = glyph props, packed = glyph
glyph_data.extend([b for b in compressed]) glyph_data.extend([b for b in packed])
glyph_props.append(props) glyph_props.append(props)
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * compressed: {compress}\n */") print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n */")
print("#pragma once") print("#pragma once")
print("#include \"EpdFontData.h\"\n") print("#include \"EpdFontData.h\"\n")
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{") print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
@ -230,7 +245,6 @@ print(f" {font_name}Bitmaps,")
print(f" {font_name}Glyphs,") print(f" {font_name}Glyphs,")
print(f" {font_name}Intervals,") print(f" {font_name}Intervals,")
print(f" {len(intervals)},") print(f" {len(intervals)},")
print(f" {1 if compress else 0},")
print(f" {norm_ceil(face.size.height)},") print(f" {norm_ceil(face.size.height)},")
print(f" {norm_ceil(face.size.ascender)},") print(f" {norm_ceil(face.size.ascender)},")
print(f" {norm_floor(face.size.descender)},") print(f" {norm_floor(face.size.descender)},")

View File

@ -2,13 +2,10 @@
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <Utf8.h> #include <Utf8.h>
#include <miniz.h>
inline int min(const int a, const int b) { return a < b ? a : b; } inline int min(const int a, const int b) { return a < b ? a : b; }
inline int max(const int a, const int b) { return a > b ? a : b; } inline int max(const int a, const int b) { return a > b ? a : b; }
static tinfl_decompressor decomp;
template <typename Renderable> template <typename Renderable>
class EpdFontRenderer { class EpdFontRenderer {
Renderable& renderer; Renderable& renderer;
@ -22,22 +19,6 @@ class EpdFontRenderer {
void renderString(const char* string, int* x, int* y, uint16_t color, EpdFontStyle style = REGULAR); void renderString(const char* string, int* x, int* y, uint16_t color, EpdFontStyle style = REGULAR);
}; };
inline int uncompress(uint8_t* dest, size_t uncompressedSize, const uint8_t* source, size_t sourceSize) {
if (uncompressedSize == 0 || dest == nullptr || sourceSize == 0 || source == nullptr) {
return -1;
}
tinfl_init(&decomp);
// we know everything will fit into the buffer.
const tinfl_status decomp_status =
tinfl_decompress(&decomp, source, &sourceSize, dest, dest, &uncompressedSize,
TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
if (decomp_status != TINFL_STATUS_DONE) {
return decomp_status;
}
return 0;
}
template <typename Renderable> template <typename Renderable>
void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const uint16_t color, void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const uint16_t color,
const EpdFontStyle style) { const EpdFontStyle style) {
@ -79,49 +60,23 @@ void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const in
const uint8_t height = glyph->height; const uint8_t height = glyph->height;
const int left = glyph->left; const int left = glyph->left;
const int byteWidth = width / 2 + width % 2;
const unsigned long bitmapSize = byteWidth * height;
const uint8_t* bitmap = nullptr; const uint8_t* bitmap = nullptr;
if (fontFamily->getData(style)->compressed) {
auto* tmpBitmap = static_cast<uint8_t*>(malloc(bitmapSize));
if (tmpBitmap == nullptr && bitmapSize) {
Serial.println("Failed to allocate memory for decompression buffer");
return;
}
uncompress(tmpBitmap, bitmapSize, &fontFamily->getData(style)->bitmap[offset], glyph->compressedSize);
bitmap = tmpBitmap;
} else {
bitmap = &fontFamily->getData(style)->bitmap[offset]; bitmap = &fontFamily->getData(style)->bitmap[offset];
}
if (bitmap != nullptr) { if (bitmap != nullptr) {
for (int localY = 0; localY < height; localY++) { for (int glyphY = 0; glyphY < height; glyphY++) {
int yy = *y - glyph->top + localY; int screenY = *y - glyph->top + glyphY;
const int startPos = *x + left; for (int glyphX = 0; glyphX < width; glyphX++) {
bool byteComplete = startPos % 2; const int pixelPosition = glyphY * width + glyphX;
int localX = max(0, -startPos); int screenX = *x + left + glyphX;
const int maxX = startPos + width;
for (int xx = startPos; xx < maxX; xx++) { const uint8_t byte = bitmap[pixelPosition / 8];
uint8_t bm = bitmap[localY * byteWidth + localX / 2]; const uint8_t bit_index = 7 - (pixelPosition % 8);
if ((localX & 1) == 0) {
bm = bm & 0xF;
} else {
bm = bm >> 4;
}
if (bm) { if ((byte >> bit_index) & 1) {
renderer.drawPixel(xx, yy, color); renderer.drawPixel(screenX, screenY, color);
}
byteComplete = !byteComplete;
localX++;
} }
} }
if (fontFamily->getData(style)->compressed) {
free(const_cast<uint8_t*>(bitmap));
} }
} }