mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2025-12-18 15:17:42 +03:00
Move to SDK EInkDisplay and enable anti-aliased 2-bit text (#5)
* First pass at moving to SDK EInkDisplay library * Add 2-bit grayscale text and anti-aliased rendering of text * Render status bar for empty chapters * Refresh screen every 15 pages to avoid ghosting * Simplify boot and sleep screens * Give FileSelectionScreen task more stack memory * Move text around slightly on Boot and Sleep screens * Re-use existing buffer and write to whole screen for 'partial update'
This commit is contained in:
parent
de453fed1d
commit
2ed8017aa2
@ -31,4 +31,5 @@ typedef struct {
|
|||||||
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
|
||||||
|
bool is2Bit;
|
||||||
} EpdFontData;
|
} EpdFontData;
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
* generated by fontconvert.py
|
* generated by fontconvert.py
|
||||||
* name: babyblue
|
* name: babyblue
|
||||||
* size: 8
|
* size: 8
|
||||||
|
* mode: 1-bit
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "EpdFontData.h"
|
#include "EpdFontData.h"
|
||||||
@ -500,5 +501,5 @@ static const EpdUnicodeInterval babyblueIntervals[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const EpdFontData babyblue = {
|
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
2608
lib/EpdFont/builtinFonts/bookerly_2b.h
Normal file
2608
lib/EpdFont/builtinFonts/bookerly_2b.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2748
lib/EpdFont/builtinFonts/bookerly_bold_2b.h
Normal file
2748
lib/EpdFont/builtinFonts/bookerly_bold_2b.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2859
lib/EpdFont/builtinFonts/bookerly_bold_italic_2b.h
Normal file
2859
lib/EpdFont/builtinFonts/bookerly_bold_italic_2b.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2680
lib/EpdFont/builtinFonts/bookerly_italic_2b.h
Normal file
2680
lib/EpdFont/builtinFonts/bookerly_italic_2b.h
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@
|
|||||||
* generated by fontconvert.py
|
* generated by fontconvert.py
|
||||||
* name: ubuntu_10
|
* name: ubuntu_10
|
||||||
* size: 10
|
* size: 10
|
||||||
|
* mode: 1-bit
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "EpdFontData.h"
|
#include "EpdFontData.h"
|
||||||
@ -762,5 +763,5 @@ static const EpdUnicodeInterval ubuntu_10Intervals[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const EpdFontData ubuntu_10 = {
|
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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
* generated by fontconvert.py
|
* generated by fontconvert.py
|
||||||
* name: ubuntu_bold_10
|
* name: ubuntu_bold_10
|
||||||
* size: 10
|
* size: 10
|
||||||
|
* mode: 1-bit
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "EpdFontData.h"
|
#include "EpdFontData.h"
|
||||||
@ -806,5 +807,5 @@ static const EpdUnicodeInterval ubuntu_bold_10Intervals[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const EpdFontData ubuntu_bold_10 = {
|
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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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("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("--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.")
|
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", "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
|
||||||
size = args.size
|
size = args.size
|
||||||
font_name = args.name
|
font_name = args.name
|
||||||
|
|
||||||
@ -173,6 +175,42 @@ for i_start, i_end in intervals:
|
|||||||
pixels4g.append(px)
|
pixels4g.append(px)
|
||||||
px = 0
|
px = 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 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
|
# Downsample to 1-bit bitmap - treat any non-zero as black
|
||||||
pixelsbw = []
|
pixelsbw = []
|
||||||
px = 0
|
px = 0
|
||||||
@ -190,9 +228,20 @@ for i_start, i_end in intervals:
|
|||||||
px = px << (8 - (bitmap.width * bitmap.rows) % 8)
|
px = px << (8 - (bitmap.width * bitmap.rows) % 8)
|
||||||
pixelsbw.append(px)
|
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
|
# Build output data
|
||||||
packed = bytes(pixelsbw)
|
packed = bytes(pixels)
|
||||||
glyph = GlyphProps(
|
glyph = GlyphProps(
|
||||||
width = bitmap.width,
|
width = bitmap.width,
|
||||||
height = bitmap.rows,
|
height = bitmap.rows,
|
||||||
@ -216,7 +265,7 @@ for index, glyph in enumerate(all_glyphs):
|
|||||||
glyph_data.extend([b for b in packed])
|
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 */")
|
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("#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)}] = {{")
|
||||||
@ -244,4 +293,5 @@ print(f" {len(intervals)},")
|
|||||||
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)},")
|
||||||
|
print(f" {'true' if is2Bit else 'false'},")
|
||||||
print("};")
|
print("};")
|
||||||
|
|||||||
@ -6,22 +6,27 @@
|
|||||||
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; }
|
||||||
|
|
||||||
|
enum EpdFontRendererMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
|
||||||
|
|
||||||
template <typename Renderable>
|
template <typename Renderable>
|
||||||
class EpdFontRenderer {
|
class EpdFontRenderer {
|
||||||
Renderable& renderer;
|
Renderable& renderer;
|
||||||
void renderChar(uint32_t cp, int* x, const int* y, uint16_t color, EpdFontStyle style = REGULAR);
|
void renderChar(uint32_t cp, int* x, const int* y, bool pixelState, EpdFontStyle style = REGULAR,
|
||||||
|
EpdFontRendererMode mode = BW);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
const EpdFontFamily* fontFamily;
|
const EpdFontFamily* fontFamily;
|
||||||
explicit EpdFontRenderer(const EpdFontFamily* fontFamily, Renderable& renderer)
|
explicit EpdFontRenderer(const EpdFontFamily* fontFamily, Renderable& renderer)
|
||||||
: fontFamily(fontFamily), renderer(renderer) {}
|
: fontFamily(fontFamily), renderer(renderer) {}
|
||||||
~EpdFontRenderer() = default;
|
~EpdFontRenderer() = default;
|
||||||
void renderString(const char* string, int* x, int* y, uint16_t color, 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>
|
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 bool pixelState,
|
||||||
const EpdFontStyle style) {
|
const EpdFontStyle style, const EpdFontRendererMode mode) {
|
||||||
// cannot draw a NULL / empty string
|
// cannot draw a NULL / empty string
|
||||||
if (string == nullptr || *string == '\0') {
|
if (string == nullptr || *string == '\0') {
|
||||||
return;
|
return;
|
||||||
@ -34,15 +39,49 @@ void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int*
|
|||||||
|
|
||||||
uint32_t cp;
|
uint32_t cp;
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||||
renderChar(cp, x, y, color, style);
|
renderChar(cp, x, y, pixelState, style, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
*y += fontFamily->getData(style)->advanceY;
|
*y += fontFamily->getData(style)->advanceY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Consolidate this with EpdRenderer implementation
|
||||||
template <typename Renderable>
|
template <typename Renderable>
|
||||||
void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, uint16_t color,
|
void EpdFontRenderer<Renderable>::drawPixel(const int x, const int y, const bool pixelState) {
|
||||||
const EpdFontStyle style) {
|
uint8_t* frameBuffer = renderer.getFrameBuffer();
|
||||||
|
|
||||||
|
// Early return if no framebuffer is set
|
||||||
|
if (!frameBuffer) {
|
||||||
|
Serial.printf("!!No framebuffer\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds checking (portrait: 480x800)
|
||||||
|
if (x < 0 || x >= EInkDisplay::DISPLAY_HEIGHT || y < 0 || y >= EInkDisplay::DISPLAY_WIDTH) {
|
||||||
|
Serial.printf("!!Outside range (%d, %d)\n", x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate coordinates: portrait (480x800) -> landscape (800x480)
|
||||||
|
// Rotation: 90 degrees clockwise
|
||||||
|
const int16_t rotatedX = y;
|
||||||
|
const int16_t rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||||
|
|
||||||
|
// Calculate byte position and bit position
|
||||||
|
const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
|
||||||
|
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
|
||||||
|
|
||||||
|
// Set or clear the bit
|
||||||
|
if (pixelState) {
|
||||||
|
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
||||||
|
} else {
|
||||||
|
frameBuffer[byteIndex] |= (1 << bitPosition); // Set bit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Renderable>
|
||||||
|
void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, const bool pixelState,
|
||||||
|
const EpdFontStyle style, const EpdFontRendererMode mode) {
|
||||||
const EpdGlyph* glyph = fontFamily->getGlyph(cp, style);
|
const EpdGlyph* glyph = fontFamily->getGlyph(cp, style);
|
||||||
if (!glyph) {
|
if (!glyph) {
|
||||||
// TODO: Replace with fallback glyph property?
|
// TODO: Replace with fallback glyph property?
|
||||||
@ -55,6 +94,7 @@ void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const in
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int is2Bit = fontFamily->getData(style)->is2Bit;
|
||||||
const uint32_t offset = glyph->dataOffset;
|
const uint32_t offset = glyph->dataOffset;
|
||||||
const uint8_t width = glyph->width;
|
const uint8_t width = glyph->width;
|
||||||
const uint8_t height = glyph->height;
|
const uint8_t height = glyph->height;
|
||||||
@ -70,11 +110,26 @@ void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const in
|
|||||||
const int pixelPosition = glyphY * width + glyphX;
|
const int pixelPosition = glyphY * width + glyphX;
|
||||||
int screenX = *x + left + glyphX;
|
int screenX = *x + left + glyphX;
|
||||||
|
|
||||||
|
if (is2Bit) {
|
||||||
|
const uint8_t byte = bitmap[pixelPosition / 4];
|
||||||
|
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
|
||||||
|
|
||||||
|
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 byte = bitmap[pixelPosition / 8];
|
||||||
const uint8_t bit_index = 7 - (pixelPosition % 8);
|
const uint8_t bit_index = 7 - (pixelPosition % 8);
|
||||||
|
|
||||||
if ((byte >> bit_index) & 1) {
|
if ((byte >> bit_index) & 1) {
|
||||||
renderer.drawPixel(screenX, screenY, color);
|
drawPixel(screenX, screenY, pixelState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
#include "EpdRenderer.h"
|
#include "EpdRenderer.h"
|
||||||
|
|
||||||
#include "builtinFonts/babyblue.h"
|
#include "builtinFonts/babyblue.h"
|
||||||
#include "builtinFonts/bookerly.h"
|
#include "builtinFonts/bookerly_2b.h"
|
||||||
#include "builtinFonts/bookerly_bold.h"
|
#include "builtinFonts/bookerly_bold_2b.h"
|
||||||
#include "builtinFonts/bookerly_bold_italic.h"
|
#include "builtinFonts/bookerly_bold_italic_2b.h"
|
||||||
#include "builtinFonts/bookerly_italic.h"
|
#include "builtinFonts/bookerly_italic_2b.h"
|
||||||
#include "builtinFonts/ubuntu_10.h"
|
#include "builtinFonts/ubuntu_10.h"
|
||||||
#include "builtinFonts/ubuntu_bold_10.h"
|
#include "builtinFonts/ubuntu_bold_10.h"
|
||||||
|
|
||||||
EpdFont bookerlyFont(&bookerly);
|
EpdFont bookerlyFont(&bookerly_2b);
|
||||||
EpdFont bookerlyBoldFont(&bookerly_bold);
|
EpdFont bookerlyBoldFont(&bookerly_bold_2b);
|
||||||
EpdFont bookerlyItalicFont(&bookerly_italic);
|
EpdFont bookerlyItalicFont(&bookerly_italic_2b);
|
||||||
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic);
|
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b);
|
||||||
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
|
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
|
||||||
|
|
||||||
EpdFont smallFont(&babyblue);
|
EpdFont smallFont(&babyblue);
|
||||||
@ -21,11 +21,17 @@ EpdFont ubuntu10Font(&ubuntu_10);
|
|||||||
EpdFont ununtuBold10Font(&ubuntu_bold_10);
|
EpdFont ununtuBold10Font(&ubuntu_bold_10);
|
||||||
EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ununtuBold10Font);
|
EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ununtuBold10Font);
|
||||||
|
|
||||||
EpdRenderer::EpdRenderer(XteinkDisplay& display)
|
EpdRenderer::EpdRenderer(EInkDisplay& einkDisplay)
|
||||||
: display(display), marginTop(11), marginBottom(30), marginLeft(10), marginRight(10), lineCompression(0.95f) {
|
: einkDisplay(einkDisplay),
|
||||||
this->regularFontRenderer = new EpdFontRenderer<XteinkDisplay>(&bookerlyFontFamily, display);
|
marginTop(11),
|
||||||
this->smallFontRenderer = new EpdFontRenderer<XteinkDisplay>(&smallFontFamily, display);
|
marginBottom(30),
|
||||||
this->uiFontRenderer = new EpdFontRenderer<XteinkDisplay>(&ubuntuFontFamily, display);
|
marginLeft(10),
|
||||||
|
marginRight(10),
|
||||||
|
fontRendererMode(BW),
|
||||||
|
lineCompression(0.95f) {
|
||||||
|
this->regularFontRenderer = new EpdFontRenderer<EInkDisplay>(&bookerlyFontFamily, einkDisplay);
|
||||||
|
this->smallFontRenderer = new EpdFontRenderer<EInkDisplay>(&smallFontFamily, einkDisplay);
|
||||||
|
this->uiFontRenderer = new EpdFontRenderer<EInkDisplay>(&ubuntuFontFamily, einkDisplay);
|
||||||
}
|
}
|
||||||
|
|
||||||
EpdRenderer::~EpdRenderer() {
|
EpdRenderer::~EpdRenderer() {
|
||||||
@ -34,6 +40,40 @@ EpdRenderer::~EpdRenderer() {
|
|||||||
delete uiFontRenderer;
|
delete uiFontRenderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EpdRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||||
|
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
||||||
|
|
||||||
|
// Early return if no framebuffer is set
|
||||||
|
if (!frameBuffer) {
|
||||||
|
Serial.printf("!!No framebuffer\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int adjX = x + marginLeft;
|
||||||
|
const int adjY = y + marginTop;
|
||||||
|
|
||||||
|
// Bounds checking (portrait: 480x800)
|
||||||
|
if (adjX < 0 || adjX >= EInkDisplay::DISPLAY_HEIGHT || adjY < 0 || adjY >= EInkDisplay::DISPLAY_WIDTH) {
|
||||||
|
Serial.printf("!!Outside range (%d, %d)\n", adjX, adjY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate coordinates: portrait (480x800) -> landscape (800x480)
|
||||||
|
// Rotation: 90 degrees clockwise
|
||||||
|
const int rotatedX = adjY;
|
||||||
|
const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - adjX;
|
||||||
|
|
||||||
|
// Calculate byte position and bit position
|
||||||
|
const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
|
||||||
|
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
||||||
|
} else {
|
||||||
|
frameBuffer[byteIndex] |= 1 << bitPosition; // Set bit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int EpdRenderer::getTextWidth(const char* text, const EpdFontStyle style) const {
|
int EpdRenderer::getTextWidth(const char* text, const EpdFontStyle style) const {
|
||||||
int w = 0, h = 0;
|
int w = 0, h = 0;
|
||||||
|
|
||||||
@ -58,25 +98,25 @@ int EpdRenderer::getSmallTextWidth(const char* text, const EpdFontStyle style) c
|
|||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpdRenderer::drawText(const int x, const int y, const char* text, const uint16_t color,
|
void EpdRenderer::drawText(const int x, const int y, const char* text, const bool state,
|
||||||
const EpdFontStyle style) const {
|
const EpdFontStyle style) const {
|
||||||
int ypos = y + getLineHeight() + marginTop;
|
int ypos = y + getLineHeight() + marginTop;
|
||||||
int xpos = x + marginLeft;
|
int xpos = x + marginLeft;
|
||||||
regularFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
|
regularFontRenderer->renderString(text, &xpos, &ypos, state, style, fontRendererMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpdRenderer::drawUiText(const int x, const int y, const char* text, const uint16_t color,
|
void EpdRenderer::drawUiText(const int x, const int y, const char* text, const bool state,
|
||||||
const EpdFontStyle style) const {
|
const EpdFontStyle style) const {
|
||||||
int ypos = y + uiFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
|
int ypos = y + uiFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
|
||||||
int xpos = x + marginLeft;
|
int xpos = x + marginLeft;
|
||||||
uiFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
|
uiFontRenderer->renderString(text, &xpos, &ypos, state, style, fontRendererMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const uint16_t color,
|
void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const bool state,
|
||||||
const EpdFontStyle style) const {
|
const EpdFontStyle style) const {
|
||||||
int ypos = y + smallFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
|
int ypos = y + smallFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
|
||||||
int xpos = x + marginLeft;
|
int xpos = x + marginLeft;
|
||||||
smallFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, 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,
|
void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height,
|
||||||
@ -88,7 +128,7 @@ void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text,
|
|||||||
int ypos = 0;
|
int ypos = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (end >= length) {
|
if (end >= length) {
|
||||||
drawText(x, y + ypos, text.substr(start, length - start).c_str(), 1, style);
|
drawText(x, y + ypos, text.substr(start, length - start).c_str(), true, style);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +137,7 @@ void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (text[end - 1] == '\n') {
|
if (text[end - 1] == '\n') {
|
||||||
drawText(x, y + ypos, text.substr(start, end - start).c_str(), 1, style);
|
drawText(x, y + ypos, text.substr(start, end - start).c_str(), true, style);
|
||||||
ypos += getLineHeight();
|
ypos += getLineHeight();
|
||||||
start = end;
|
start = end;
|
||||||
end = start + 1;
|
end = start + 1;
|
||||||
@ -105,7 +145,7 @@ void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (getTextWidth(text.substr(start, end - start).c_str(), style) > width) {
|
if (getTextWidth(text.substr(start, end - start).c_str(), style) > width) {
|
||||||
drawText(x, y + ypos, text.substr(start, end - start - 1).c_str(), 1, style);
|
drawText(x, y + ypos, text.substr(start, end - start - 1).c_str(), true, style);
|
||||||
ypos += getLineHeight();
|
ypos += getLineHeight();
|
||||||
start = end - 1;
|
start = end - 1;
|
||||||
continue;
|
continue;
|
||||||
@ -115,54 +155,88 @@ void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpdRenderer::drawLine(int x1, int y1, int x2, int y2, uint16_t color) const {
|
void EpdRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
|
||||||
display.drawLine(x1 + marginLeft, y1 + marginTop, x2 + marginLeft, y2 + marginTop,
|
if (x1 == x2) {
|
||||||
color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
if (y2 < y1) {
|
||||||
|
std::swap(y1, y2);
|
||||||
|
}
|
||||||
|
for (int y = y1; y <= y2; y++) {
|
||||||
|
drawPixel(x1, y, state);
|
||||||
|
}
|
||||||
|
} else if (y1 == y2) {
|
||||||
|
if (x2 < x1) {
|
||||||
|
std::swap(x1, x2);
|
||||||
|
}
|
||||||
|
for (int x = x1; x <= x2; x++) {
|
||||||
|
drawPixel(x, y1, state);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: Implement
|
||||||
|
Serial.println("Line drawing not supported");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpdRenderer::drawRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
|
void EpdRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||||
display.drawRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
drawLine(x, y, x + width - 1, y, state);
|
||||||
|
drawLine(x + width - 1, y, x + width - 1, y + height - 1, state);
|
||||||
|
drawLine(x + width - 1, y + height - 1, x, y + height - 1, state);
|
||||||
|
drawLine(x, y, x, y + height - 1, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpdRenderer::fillRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
|
void EpdRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||||
display.fillRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
for (int fillY = y; fillY < y + height; fillY++) {
|
||||||
|
drawLine(x, fillY, x + width - 1, fillY, state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpdRenderer::drawCircle(const int x, const int y, const int radius, const uint16_t color) const {
|
void EpdRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
||||||
display.drawCircle(x + marginLeft, y + marginTop, radius, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
drawImageNoMargin(bitmap, x + marginLeft, y + marginTop, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpdRenderer::fillCircle(const int x, const int y, const int radius, const uint16_t color) const {
|
// TODO: Support y-mirror?
|
||||||
display.fillCircle(x + marginLeft, y + marginTop, radius, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
void EpdRenderer::drawImageNoMargin(const uint8_t bitmap[], const int x, const int y, const int width,
|
||||||
|
const int height) const {
|
||||||
|
einkDisplay.drawImage(bitmap, x, y, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpdRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height,
|
void EpdRenderer::clearScreen(const uint8_t color) const {
|
||||||
const bool invert, const bool mirrorY) const {
|
|
||||||
drawImageNoMargin(bitmap, x + marginLeft, y + marginTop, width, height, invert, mirrorY);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::drawImageNoMargin(const uint8_t bitmap[], const int x, const int y, const int width, const int height,
|
|
||||||
const bool invert, const bool mirrorY) const {
|
|
||||||
display.drawImage(bitmap, x, y, width, height, invert, mirrorY);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::clearScreen(const bool black) const {
|
|
||||||
Serial.println("Clearing screen");
|
Serial.println("Clearing screen");
|
||||||
display.fillScreen(black ? GxEPD_BLACK : GxEPD_WHITE);
|
einkDisplay.clearScreen(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpdRenderer::flushDisplay(const bool partialUpdate) const { display.display(partialUpdate); }
|
void EpdRenderer::invertScreen() const {
|
||||||
|
uint8_t *buffer = einkDisplay.getFrameBuffer();
|
||||||
void EpdRenderer::flushArea(const int x, const int y, const int width, const int height) const {
|
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
|
||||||
display.displayWindow(x + marginLeft, y + marginTop, width, height);
|
buffer[i] = ~buffer[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int EpdRenderer::getPageWidth() const { return display.width() - marginLeft - marginRight; }
|
void EpdRenderer::flushDisplay(const EInkDisplay::RefreshMode refreshMode) const {
|
||||||
|
einkDisplay.displayBuffer(refreshMode);
|
||||||
|
}
|
||||||
|
|
||||||
int EpdRenderer::getPageHeight() const { return display.height() - marginTop - marginBottom; }
|
// TODO: Support partial window update
|
||||||
|
// void EpdRenderer::flushArea(const int x, const int y, const int width, const int height) const {
|
||||||
|
// const int rotatedX = y;
|
||||||
|
// const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||||
|
//
|
||||||
|
// einkDisplay.displayBuffer(EInkDisplay::FAST_REFRESH, rotatedX, rotatedY, height, width);
|
||||||
|
// }
|
||||||
|
|
||||||
|
int EpdRenderer::getPageWidth() const { return EInkDisplay::DISPLAY_HEIGHT - marginLeft - marginRight; }
|
||||||
|
|
||||||
|
int EpdRenderer::getPageHeight() const { return EInkDisplay::DISPLAY_WIDTH - marginTop - marginBottom; }
|
||||||
|
|
||||||
int EpdRenderer::getSpaceWidth() const { return regularFontRenderer->fontFamily->getGlyph(' ', REGULAR)->advanceX; }
|
int EpdRenderer::getSpaceWidth() const { return regularFontRenderer->fontFamily->getGlyph(' ', REGULAR)->advanceX; }
|
||||||
|
|
||||||
int EpdRenderer::getLineHeight() const {
|
int EpdRenderer::getLineHeight() const {
|
||||||
return regularFontRenderer->fontFamily->getData(REGULAR)->advanceY * lineCompression;
|
return regularFontRenderer->fontFamily->getData(REGULAR)->advanceY * lineCompression;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EpdRenderer::swapBuffers() const { einkDisplay.swapBuffers(); }
|
||||||
|
|
||||||
|
void EpdRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }
|
||||||
|
|
||||||
|
void EpdRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); }
|
||||||
|
|
||||||
|
void EpdRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); }
|
||||||
|
|||||||
@ -1,52 +1,58 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <GxEPD2_BW.h>
|
#include <EInkDisplay.h>
|
||||||
|
|
||||||
#include <EpdFontRenderer.hpp>
|
#include <EpdFontRenderer.hpp>
|
||||||
|
|
||||||
#define XteinkDisplay GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT>
|
|
||||||
|
|
||||||
class EpdRenderer {
|
class EpdRenderer {
|
||||||
XteinkDisplay& display;
|
EInkDisplay& einkDisplay;
|
||||||
EpdFontRenderer<XteinkDisplay>* regularFontRenderer;
|
EpdFontRenderer<EInkDisplay>* regularFontRenderer;
|
||||||
EpdFontRenderer<XteinkDisplay>* smallFontRenderer;
|
EpdFontRenderer<EInkDisplay>* smallFontRenderer;
|
||||||
EpdFontRenderer<XteinkDisplay>* uiFontRenderer;
|
EpdFontRenderer<EInkDisplay>* uiFontRenderer;
|
||||||
int marginTop;
|
int marginTop;
|
||||||
int marginBottom;
|
int marginBottom;
|
||||||
int marginLeft;
|
int marginLeft;
|
||||||
int marginRight;
|
int marginRight;
|
||||||
|
EpdFontRendererMode fontRendererMode;
|
||||||
float lineCompression;
|
float lineCompression;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit EpdRenderer(XteinkDisplay& display);
|
explicit EpdRenderer(EInkDisplay& einkDisplay);
|
||||||
~EpdRenderer();
|
~EpdRenderer();
|
||||||
|
void drawPixel(int x, int y, bool state = true) const;
|
||||||
int getTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
|
int getTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
|
||||||
int getUiTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
|
int getUiTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
|
||||||
int getSmallTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
|
int getSmallTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
|
||||||
void drawText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
|
void drawText(int x, int y, const char* text, bool state = true, EpdFontStyle style = REGULAR) const;
|
||||||
void drawUiText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
|
void drawUiText(int x, int y, const char* text, bool state = true, EpdFontStyle style = REGULAR) const;
|
||||||
void drawSmallText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
|
void drawSmallText(int x, int y, const char* text, bool state = true, EpdFontStyle style = REGULAR) const;
|
||||||
void drawTextBox(int x, int y, const std::string& text, int width, int height, EpdFontStyle style = REGULAR) const;
|
void drawTextBox(int x, int y, const std::string& text, int width, int height, EpdFontStyle style = REGULAR) const;
|
||||||
void drawLine(int x1, int y1, int x2, int y2, uint16_t color = 1) const;
|
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
|
||||||
void drawRect(int x, int y, int width, int height, uint16_t color = 1) const;
|
void drawRect(int x, int y, int width, int height, bool state = true) const;
|
||||||
void fillRect(int x, int y, int width, int height, uint16_t color = 1) const;
|
void fillRect(int x, int y, int width, int height, bool state = true) const;
|
||||||
void drawCircle(int x, int y, int radius, uint16_t color = 1) const;
|
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||||
void fillCircle(int x, int y, int radius, uint16_t color = 1) const;
|
void drawImageNoMargin(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||||
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height, bool invert = false,
|
|
||||||
bool mirrorY = false) const;
|
void swapBuffers() const;
|
||||||
void drawImageNoMargin(const uint8_t bitmap[], int x, int y, int width, int height, bool invert = false,
|
void copyGrayscaleLsbBuffers() const;
|
||||||
bool mirrorY = false) const;
|
void copyGrayscaleMsbBuffers() const;
|
||||||
void clearScreen(bool black = false) const;
|
void displayGrayBuffer() const;
|
||||||
void flushDisplay(bool partialUpdate = true) const;
|
void clearScreen(uint8_t color = 0xFF) const;
|
||||||
void flushArea(int x, int y, int width, int height) const;
|
|
||||||
|
void invertScreen() const;
|
||||||
|
|
||||||
|
void flushDisplay(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
|
||||||
|
// void flushArea(int x, int y, int width, int height) const;
|
||||||
|
|
||||||
int getPageWidth() const;
|
int getPageWidth() const;
|
||||||
int getPageHeight() const;
|
int getPageHeight() const;
|
||||||
int getSpaceWidth() const;
|
int getSpaceWidth() const;
|
||||||
int getLineHeight() const;
|
int getLineHeight() const;
|
||||||
|
|
||||||
// set margins
|
// set margins
|
||||||
void setMarginTop(const int newMarginTop) { this->marginTop = newMarginTop; }
|
void setMarginTop(const int newMarginTop) { this->marginTop = newMarginTop; }
|
||||||
void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; }
|
void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; }
|
||||||
void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; }
|
void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; }
|
||||||
void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; }
|
void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; }
|
||||||
|
void setFontRendererMode(const EpdFontRendererMode mode) { this->fontRendererMode = mode; }
|
||||||
};
|
};
|
||||||
|
|||||||
@ -98,21 +98,15 @@ bool Section::persistPageDataToSD() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Section::renderPage() const {
|
Page* Section::loadPageFromSD() const {
|
||||||
if (0 <= currentPage && currentPage < pageCount) {
|
|
||||||
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
|
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
|
||||||
std::ifstream inputFile(filePath);
|
if (!SD.exists(filePath.c_str() + 3)) {
|
||||||
const Page* p = Page::deserialize(inputFile);
|
Serial.printf("Page file does not exist: %s\n", filePath.c_str());
|
||||||
inputFile.close();
|
return nullptr;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::ifstream inputFile(filePath);
|
||||||
|
Page* p = Page::deserialize(inputFile);
|
||||||
|
inputFile.close();
|
||||||
|
return p;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,5 +26,5 @@ class Section {
|
|||||||
void setupCacheDir() const;
|
void setupCacheDir() const;
|
||||||
void clearCache() const;
|
void clearCache() const;
|
||||||
bool persistPageDataToSD();
|
bool persistPageDataToSD();
|
||||||
void renderPage() const;
|
Page* loadPageFromSD() const;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -165,7 +165,7 @@ void TextBlock::render(const EpdRenderer& renderer, const int x, const int y) co
|
|||||||
} else if (wordStyles[i] & ITALIC_SPAN) {
|
} else if (wordStyles[i] & ITALIC_SPAN) {
|
||||||
fontStyle = ITALIC;
|
fontStyle = ITALIC;
|
||||||
}
|
}
|
||||||
renderer.drawText(x + wordXpos[i], y, words[i].c_str(), 1, fontStyle);
|
renderer.drawText(x + wordXpos[i], y, words[i].c_str(), true, fontStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit 8224d278c58e76abf781c2e015f28a09419f27b2
|
Subproject commit a126d4b0bf66cd2895d11748774f7ec2c366cc4c
|
||||||
@ -34,7 +34,7 @@ build_flags =
|
|||||||
|
|
||||||
; Libraries
|
; Libraries
|
||||||
lib_deps =
|
lib_deps =
|
||||||
zinggjm/GxEPD2@^1.6.5
|
|
||||||
https://github.com/leethomason/tinyxml2.git#11.0.0
|
https://github.com/leethomason/tinyxml2.git#11.0.0
|
||||||
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
||||||
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
||||||
|
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
extern const uint8_t CrossLarge[] = {
|
static const uint8_t CrossLarge[] = {
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF,
|
0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF,
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
16
src/main.cpp
16
src/main.cpp
@ -1,7 +1,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <EInkDisplay.h>
|
||||||
#include <EpdRenderer.h>
|
#include <EpdRenderer.h>
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GxEPD2_BW.h>
|
|
||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
@ -28,10 +28,9 @@
|
|||||||
#define SD_SPI_CS 12
|
#define SD_SPI_CS 12
|
||||||
#define SD_SPI_MISO 7
|
#define SD_SPI_MISO 7
|
||||||
|
|
||||||
GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC,
|
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
|
||||||
EPD_RST, EPD_BUSY));
|
|
||||||
InputManager inputManager;
|
InputManager inputManager;
|
||||||
EpdRenderer renderer(display);
|
EpdRenderer renderer(einkDisplay);
|
||||||
Screen* currentScreen;
|
Screen* currentScreen;
|
||||||
CrossPointState appState;
|
CrossPointState appState;
|
||||||
|
|
||||||
@ -123,7 +122,7 @@ void enterDeepSleep() {
|
|||||||
// Enable Wakeup on LOW (button press)
|
// Enable Wakeup on LOW (button press)
|
||||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||||
|
|
||||||
display.hibernate();
|
einkDisplay.deepSleep();
|
||||||
|
|
||||||
// Enter Deep Sleep
|
// Enter Deep Sleep
|
||||||
esp_deep_sleep_start();
|
esp_deep_sleep_start();
|
||||||
@ -142,7 +141,7 @@ void onSelectEpubFile(const std::string& path) {
|
|||||||
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
|
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
|
||||||
} else {
|
} else {
|
||||||
exitScreen();
|
exitScreen();
|
||||||
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, false, false));
|
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH));
|
||||||
delay(2000);
|
delay(2000);
|
||||||
onGoHome();
|
onGoHome();
|
||||||
}
|
}
|
||||||
@ -170,10 +169,7 @@ void setup() {
|
|||||||
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
|
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
|
||||||
|
|
||||||
// Initialize display
|
// Initialize display
|
||||||
const SPISettings spi_settings(SPI_FQ, MSBFIRST, SPI_MODE0);
|
einkDisplay.begin();
|
||||||
display.init(115200, true, 2, false, SPI, spi_settings);
|
|
||||||
display.setRotation(3); // 270 degrees
|
|
||||||
display.setTextColor(GxEPD_BLACK);
|
|
||||||
Serial.println("Display initialized");
|
Serial.println("Display initialized");
|
||||||
|
|
||||||
exitScreen();
|
exitScreen();
|
||||||
|
|||||||
@ -11,4 +11,9 @@ void BootLogoScreen::onEnter() {
|
|||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
// Location for images is from top right in landscape orientation
|
// Location for images is from top right in landscape orientation
|
||||||
renderer.drawImage(CrossLarge, (pageHeight - 128) / 2, (pageWidth - 128) / 2, 128, 128);
|
renderer.drawImage(CrossLarge, (pageHeight - 128) / 2, (pageWidth - 128) / 2, 128, 128);
|
||||||
|
const int width = renderer.getUiTextWidth("CrossPoint", BOLD);
|
||||||
|
renderer.drawUiText((pageWidth - width)/ 2, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
|
||||||
|
const int bootingWidth = renderer.getSmallTextWidth("BOOTING");
|
||||||
|
renderer.drawSmallText((pageWidth - bootingWidth) / 2, pageHeight / 2 + 95, "BOOTING");
|
||||||
|
renderer.flushDisplay();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
#include "EpubReaderScreen.h"
|
#include "EpubReaderScreen.h"
|
||||||
|
|
||||||
#include <EpdRenderer.h>
|
#include <EpdRenderer.h>
|
||||||
|
#include <Epub/Page.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
|
||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
|
|
||||||
constexpr int PAGES_PER_REFRESH = 20;
|
constexpr int PAGES_PER_REFRESH = 15;
|
||||||
constexpr unsigned long SKIP_CHAPTER_MS = 700;
|
constexpr unsigned long SKIP_CHAPTER_MS = 700;
|
||||||
|
|
||||||
void EpubReaderScreen::taskTrampoline(void* param) {
|
void EpubReaderScreen::taskTrampoline(void* param) {
|
||||||
@ -128,7 +129,7 @@ void EpubReaderScreen::displayTaskLoop() {
|
|||||||
if (updateRequired) {
|
if (updateRequired) {
|
||||||
updateRequired = false;
|
updateRequired = false;
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
renderPage();
|
renderScreen();
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
}
|
}
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
@ -136,7 +137,7 @@ void EpubReaderScreen::displayTaskLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Failure handling
|
// TODO: Failure handling
|
||||||
void EpubReaderScreen::renderPage() {
|
void EpubReaderScreen::renderScreen() {
|
||||||
if (!epub) {
|
if (!epub) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -159,10 +160,12 @@ void EpubReaderScreen::renderPage() {
|
|||||||
constexpr int y = 50;
|
constexpr int y = 50;
|
||||||
const int w = textWidth + margin * 2;
|
const int w = textWidth + margin * 2;
|
||||||
const int h = renderer.getLineHeight() + margin * 2;
|
const int h = renderer.getLineHeight() + margin * 2;
|
||||||
|
renderer.swapBuffers();
|
||||||
renderer.fillRect(x, y, w, h, 0);
|
renderer.fillRect(x, y, w, h, 0);
|
||||||
renderer.drawText(x + margin, y + margin, "Indexing...");
|
renderer.drawText(x + margin, y + margin, "Indexing...");
|
||||||
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
|
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
|
||||||
renderer.flushArea(x, y, w, h);
|
renderer.flushDisplay(EInkDisplay::HALF_REFRESH);
|
||||||
|
pagesUntilFullRefresh = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
section->setupCacheDir();
|
section->setupCacheDir();
|
||||||
@ -184,16 +187,29 @@ void EpubReaderScreen::renderPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
section->renderPage();
|
|
||||||
|
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);
|
||||||
renderStatusBar();
|
renderStatusBar();
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
|
||||||
renderer.flushDisplay(false);
|
|
||||||
pagesUntilFullRefresh = PAGES_PER_REFRESH;
|
|
||||||
} else {
|
|
||||||
renderer.flushDisplay();
|
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);
|
||||||
|
renderStatusBar();
|
||||||
|
renderer.flushDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Page* p = section->loadPageFromSD();
|
||||||
|
renderContents(p);
|
||||||
|
delete p;
|
||||||
|
|
||||||
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
|
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
|
||||||
uint8_t data[4];
|
uint8_t data[4];
|
||||||
data[0] = currentSpineIndex & 0xFF;
|
data[0] = currentSpineIndex & 0xFF;
|
||||||
@ -204,6 +220,36 @@ void EpubReaderScreen::renderPage() {
|
|||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EpubReaderScreen::renderContents(const Page* p) {
|
||||||
|
p->render(renderer);
|
||||||
|
renderStatusBar();
|
||||||
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
|
renderer.flushDisplay(EInkDisplay::HALF_REFRESH);
|
||||||
|
pagesUntilFullRefresh = PAGES_PER_REFRESH;
|
||||||
|
} else {
|
||||||
|
renderer.flushDisplay();
|
||||||
|
pagesUntilFullRefresh--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
void EpubReaderScreen::renderStatusBar() const {
|
||||||
const auto pageWidth = renderer.getPageWidth();
|
const auto pageWidth = renderer.getPageWidth();
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,8 @@ class EpubReaderScreen final : public Screen {
|
|||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
void renderPage();
|
void renderScreen();
|
||||||
|
void renderContents(const Page *p);
|
||||||
void renderStatusBar() const;
|
void renderStatusBar() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
@ -51,7 +51,7 @@ void FileSelectionScreen::onEnter() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask",
|
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask",
|
||||||
1024, // Stack size
|
2048, // Stack size
|
||||||
this, // Parameters
|
this, // Parameters
|
||||||
1, // Priority
|
1, // Priority
|
||||||
&displayTaskHandle // Task handle
|
&displayTaskHandle // Task handle
|
||||||
@ -120,7 +120,7 @@ void FileSelectionScreen::render() const {
|
|||||||
|
|
||||||
const auto pageWidth = renderer.getPageWidth();
|
const auto pageWidth = renderer.getPageWidth();
|
||||||
const auto titleWidth = renderer.getTextWidth("CrossPoint Reader", BOLD);
|
const auto titleWidth = renderer.getTextWidth("CrossPoint Reader", BOLD);
|
||||||
renderer.drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", 1, BOLD);
|
renderer.drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", true, BOLD);
|
||||||
|
|
||||||
if (files.empty()) {
|
if (files.empty()) {
|
||||||
renderer.drawUiText(10, 50, "No EPUBs found");
|
renderer.drawUiText(10, 50, "No EPUBs found");
|
||||||
@ -130,7 +130,7 @@ void FileSelectionScreen::render() const {
|
|||||||
|
|
||||||
for (size_t i = 0; i < files.size(); i++) {
|
for (size_t i = 0; i < files.size(); i++) {
|
||||||
const auto file = files[i];
|
const auto file = files[i];
|
||||||
renderer.drawUiText(10, 50 + i * 30, file.c_str(), i == selectorIndex ? 0 : 1);
|
renderer.drawUiText(10, 50 + i * 30, file.c_str(), i != selectorIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ void FullScreenMessageScreen::onEnter() {
|
|||||||
const auto left = (renderer.getPageWidth() - width) / 2;
|
const auto left = (renderer.getPageWidth() - width) / 2;
|
||||||
const auto top = (renderer.getPageHeight() - height) / 2;
|
const auto top = (renderer.getPageHeight() - height) / 2;
|
||||||
|
|
||||||
renderer.clearScreen(invert);
|
renderer.clearScreen();
|
||||||
renderer.drawUiText(left, top, text.c_str(), invert ? 0 : 1, style);
|
renderer.drawUiText(left, top, text.c_str(), true, style);
|
||||||
renderer.flushDisplay(partialUpdate);
|
renderer.flushDisplay(refreshMode);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,23 +2,22 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "EpdFontFamily.h"
|
#include <EInkDisplay.h>
|
||||||
|
#include <EpdFontFamily.h>
|
||||||
#include "Screen.h"
|
#include "Screen.h"
|
||||||
|
|
||||||
class FullScreenMessageScreen final : public Screen {
|
class FullScreenMessageScreen final : public Screen {
|
||||||
std::string text;
|
std::string text;
|
||||||
EpdFontStyle style;
|
EpdFontStyle style;
|
||||||
bool invert;
|
EInkDisplay::RefreshMode refreshMode;
|
||||||
bool partialUpdate;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FullScreenMessageScreen(EpdRenderer& renderer, InputManager& inputManager, std::string text,
|
explicit FullScreenMessageScreen(EpdRenderer& renderer, InputManager& inputManager, std::string text,
|
||||||
const EpdFontStyle style = REGULAR, const bool invert = false,
|
const EpdFontStyle style = REGULAR,
|
||||||
const bool partialUpdate = true)
|
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
|
||||||
: Screen(renderer, inputManager),
|
: Screen(renderer, inputManager),
|
||||||
text(std::move(text)),
|
text(std::move(text)),
|
||||||
style(style),
|
style(style),
|
||||||
invert(invert),
|
refreshMode(refreshMode) {}
|
||||||
partialUpdate(partialUpdate) {}
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,6 +2,18 @@
|
|||||||
|
|
||||||
#include <EpdRenderer.h>
|
#include <EpdRenderer.h>
|
||||||
|
|
||||||
#include "images/SleepScreenImg.h"
|
#include "images/CrossLarge.h"
|
||||||
|
|
||||||
void SleepScreen::onEnter() { renderer.drawImageNoMargin(SleepScreenImg, 0, 0, 800, 480, false, true); }
|
void SleepScreen::onEnter() {
|
||||||
|
const auto pageWidth = renderer.getPageWidth();
|
||||||
|
const auto pageHeight = renderer.getPageHeight();
|
||||||
|
|
||||||
|
renderer.clearScreen();
|
||||||
|
renderer.drawImage(CrossLarge, (pageHeight - 128) / 2, (pageWidth - 128) / 2, 128, 128);
|
||||||
|
const int width = renderer.getUiTextWidth("CrossPoint", BOLD);
|
||||||
|
renderer.drawUiText((pageWidth - width)/ 2, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
|
||||||
|
const int bootingWidth = renderer.getSmallTextWidth("SLEEPING");
|
||||||
|
renderer.drawSmallText((pageWidth - bootingWidth) / 2, pageHeight / 2 + 95, "SLEEPING");
|
||||||
|
renderer.invertScreen();
|
||||||
|
renderer.flushDisplay(EInkDisplay::FULL_REFRESH);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user