Add 2-bit grayscale text and anti-aliased rendering of text

This commit is contained in:
Dave Allie 2025-12-08 02:40:29 +11:00
parent 9e046d7086
commit ba66f36f4c
No known key found for this signature in database
GPG Key ID: F2FDDB3AD8D0276F
20 changed files with 11079 additions and 6615 deletions

View File

@ -31,4 +31,5 @@ typedef struct {
uint8_t advanceY; ///< Newline distance (y axis)
int ascender; ///< Maximal height of a glyph above the base line
int descender; ///< Maximal height of a glyph below the base line
bool is2Bit;
} EpdFontData;

View File

@ -2,6 +2,7 @@
* generated by fontconvert.py
* name: babyblue
* size: 8
* mode: 1-bit
*/
#pragma once
#include "EpdFontData.h"
@ -500,5 +501,5 @@ static const EpdUnicodeInterval babyblueIntervals[] = {
};
static const EpdFontData babyblue = {
babyblueBitmaps, babyblueGlyphs, babyblueIntervals, 5, 17, 13, -4,
babyblueBitmaps, babyblueGlyphs, babyblueIntervals, 5, 17, 13, -4, false,
};

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

@ -2,6 +2,7 @@
* generated by fontconvert.py
* name: ubuntu_10
* size: 10
* mode: 1-bit
*/
#pragma once
#include "EpdFontData.h"
@ -762,5 +763,5 @@ static const EpdUnicodeInterval ubuntu_10Intervals[] = {
};
static const EpdFontData ubuntu_10 = {
ubuntu_10Bitmaps, ubuntu_10Glyphs, ubuntu_10Intervals, 31, 24, 20, -4,
ubuntu_10Bitmaps, ubuntu_10Glyphs, ubuntu_10Intervals, 31, 24, 20, -4, false,
};

View File

@ -2,6 +2,7 @@
* generated by fontconvert.py
* name: ubuntu_bold_10
* size: 10
* mode: 1-bit
*/
#pragma once
#include "EpdFontData.h"
@ -806,5 +807,5 @@ static const EpdUnicodeInterval ubuntu_bold_10Intervals[] = {
};
static const EpdFontData ubuntu_bold_10 = {
ubuntu_bold_10Bitmaps, ubuntu_bold_10Glyphs, ubuntu_bold_10Intervals, 31, 24, 20, -4,
ubuntu_bold_10Bitmaps, ubuntu_bold_10Glyphs, ubuntu_bold_10Intervals, 31, 24, 20, -4, false,
};

View File

