Compare commits

...

3 Commits

Author SHA1 Message Date
Dave Allie
551e57295f
Alloc/free inflator once per drawText call 2026-01-05 23:13:17 +11:00
Dave Allie
c9cfedf3d6
Do not include ZLIB header or checksum bytes in compressed glyphs 2026-01-05 22:58:42 +11:00
Dave Allie
cb3e08e73c
Basic glyph compression 2026-01-05 22:54:09 +11:00
57 changed files with 198634 additions and 249536 deletions

View File

@ -5,6 +5,7 @@
#include <cstdint> #include <cstdint>
/// Font data stored PER GLYPH /// Font data stored PER GLYPH
#pragma pack(push, 1)
typedef struct { typedef struct {
uint8_t width; ///< Bitmap dimensions in pixels uint8_t width; ///< Bitmap dimensions in pixels
uint8_t height; ///< Bitmap dimensions in pixels uint8_t height; ///< Bitmap dimensions in pixels
@ -13,7 +14,9 @@ typedef struct {
int16_t top; ///< Y dist from cursor pos to UL corner int16_t top; ///< Y dist from cursor pos to UL corner
uint16_t dataLength; ///< Size of the font data. uint16_t dataLength; ///< Size of the font data.
uint32_t dataOffset; ///< Pointer into EpdFont->bitmap uint32_t dataOffset; ///< Pointer into EpdFont->bitmap
bool compressed;
} EpdGlyph; } EpdGlyph;
#pragma pack(pop)
/// Glyph interval structure /// Glyph interval structure
typedef struct { typedef struct {

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

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

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

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

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

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

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

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

@ -17,7 +17,7 @@ parser.add_argument("--2bit", dest="is2Bit", action="store_true", help="generate
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", "data_length", "data_offset", "code_point"]) GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "compressed", "code_point"])
font_stack = [freetype.Face(f) for f in args.fontstack] font_stack = [freetype.Face(f) for f in args.fontstack]
is2Bit = args.is2Bit is2Bit = args.is2Bit
@ -124,7 +124,6 @@ def load_glyph(code_point):
face.load_glyph(glyph_index, freetype.FT_LOAD_RENDER) face.load_glyph(glyph_index, freetype.FT_LOAD_RENDER)
return face return face
face_index += 1 face_index += 1
print(f"code point {code_point} ({hex(code_point)}) not found in font stack!", file=sys.stderr)
return None return None
unmerged_intervals = sorted(intervals + add_ints) unmerged_intervals = sorted(intervals + add_ints)
@ -242,6 +241,13 @@ for i_start, i_end in intervals:
# Build output data # Build output data
packed = bytes(pixels) packed = bytes(pixels)
# DEFLATE compressed data without zlib header/footer
compressed = zlib.compress(packed, wbits=-15)
is_compressed = len(compressed) < len(packed) * 0.9
# Use compressed data only if it's at least 10% smaller
if is_compressed:
packed = compressed
glyph = GlyphProps( glyph = GlyphProps(
width = bitmap.width, width = bitmap.width,
height = bitmap.rows, height = bitmap.rows,
@ -250,6 +256,7 @@ for i_start, i_end in intervals:
top = face.glyph.bitmap_top, top = face.glyph.bitmap_top,
data_length = len(packed), data_length = len(packed),
data_offset = total_size, data_offset = total_size,
compressed = 'true' if is_compressed else 'false',
code_point = code_point, code_point = code_point,
) )
total_size += len(packed) total_size += len(packed)

View File

@ -1,6 +1,31 @@
#include "GfxRenderer.h" #include "GfxRenderer.h"
#include <Utf8.h> #include <Utf8.h>
#include <miniz.h>
namespace {
tinfl_decompressor* inflator = nullptr;
bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) {
if (!inflator) {
Serial.printf("[%lu] [GFX] Inflator not initialized\n", millis());
return false;
}
size_t inBytes = deflatedSize;
size_t outBytes = inflatedSize;
tinfl_init(inflator);
const tinfl_status status = tinfl_decompress(inflator, inputBuf, &inBytes, nullptr, outputBuf, &outBytes,
TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
if (status != TINFL_STATUS_DONE) {
Serial.printf("[%lu] [GFX] tinfl_decompress() failed with status %d\n", millis(), status);
return false;
}
return true;
}
} // namespace
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); } void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
@ -104,10 +129,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
return; return;
} }
// Setup inflator
inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
if (!inflator) {
Serial.printf("[%lu] [GFX] Failed to allocate memory for inflator\n", millis());
return;
}
memset(inflator, 0, sizeof(tinfl_decompressor));
uint32_t cp; uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) { while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
renderChar(font, cp, &xpos, &yPos, black, style); renderChar(font, cp, &xpos, &yPos, black, style);
} }
free(inflator);
inflator = nullptr;
} }
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const { void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
@ -616,52 +652,82 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
} }
const int is2Bit = fontFamily.getData(style)->is2Bit; const int is2Bit = fontFamily.getData(style)->is2Bit;
const uint32_t offset = glyph->dataOffset; // const uint32_t offset = glyph->dataOffset;
// const uint32_t dataLength = glyph->dataLength;
const uint8_t width = glyph->width; const uint8_t width = glyph->width;
const uint8_t height = glyph->height; const uint8_t height = glyph->height;
const int left = glyph->left; const int left = glyph->left;
const size_t outputDataSize = is2Bit ? ((width * height + 3) / 4) : ((width * height + 7) / 8);
const uint8_t* bitmap = nullptr; if (outputDataSize != glyph->dataLength && !glyph->compressed) {
bitmap = &fontFamily.getData(style)->bitmap[offset]; Serial.printf("[%lu] [GFX] Glyph bitmap size mismatch for codepoint %d (expected %zu, got %d)\n", millis(), cp,
outputDataSize, glyph->dataLength);
}
if (bitmap != nullptr) { uint8_t* bitmapData = nullptr;
for (int glyphY = 0; glyphY < height; glyphY++) { const uint8_t* bitmap;
const int screenY = *y - glyph->top + glyphY;
for (int glyphX = 0; glyphX < width; glyphX++) {
const int pixelPosition = glyphY * width + glyphX;
const int screenX = *x + left + glyphX;
if (is2Bit) { if (glyph->compressed) {
const uint8_t byte = bitmap[pixelPosition / 4]; bitmapData = static_cast<uint8_t*>(malloc(outputDataSize));
const uint8_t bit_index = (3 - pixelPosition % 4) * 2; if (!bitmapData) {
// the direct bit from the font is 0 -> white, 1 -> light gray, 2 -> dark gray, 3 -> black Serial.printf("[%lu] [GFX] Failed to allocate memory for glyph bitmap for codepoint %d\n", millis(), cp);
// we swap this to better match the way images and screen think about colors: return;
// 0 -> black, 1 -> dark grey, 2 -> light grey, 3 -> white }
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
if (renderMode == BW && bmpVal < 3) { const auto success = inflateOneShot(&fontFamily.getData(style)->bitmap[glyph->dataOffset], glyph->dataLength,
// Black (also paints over the grays in BW mode) bitmapData, outputDataSize);
drawPixel(screenX, screenY, pixelState); if (!success) {
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { Serial.printf("[%lu] [GFX] Failed to inflate glyph bitmap for codepoint %d\n", millis(), cp);
// Light gray (also mark the MSB if it's going to be a dark gray too) free(bitmapData);
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update *x += glyph->advanceX;
drawPixel(screenX, screenY, false); return;
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) { }
// Dark gray
drawPixel(screenX, screenY, false);
}
} else {
const uint8_t byte = bitmap[pixelPosition / 8];
const uint8_t bit_index = 7 - (pixelPosition % 8);
if ((byte >> bit_index) & 1) { bitmap = bitmapData;
drawPixel(screenX, screenY, pixelState); } else {
} bitmap = &fontFamily.getData(style)->bitmap[glyph->dataOffset];
}
for (int glyphY = 0; glyphY < height; glyphY++) {
const int screenY = *y - glyph->top + glyphY;
for (int glyphX = 0; glyphX < width; glyphX++) {
const int pixelPosition = glyphY * width + glyphX;
const int screenX = *x + left + glyphX;
if (is2Bit) {
const uint8_t byte = bitmap[pixelPosition / 4];
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
// the direct bit from the font is 0 -> white, 1 -> light gray, 2 -> dark gray, 3 -> black
// we swap this to better match the way images and screen think about colors:
// 0 -> black, 1 -> dark grey, 2 -> light grey, 3 -> white
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
if (renderMode == BW && bmpVal < 3) {
// Black (also paints over the grays in BW mode)
drawPixel(screenX, screenY, pixelState);
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
// Light gray (also mark the MSB if it's going to be a dark gray too)
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
drawPixel(screenX, screenY, false);
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
// Dark gray
drawPixel(screenX, screenY, false);
}
} else {
const uint8_t byte = bitmap[pixelPosition / 8];
const uint8_t bit_index = 7 - (pixelPosition % 8);
if ((byte >> bit_index) & 1) {
drawPixel(screenX, screenY, pixelState);
} }
} }
} }
} }
if (bitmapData) {
free(bitmapData);
}
*x += glyph->advanceX; *x += glyph->advanceX;
} }

View File

@ -4,6 +4,7 @@
#include <SDCardManager.h> #include <SDCardManager.h>
#include <miniz.h> #include <miniz.h>
namespace {
bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) { bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) {
// Setup inflator // Setup inflator
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor))); const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
@ -27,6 +28,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
return true; return true;
} }
} // namespace
bool ZipFile::loadAllFileStatSlims() { bool ZipFile::loadAllFileStatSlims() {
const bool wasOpen = isOpen(); const bool wasOpen = isOpen();