@ -13,12 +13,14 @@ parser = argparse.ArgumentParser(description="Generate a header file from a 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("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.")
parser.add_argument("--2bit", dest="is2Bit", action="store_true", help="generate 2-bit greyscale bitmap instead of 1-bit black and white.")
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()
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]
is2Bit = args.is2Bit
size = args.size
font_name = args.name
@ -173,26 +175,73 @@ for i_start, i_end in intervals:
pixels4g.append(px)
px = 0
# Downsample to 1-bit bitmap - treat any non-zero as black
pixelsbw = []
px = 0
pitch = (bitmap.width // 2) + (bitmap.width % 2)
for y in range(bitmap.rows):
for x in range(bitmap.width):
px = px << 1
bm = pixels4g[y * pitch + (x // 2)]
px += 1 if ((x & 1) == 0 and bm & 0xF > 0) or ((x & 1) == 1 and bm & 0xF0 > 0) else 0
if is2Bit:
# 0 = white, 15 black, 8+ dark grey, 7- light grey
# Downsample to 2-bit bitmap
pixels2b = []
px = 0
pitch = (bitmap.width // 2) + (bitmap.width % 2)
for y in range(bitmap.rows):
for x in range(bitmap.width):
px = px << 2
bm = pixels4g[y * pitch + (x // 2)]
bm = (bm >> ((x % 2) * 4)) & 0xF
if (y * bitmap.width + x) % 8 == 7:
pixelsbw.append(px)
px = 0
if (bitmap.width * bitmap.rows) % 8 != 0:
px = px << (8 - (bitmap.width * bitmap.rows) % 8)
pixelsbw.append(px)
if bm == 15:
px += 3
elif bm >= 8:
px += 2
elif bm > 0:
px += 1
if (y * bitmap.width + x) % 4 == 3:
pixels2b.append(px)
px = 0
if (bitmap.width * bitmap.rows) % 4 != 0:
px = px << (4 - (bitmap.width * bitmap.rows) % 4) * 2
pixels2b.append(px)
# for y in range(bitmap.rows):
# line = ''
# for x in range(bitmap.width):
# pixelPosition = y * bitmap.width + x
# byte = pixels2b[pixelPosition // 4]
# bit_index = (3 - (pixelPosition % 4)) * 2
# line += '#' if ((byte >> bit_index) & 3) > 0 else '.'
# print(line)
# print('')
else:
# Downsample to 1-bit bitmap - treat any non-zero as black
pixelsbw = []
px = 0
pitch = (bitmap.width // 2) + (bitmap.width % 2)
for y in range(bitmap.rows):
for x in range(bitmap.width):
px = px << 1
bm = pixels4g[y * pitch + (x // 2)]
px += 1 if ((x & 1) == 0 and bm & 0xF > 0) or ((x & 1) == 1 and bm & 0xF0 > 0) else 0
if (y * bitmap.width + x) % 8 == 7:
pixelsbw.append(px)
px = 0
if (bitmap.width * bitmap.rows) % 8 != 0:
px = px << (8 - (bitmap.width * bitmap.rows) % 8)
pixelsbw.append(px)
# for y in range(bitmap.rows):
# line = ''
# for x in range(bitmap.width):
# pixelPosition = y * bitmap.width + x
# byte = pixelsbw[pixelPosition // 8]
# bit_index = 7 - (pixelPosition % 8)
# line += '#' if (byte >> bit_index) & 1 else '.'
# print(line)
# print('')
pixels = pixels2b if is2Bit else pixelsbw
# Build output data
packed = bytes(pixelsbw)
packed = bytes(pixels)
glyph = GlyphProps(
width = bitmap.width,
height = bitmap.rows,
@ -216,7 +265,7 @@ for index, glyph in enumerate(all_glyphs):
glyph_data.extend([b for b in packed])
glyph_props.append(props)
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n */")
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * mode: {'2-bit' if is2Bit else '1-bit'}\n */")
print("#pragma once")
print("#include \"EpdFontData.h\"\n")
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
@ -244,4 +293,5 @@ print(f" {len(intervals)},")
print(f" {norm_ceil(face.size.height)},")
print(f" {norm_ceil(face.size.ascender)},")
print(f" {norm_floor(face.size.descender)},")
print(f" {'true' if is2Bit else 'false'},")
print("};")

View File

@ -6,23 +6,27 @@
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; }
enum EpdFontRendererMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
template <typename Renderable>
class EpdFontRenderer {
Renderable& renderer;
void renderChar(uint32_t cp, int* x, const int* y, bool pixelState, EpdFontStyle style = REGULAR);
void renderChar(uint32_t cp, int* x, const int* y, bool pixelState, EpdFontStyle style = REGULAR,
EpdFontRendererMode mode = BW);
public:
const EpdFontFamily* fontFamily;
explicit EpdFontRenderer(const EpdFontFamily* fontFamily, Renderable& renderer)
: fontFamily(fontFamily), renderer(renderer) {}
~EpdFontRenderer() = default;
void renderString(const char* string, int* x, int* y, bool pixelState = true, EpdFontStyle style = REGULAR);
void renderString(const char* string, int* x, int* y, bool pixelState = true, EpdFontStyle style = REGULAR,
EpdFontRendererMode mode = BW);
void drawPixel(int x, int y, bool pixelState);
};
template <typename Renderable>
void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const bool pixelState,
const EpdFontStyle style) {
const EpdFontStyle style, const EpdFontRendererMode mode) {
// cannot draw a NULL / empty string
if (string == nullptr || *string == '\0') {
return;
@ -35,7 +39,7 @@ void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int*
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
renderChar(cp, x, y, pixelState, style);
renderChar(cp, x, y, pixelState, style, mode);
}
*y += fontFamily->getData(style)->advanceY;
@ -77,7 +81,7 @@ void EpdFontRenderer<Renderable>::drawPixel(const int x, const int y, const bool
template <typename Renderable>
void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, const bool pixelState,
const EpdFontStyle style) {
const EpdFontStyle style, const EpdFontRendererMode mode) {
const EpdGlyph* glyph = fontFamily->getGlyph(cp, style);
if (!glyph) {
// TODO: Replace with fallback glyph property?
@ -90,6 +94,7 @@ void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const in
return;
}
const int is2Bit = fontFamily->getData(style)->is2Bit;
const uint32_t offset = glyph->dataOffset;
const uint8_t width = glyph->width;
const uint8_t height = glyph->height;
@ -105,11 +110,26 @@ void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const in
const int pixelPosition = glyphY * width + glyphX;
int screenX = *x + left + glyphX;
const uint8_t byte = bitmap[pixelPosition / 8];
const uint8_t bit_index = 7 - (pixelPosition % 8);
if (is2Bit) {
const uint8_t byte = bitmap[pixelPosition / 4];
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
if ((byte >> bit_index) & 1) {
drawPixel(screenX, screenY, pixelState);
const uint8_t val = (byte >> bit_index) & 0x3;
if (mode == BW && val > 0) {
drawPixel(screenX, screenY, pixelState);
} else if (mode == GRAYSCALE_MSB && val == 1) {
// TODO: Not sure how this anti-aliasing goes on black backgrounds
drawPixel(screenX, screenY, false);
} else if (mode == GRAYSCALE_LSB && val == 2) {
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);
}
}
}
}

View File

@ -1,17 +1,17 @@
#include "EpdRenderer.h"
#include "builtinFonts/babyblue.h"
#include "builtinFonts/bookerly.h"
#include "builtinFonts/bookerly_bold.h"
#include "builtinFonts/bookerly_bold_italic.h"
#include "builtinFonts/bookerly_italic.h"
#include "builtinFonts/bookerly_2b.h"
#include "builtinFonts/bookerly_bold_2b.h"
#include "builtinFonts/bookerly_bold_italic_2b.h"
#include "builtinFonts/bookerly_italic_2b.h"
#include "builtinFonts/ubuntu_10.h"
#include "builtinFonts/ubuntu_bold_10.h"
EpdFont bookerlyFont(&bookerly);
EpdFont bookerlyBoldFont(&bookerly_bold);
EpdFont bookerlyItalicFont(&bookerly_italic);
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic);
EpdFont bookerlyFont(&bookerly_2b);
EpdFont bookerlyBoldFont(&bookerly_bold_2b);
EpdFont bookerlyItalicFont(&bookerly_italic_2b);
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b);
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
EpdFont smallFont(&babyblue);
@ -27,6 +27,7 @@ EpdRenderer::EpdRenderer(EInkDisplay& einkDisplay)
marginBottom(30),
marginLeft(10),
marginRight(10),
fontRendererMode(BW),
lineCompression(0.95f) {
this->regularFontRenderer = new EpdFontRenderer<EInkDisplay>(&bookerlyFontFamily, einkDisplay);
this->smallFontRenderer = new EpdFontRenderer<EInkDisplay>(&smallFontFamily, einkDisplay);
@ -101,21 +102,21 @@ void EpdRenderer::drawText(const int x, const int y, const char* text, const boo
const EpdFontStyle style) const {
int ypos = y + getLineHeight() + marginTop;
int xpos = x + marginLeft;
regularFontRenderer->renderString(text, &xpos, &ypos, state, style);
regularFontRenderer->renderString(text, &xpos, &ypos, state, style, fontRendererMode);
}
void EpdRenderer::drawUiText(const int x, const int y, const char* text, const bool state,
const EpdFontStyle style) const {
int ypos = y + uiFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
int xpos = x + marginLeft;
uiFontRenderer->renderString(text, &xpos, &ypos, state, style);
uiFontRenderer->renderString(text, &xpos, &ypos, state, style, fontRendererMode);
}
void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const bool state,
const EpdFontStyle style) const {
int ypos = y + smallFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
int xpos = x + marginLeft;
smallFontRenderer->renderString(text, &xpos, &ypos, state, style);
smallFontRenderer->renderString(text, &xpos, &ypos, state, style, fontRendererMode);
}
void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height,
@ -221,3 +222,9 @@ int EpdRenderer::getSpaceWidth() const { return regularFontRenderer->fontFamily-
int EpdRenderer::getLineHeight() const {
return regularFontRenderer->fontFamily->getData(REGULAR)->advanceY * lineCompression;
}
void EpdRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }
void EpdRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); }
void EpdRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); }

View File

@ -13,6 +13,7 @@ class EpdRenderer {
int marginBottom;
int marginLeft;
int marginRight;
EpdFontRendererMode fontRendererMode;
float lineCompression;
public:
@ -32,6 +33,9 @@ class EpdRenderer {
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawImageNoMargin(const uint8_t bitmap[], int x, int y, int width, int height) const;
void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const;
void clearScreen(uint8_t color = 0xFF) const;
void flushDisplay(bool partialUpdate = true) const;
void flushArea(int x, int y, int width, int height) const;
@ -46,4 +50,5 @@ class EpdRenderer {
void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; }
void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; }
void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; }
void setFontRendererMode(const EpdFontRendererMode mode) { this->fontRendererMode = mode; }
};

View File

@ -98,21 +98,15 @@ bool Section::persistPageDataToSD() {
return true;
}
void Section::renderPage() const {
if (0 <= currentPage && currentPage < pageCount) {
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
std::ifstream inputFile(filePath);
const Page* p = Page::deserialize(inputFile);
inputFile.close();
p->render(renderer);
delete p;
} else if (pageCount == 0) {
Serial.println("No pages to render");
const int width = renderer.getTextWidth("Empty chapter", BOLD);
renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Empty chapter", 1, BOLD);
} else {
Serial.printf("Page out of bounds: %d (max %d)\n", currentPage, pageCount);
const int width = renderer.getTextWidth("Out of bounds", BOLD);
renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Out of bounds", 1, BOLD);
Page* Section::loadPageFromSD() const {
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
if (!SD.exists(filePath.c_str() + 3)) {
Serial.printf("Page file does not exist: %s\n", filePath.c_str());
return nullptr;
}
std::ifstream inputFile(filePath);
Page* p = Page::deserialize(inputFile);
inputFile.close();
return p;
}

View File

@ -26,5 +26,5 @@ class Section {
void setupCacheDir() const;
void clearCache() const;
bool persistPageDataToSD();
void renderPage() const;
Page* loadPageFromSD() const;
};

View File

@ -1,6 +1,7 @@
#include "EpubReaderScreen.h"
#include <EpdRenderer.h>
#include <Epub/Page.h>
#include <SD.h>
#include "Battery.h"
@ -128,7 +129,7 @@ void EpubReaderScreen::displayTaskLoop() {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderPage();
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
@ -136,7 +137,7 @@ void EpubReaderScreen::displayTaskLoop() {
}
// TODO: Failure handling
void EpubReaderScreen::renderPage() {
void EpubReaderScreen::renderScreen() {
if (!epub) {
return;
}
@ -184,16 +185,27 @@ void EpubReaderScreen::renderPage() {
}
renderer.clearScreen();
section->renderPage();
renderStatusBar();
if (pagesUntilFullRefresh <= 1) {
renderer.flushDisplay(false);
pagesUntilFullRefresh = PAGES_PER_REFRESH;
} else {
if (section->pageCount == 0) {
Serial.println("No pages to render");
const int width = renderer.getTextWidth("Empty chapter", BOLD);
renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Empty chapter", true, BOLD);
renderer.flushDisplay();
pagesUntilFullRefresh--;
return;
}
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
Serial.printf("Page out of bounds: %d (max %d)\n", section->currentPage, section->pageCount);
const int width = renderer.getTextWidth("Out of bounds", BOLD);
renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Out of bounds", true, BOLD);
renderer.flushDisplay();
return;
}
const Page* p = section->loadPageFromSD();
renderContents(p);
delete p;
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
uint8_t data[4];
data[0] = currentSpineIndex & 0xFF;
@ -204,6 +216,30 @@ void EpubReaderScreen::renderPage() {
f.close();
}
void EpubReaderScreen::renderContents(const Page* p) const {
p->render(renderer);
renderStatusBar();
renderer.flushDisplay();
// grayscale rendering
{
renderer.clearScreen(0x00);
renderer.setFontRendererMode(GRAYSCALE_LSB);
p->render(renderer);
renderer.copyGrayscaleLsbBuffers();
// Render and copy to MSB buffer
renderer.clearScreen(0x00);
renderer.setFontRendererMode(GRAYSCALE_MSB);
p->render(renderer);
renderer.copyGrayscaleMsbBuffers();
// display grayscale part
renderer.displayGrayBuffer();
renderer.setFontRendererMode(BW);
}
}
void EpubReaderScreen::renderStatusBar() const {
const auto pageWidth = renderer.getPageWidth();

View File

@ -20,7 +20,8 @@ class EpubReaderScreen final : public Screen {
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderPage();
void renderScreen();
void renderContents(const Page* p) const;
void renderStatusBar() const;
public: