Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45af2d0e81 | ||
|
|
0926e9e6e4 | ||
|
|
02b157c02b | ||
|
|
07cc589e59 | ||
|
|
b743a1ca8e | ||
|
|
2ed8017aa2 |
@@ -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;
|
||||
|
||||
@@ -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
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
|
||||
* 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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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("};")
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
#pragma once
|
||||
#include <EpdFontFamily.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
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; }
|
||||
|
||||
template <typename Renderable>
|
||||
class EpdFontRenderer {
|
||||
Renderable& renderer;
|
||||
void renderChar(uint32_t cp, int* x, const int* y, uint16_t color, EpdFontStyle style = REGULAR);
|
||||
|
||||
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, uint16_t color, EpdFontStyle style = REGULAR);
|
||||
};
|
||||
|
||||
template <typename Renderable>
|
||||
void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const uint16_t color,
|
||||
const EpdFontStyle style) {
|
||||
// cannot draw a NULL / empty string
|
||||
if (string == nullptr || *string == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
// no printable characters
|
||||
if (!fontFamily->hasPrintableChars(string, style)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||
renderChar(cp, x, y, color, style);
|
||||
}
|
||||
|
||||
*y += fontFamily->getData(style)->advanceY;
|
||||
}
|
||||
|
||||
template <typename Renderable>
|
||||
void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, uint16_t color,
|
||||
const EpdFontStyle style) {
|
||||
const EpdGlyph* glyph = fontFamily->getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
// TODO: Replace with fallback glyph property?
|
||||
glyph = fontFamily->getGlyph('?', style);
|
||||
}
|
||||
|
||||
// no glyph?
|
||||
if (!glyph) {
|
||||
Serial.printf("No glyph for codepoint %d\n", cp);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t offset = glyph->dataOffset;
|
||||
const uint8_t width = glyph->width;
|
||||
const uint8_t height = glyph->height;
|
||||
const int left = glyph->left;
|
||||
|
||||
const uint8_t* bitmap = nullptr;
|
||||
bitmap = &fontFamily->getData(style)->bitmap[offset];
|
||||
|
||||
if (bitmap != nullptr) {
|
||||
for (int glyphY = 0; glyphY < height; glyphY++) {
|
||||
int screenY = *y - glyph->top + glyphY;
|
||||
for (int glyphX = 0; glyphX < width; glyphX++) {
|
||||
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 ((byte >> bit_index) & 1) {
|
||||
renderer.drawPixel(screenX, screenY, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*x += glyph->advanceX;
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
#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/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);
|
||||
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
|
||||
|
||||
EpdFont smallFont(&babyblue);
|
||||
EpdFontFamily smallFontFamily(&smallFont);
|
||||
|
||||
EpdFont ubuntu10Font(&ubuntu_10);
|
||||
EpdFont ununtuBold10Font(&ubuntu_bold_10);
|
||||
EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ununtuBold10Font);
|
||||
|
||||
EpdRenderer::EpdRenderer(XteinkDisplay& display)
|
||||
: display(display), marginTop(11), marginBottom(30), marginLeft(10), marginRight(10), lineCompression(0.95f) {
|
||||
this->regularFontRenderer = new EpdFontRenderer<XteinkDisplay>(&bookerlyFontFamily, display);
|
||||
this->smallFontRenderer = new EpdFontRenderer<XteinkDisplay>(&smallFontFamily, display);
|
||||
this->uiFontRenderer = new EpdFontRenderer<XteinkDisplay>(&ubuntuFontFamily, display);
|
||||
}
|
||||
|
||||
EpdRenderer::~EpdRenderer() {
|
||||
delete regularFontRenderer;
|
||||
delete smallFontRenderer;
|
||||
delete uiFontRenderer;
|
||||
}
|
||||
|
||||
int EpdRenderer::getTextWidth(const char* text, const EpdFontStyle style) const {
|
||||
int w = 0, h = 0;
|
||||
|
||||
regularFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
int EpdRenderer::getUiTextWidth(const char* text, const EpdFontStyle style) const {
|
||||
int w = 0, h = 0;
|
||||
|
||||
uiFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
int EpdRenderer::getSmallTextWidth(const char* text, const EpdFontStyle style) const {
|
||||
int w = 0, h = 0;
|
||||
|
||||
smallFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
void EpdRenderer::drawText(const int x, const int y, const char* text, const uint16_t color,
|
||||
const EpdFontStyle style) const {
|
||||
int ypos = y + getLineHeight() + marginTop;
|
||||
int xpos = x + marginLeft;
|
||||
regularFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
|
||||
}
|
||||
|
||||
void EpdRenderer::drawUiText(const int x, const int y, const char* text, const uint16_t color,
|
||||
const EpdFontStyle style) const {
|
||||
int ypos = y + uiFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
|
||||
int xpos = x + marginLeft;
|
||||
uiFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
|
||||
}
|
||||
|
||||
void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const uint16_t color,
|
||||
const EpdFontStyle style) const {
|
||||
int ypos = y + smallFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
|
||||
int xpos = x + marginLeft;
|
||||
smallFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
|
||||
}
|
||||
|
||||
void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height,
|
||||
const EpdFontStyle style) const {
|
||||
const size_t length = text.length();
|
||||
// fit the text into the box
|
||||
int start = 0;
|
||||
int end = 1;
|
||||
int ypos = 0;
|
||||
while (true) {
|
||||
if (end >= length) {
|
||||
drawText(x, y + ypos, text.substr(start, length - start).c_str(), 1, style);
|
||||
break;
|
||||
}
|
||||
|
||||
if (ypos + getLineHeight() >= height) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (text[end - 1] == '\n') {
|
||||
drawText(x, y + ypos, text.substr(start, end - start).c_str(), 1, style);
|
||||
ypos += getLineHeight();
|
||||
start = end;
|
||||
end = start + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
ypos += getLineHeight();
|
||||
start = end - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
}
|
||||
|
||||
void EpdRenderer::drawLine(int x1, int y1, int x2, int y2, uint16_t color) const {
|
||||
display.drawLine(x1 + marginLeft, y1 + marginTop, x2 + marginLeft, y2 + marginTop,
|
||||
color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::drawRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
|
||||
display.drawRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::fillRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
|
||||
display.fillRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::drawCircle(const int x, const int y, const int radius, const uint16_t color) const {
|
||||
display.drawCircle(x + marginLeft, y + marginTop, radius, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::fillCircle(const int x, const int y, const int radius, const uint16_t color) const {
|
||||
display.fillCircle(x + marginLeft, y + marginTop, radius, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height,
|
||||
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");
|
||||
display.fillScreen(black ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::flushDisplay(const bool partialUpdate) const { display.display(partialUpdate); }
|
||||
|
||||
void EpdRenderer::flushArea(const int x, const int y, const int width, const int height) const {
|
||||
display.displayWindow(x + marginLeft, y + marginTop, width, height);
|
||||
}
|
||||
|
||||
int EpdRenderer::getPageWidth() const { return display.width() - marginLeft - marginRight; }
|
||||
|
||||
int EpdRenderer::getPageHeight() const { return display.height() - marginTop - marginBottom; }
|
||||
|
||||
int EpdRenderer::getSpaceWidth() const { return regularFontRenderer->fontFamily->getGlyph(' ', REGULAR)->advanceX; }
|
||||
|
||||
int EpdRenderer::getLineHeight() const {
|
||||
return regularFontRenderer->fontFamily->getData(REGULAR)->advanceY * lineCompression;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <GxEPD2_BW.h>
|
||||
|
||||
#include <EpdFontRenderer.hpp>
|
||||
|
||||
#define XteinkDisplay GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT>
|
||||
|
||||
class EpdRenderer {
|
||||
XteinkDisplay& display;
|
||||
EpdFontRenderer<XteinkDisplay>* regularFontRenderer;
|
||||
EpdFontRenderer<XteinkDisplay>* smallFontRenderer;
|
||||
EpdFontRenderer<XteinkDisplay>* uiFontRenderer;
|
||||
int marginTop;
|
||||
int marginBottom;
|
||||
int marginLeft;
|
||||
int marginRight;
|
||||
float lineCompression;
|
||||
|
||||
public:
|
||||
explicit EpdRenderer(XteinkDisplay& display);
|
||||
~EpdRenderer();
|
||||
int getTextWidth(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;
|
||||
void drawText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
|
||||
void drawUiText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
|
||||
void drawSmallText(int x, int y, const char* text, uint16_t color = 1, 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 drawRect(int x, int y, int width, int height, uint16_t color = 1) const;
|
||||
void fillRect(int x, int y, int width, int height, uint16_t color = 1) const;
|
||||
void drawCircle(int x, int y, int radius, uint16_t color = 1) const;
|
||||
void fillCircle(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, bool invert = false,
|
||||
bool mirrorY = false) const;
|
||||
void drawImageNoMargin(const uint8_t bitmap[], int x, int y, int width, int height, bool invert = false,
|
||||
bool mirrorY = false) const;
|
||||
void clearScreen(bool black = false) const;
|
||||
void flushDisplay(bool partialUpdate = true) const;
|
||||
void flushArea(int x, int y, int width, int height) const;
|
||||
|
||||
int getPageWidth() const;
|
||||
int getPageHeight() const;
|
||||
int getSpaceWidth() const;
|
||||
int getLineHeight() const;
|
||||
// set margins
|
||||
void setMarginTop(const int newMarginTop) { this->marginTop = newMarginTop; }
|
||||
void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; }
|
||||
void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; }
|
||||
void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; }
|
||||
};
|
||||
@@ -11,7 +11,7 @@ bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) {
|
||||
size_t s;
|
||||
const auto metaInfo = reinterpret_cast<char*>(zip.readFileToMemory("META-INF/container.xml", &s, true));
|
||||
if (!metaInfo) {
|
||||
Serial.println("Could not find META-INF/container.xml");
|
||||
Serial.printf("[%lu] [EBP] Could not find META-INF/container.xml\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -21,19 +21,19 @@ bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) {
|
||||
free(metaInfo);
|
||||
|
||||
if (result != tinyxml2::XML_SUCCESS) {
|
||||
Serial.printf("Could not parse META-INF/container.xml. Error: %d\n", result);
|
||||
Serial.printf("[%lu] [EBP] Could not parse META-INF/container.xml. Error: %d\n", millis(), result);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto container = metaDataDoc.FirstChildElement("container");
|
||||
if (!container) {
|
||||
Serial.println("Could not find container element in META-INF/container.xml");
|
||||
Serial.printf("[%lu] [EBP] Could not find container element in META-INF/container.xml\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto rootfiles = container->FirstChildElement("rootfiles");
|
||||
if (!rootfiles) {
|
||||
Serial.println("Could not find rootfiles element in META-INF/container.xml");
|
||||
Serial.printf("[%lu] [EBP] Could not find rootfiles element in META-INF/container.xml\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) {
|
||||
rootfile = rootfile->NextSiblingElement("rootfile");
|
||||
}
|
||||
|
||||
Serial.println("Could not get path to content.opf file");
|
||||
Serial.printf("[%lu] [EBP] Could not get path to content.opf file\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,8 @@ bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
|
||||
free(contents);
|
||||
|
||||
if (result != tinyxml2::XML_SUCCESS) {
|
||||
Serial.printf("Error parsing content.opf - %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
|
||||
Serial.printf("[%lu] [EBP] Error parsing content.opf - %s\n", millis(),
|
||||
tinyxml2::XMLDocument::ErrorIDToName(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -73,7 +74,7 @@ bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
|
||||
if (!package) package = doc.FirstChildElement("opf:package");
|
||||
|
||||
if (!package) {
|
||||
Serial.println("Could not find package element in content.opf");
|
||||
Serial.printf("[%lu] [EBP] Could not find package element in content.opf\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -81,13 +82,13 @@ bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
|
||||
auto metadata = package->FirstChildElement("metadata");
|
||||
if (!metadata) metadata = package->FirstChildElement("opf:metadata");
|
||||
if (!metadata) {
|
||||
Serial.println("Missing metadata");
|
||||
Serial.printf("[%lu] [EBP] Missing metadata\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto titleEl = metadata->FirstChildElement("dc:title");
|
||||
if (!titleEl) {
|
||||
Serial.println("Missing title");
|
||||
Serial.printf("[%lu] [EBP] Missing title\n", millis());
|
||||
return false;
|
||||
}
|
||||
this->title = titleEl->GetText();
|
||||
@@ -98,7 +99,7 @@ bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
|
||||
cover = cover->NextSiblingElement("meta");
|
||||
}
|
||||
if (!cover) {
|
||||
Serial.println("Missing cover");
|
||||
Serial.printf("[%lu] [EBP] Missing cover\n", millis());
|
||||
}
|
||||
auto coverItem = cover ? cover->Attribute("content") : nullptr;
|
||||
|
||||
@@ -109,7 +110,7 @@ bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
|
||||
auto manifest = package->FirstChildElement("manifest");
|
||||
if (!manifest) manifest = package->FirstChildElement("opf:manifest");
|
||||
if (!manifest) {
|
||||
Serial.println("Missing manifest");
|
||||
Serial.printf("[%lu] [EBP] Missing manifest\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
|
||||
auto spineEl = package->FirstChildElement("spine");
|
||||
if (!spineEl) spineEl = package->FirstChildElement("opf:spine");
|
||||
if (!spineEl) {
|
||||
Serial.println("Missing spine");
|
||||
Serial.printf("[%lu] [EBP] Missing spine\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -164,13 +165,13 @@ bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
|
||||
bool Epub::parseTocNcxFile(const ZipFile& zip) {
|
||||
// the ncx file should have been specified in the content.opf file
|
||||
if (tocNcxItem.empty()) {
|
||||
Serial.println("No ncx file specified");
|
||||
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto ncxData = reinterpret_cast<char*>(zip.readFileToMemory(tocNcxItem.c_str(), nullptr, true));
|
||||
if (!ncxData) {
|
||||
Serial.printf("Could not find %s\n", tocNcxItem.c_str());
|
||||
Serial.printf("[%lu] [EBP] Could not find %s\n", millis(), tocNcxItem.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -180,19 +181,19 @@ bool Epub::parseTocNcxFile(const ZipFile& zip) {
|
||||
free(ncxData);
|
||||
|
||||
if (result != tinyxml2::XML_SUCCESS) {
|
||||
Serial.printf("Error parsing toc %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
|
||||
Serial.printf("[%lu] [EBP] Error parsing toc %s\n", millis(), tinyxml2::XMLDocument::ErrorIDToName(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto ncx = doc.FirstChildElement("ncx");
|
||||
if (!ncx) {
|
||||
Serial.println("Could not find first child ncx in toc");
|
||||
Serial.printf("[%lu] [EBP] Could not find first child ncx in toc\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto navMap = ncx->FirstChildElement("navMap");
|
||||
if (!navMap) {
|
||||
Serial.println("Could not find navMap child in ncx");
|
||||
Serial.printf("[%lu] [EBP] Could not find navMap child in ncx\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -231,7 +232,7 @@ bool Epub::load() {
|
||||
|
||||
std::string contentOpfFile;
|
||||
if (!findContentOpfFile(zip, contentOpfFile)) {
|
||||
Serial.println("Could not open ePub");
|
||||
Serial.printf("[%lu] [EBP] Could not open ePub\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -314,7 +315,7 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
|
||||
|
||||
const auto content = zip.readFileToMemory(path.c_str(), size, trailingNullByte);
|
||||
if (!content) {
|
||||
Serial.printf("Failed to read item %s\n", path.c_str());
|
||||
Serial.printf("[%lu] [EBP] Failed to read item %s\n", millis(), path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -332,7 +333,7 @@ int Epub::getSpineItemsCount() const { return spine.size(); }
|
||||
|
||||
std::string& Epub::getSpineItem(const int spineIndex) {
|
||||
if (spineIndex < 0 || spineIndex >= spine.size()) {
|
||||
Serial.printf("getSpineItem index:%d is out of range\n", spineIndex);
|
||||
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
||||
return spine.at(0).second;
|
||||
}
|
||||
|
||||
@@ -341,7 +342,7 @@ std::string& Epub::getSpineItem(const int spineIndex) {
|
||||
|
||||
EpubTocEntry& Epub::getTocItem(const int tocTndex) {
|
||||
if (tocTndex < 0 || tocTndex >= toc.size()) {
|
||||
Serial.printf("getTocItem index:%d is out of range\n", tocTndex);
|
||||
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocTndex);
|
||||
return toc.at(0);
|
||||
}
|
||||
|
||||
@@ -360,7 +361,7 @@ int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||
}
|
||||
}
|
||||
|
||||
Serial.println("Section not found");
|
||||
Serial.printf("[%lu] [EBP] Section not found\n", millis());
|
||||
// not found - default to the start of the book
|
||||
return 0;
|
||||
}
|
||||
@@ -374,7 +375,7 @@ int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
|
||||
}
|
||||
}
|
||||
|
||||
Serial.println("TOC item not found");
|
||||
Serial.printf("[%lu] [EBP] TOC item not found\n", millis());
|
||||
// not found - default to first item
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include <HardwareSerial.h>
|
||||
#include <Print.h>
|
||||
#include <tinyxml2.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "EpubHtmlParserSlim.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <expat.h>
|
||||
|
||||
@@ -133,7 +133,7 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s
|
||||
}
|
||||
|
||||
// If we're about to run out of space, then cut the word off and start a new one
|
||||
if (self->partWordBufferIndex >= PART_WORD_BUFFER_SIZE - 2) {
|
||||
if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
|
||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
|
||||
self->italicUntilDepth < self->depth);
|
||||
@@ -190,7 +190,7 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
||||
int done;
|
||||
|
||||
if (!parser) {
|
||||
Serial.println("Couldn't allocate memory for parser");
|
||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for parser\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
||||
|
||||
FILE* file = fopen(filepath, "r");
|
||||
if (!file) {
|
||||
Serial.printf("Couldn't open file %s\n", filepath);
|
||||
Serial.printf("[%lu] [EHP] Couldn't open file %s\n", millis(), filepath);
|
||||
XML_ParserFree(parser);
|
||||
return false;
|
||||
}
|
||||
@@ -208,7 +208,7 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
||||
do {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
if (!buf) {
|
||||
Serial.println("Couldn't allocate memory for buffer");
|
||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for buffer\n", millis());
|
||||
XML_ParserFree(parser);
|
||||
fclose(file);
|
||||
return false;
|
||||
@@ -217,7 +217,7 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
||||
const size_t len = fread(buf, 1, 1024, file);
|
||||
|
||||
if (ferror(file)) {
|
||||
Serial.println("Read error");
|
||||
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
||||
XML_ParserFree(parser);
|
||||
fclose(file);
|
||||
return false;
|
||||
@@ -226,7 +226,7 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
||||
done = feof(file);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||
Serial.printf("Parse error at line %lu:\n%s\n", XML_GetCurrentLineNumber(parser),
|
||||
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
XML_ParserFree(parser);
|
||||
fclose(file);
|
||||
@@ -251,34 +251,36 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
||||
|
||||
void EpubHtmlParserSlim::makePages() {
|
||||
if (!currentTextBlock) {
|
||||
Serial.println("!! No text block to make pages for !!");
|
||||
Serial.printf("[%lu] [EHP] !! No text block to make pages for !!\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentPage) {
|
||||
currentPage = new Page();
|
||||
currentPageNextY = marginTop;
|
||||
}
|
||||
|
||||
const int lineHeight = renderer.getLineHeight();
|
||||
const int pageHeight = renderer.getPageHeight();
|
||||
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||
const int pageHeight = GfxRenderer::getScreenHeight() - marginTop - marginBottom;
|
||||
|
||||
// Long running task, make sure to let other things happen
|
||||
vTaskDelay(1);
|
||||
|
||||
if (currentTextBlock->getType() == TEXT_BLOCK) {
|
||||
const auto lines = currentTextBlock->splitIntoLines(renderer);
|
||||
const auto lines = currentTextBlock->splitIntoLines(renderer, fontId, marginLeft + marginRight);
|
||||
|
||||
for (const auto line : lines) {
|
||||
if (currentPage->nextY + lineHeight > pageHeight) {
|
||||
if (currentPageNextY + lineHeight > pageHeight) {
|
||||
completePageFn(currentPage);
|
||||
currentPage = new Page();
|
||||
currentPageNextY = marginTop;
|
||||
}
|
||||
|
||||
currentPage->elements.push_back(new PageLine(line, currentPage->nextY));
|
||||
currentPage->nextY += lineHeight;
|
||||
currentPage->elements.push_back(new PageLine(line, marginLeft, currentPageNextY));
|
||||
currentPageNextY += lineHeight;
|
||||
}
|
||||
// add some extra line between blocks
|
||||
currentPage->nextY += lineHeight / 2;
|
||||
currentPageNextY += lineHeight / 2;
|
||||
}
|
||||
// TODO: Image block support
|
||||
// if (block->getType() == BlockType::IMAGE_BLOCK) {
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <expat.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include <climits>
|
||||
#include <functional>
|
||||
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
class Page;
|
||||
class EpdRenderer;
|
||||
class GfxRenderer;
|
||||
|
||||
#define PART_WORD_BUFFER_SIZE 200
|
||||
#define MAX_WORD_SIZE 200
|
||||
|
||||
class EpubHtmlParserSlim {
|
||||
const char* filepath;
|
||||
EpdRenderer& renderer;
|
||||
GfxRenderer& renderer;
|
||||
std::function<void(Page*)> completePageFn;
|
||||
int depth = 0;
|
||||
int skipUntilDepth = INT_MAX;
|
||||
int boldUntilDepth = INT_MAX;
|
||||
int italicUntilDepth = INT_MAX;
|
||||
// If we encounter words longer than this, but this is pretty large
|
||||
char partWordBuffer[PART_WORD_BUFFER_SIZE] = {};
|
||||
// buffer for building up words from characters, will auto break if longer than this
|
||||
// leave one char at end for null pointer
|
||||
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
|
||||
int partWordBufferIndex = 0;
|
||||
TextBlock* currentTextBlock = nullptr;
|
||||
Page* currentPage = nullptr;
|
||||
int currentPageNextY = 0;
|
||||
int fontId;
|
||||
float lineCompression;
|
||||
int marginTop;
|
||||
int marginRight;
|
||||
int marginBottom;
|
||||
int marginLeft;
|
||||
|
||||
void startNewTextBlock(BLOCK_STYLE style);
|
||||
void makePages();
|
||||
@@ -34,9 +42,19 @@ class EpubHtmlParserSlim {
|
||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
explicit EpubHtmlParserSlim(const char* filepath, EpdRenderer& renderer,
|
||||
explicit EpubHtmlParserSlim(const char* filepath, GfxRenderer& renderer, const int fontId,
|
||||
const float lineCompression, const int marginTop, const int marginRight,
|
||||
const int marginBottom, const int marginLeft,
|
||||
const std::function<void(Page*)>& completePageFn)
|
||||
: filepath(filepath), renderer(renderer), completePageFn(completePageFn) {}
|
||||
: filepath(filepath),
|
||||
renderer(renderer),
|
||||
fontId(fontId),
|
||||
lineCompression(lineCompression),
|
||||
marginTop(marginTop),
|
||||
marginRight(marginRight),
|
||||
marginBottom(marginBottom),
|
||||
marginLeft(marginLeft),
|
||||
completePageFn(completePageFn) {}
|
||||
~EpubHtmlParserSlim() = default;
|
||||
bool parseAndBuildPages();
|
||||
};
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
#include <HardwareSerial.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
void PageLine::render(EpdRenderer& renderer) { block->render(renderer, 0, yPos); }
|
||||
constexpr uint8_t PAGE_FILE_VERSION = 1;
|
||||
|
||||
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }
|
||||
|
||||
void PageLine::serialize(std::ostream& os) {
|
||||
serialization::writePod(os, xPos);
|
||||
serialization::writePod(os, yPos);
|
||||
|
||||
// serialize TextBlock pointed to by PageLine
|
||||
@@ -13,23 +16,23 @@ void PageLine::serialize(std::ostream& os) {
|
||||
}
|
||||
|
||||
PageLine* PageLine::deserialize(std::istream& is) {
|
||||
int32_t xPos;
|
||||
int32_t yPos;
|
||||
serialization::readPod(is, xPos);
|
||||
serialization::readPod(is, yPos);
|
||||
|
||||
const auto tb = TextBlock::deserialize(is);
|
||||
return new PageLine(tb, yPos);
|
||||
return new PageLine(tb, xPos, yPos);
|
||||
}
|
||||
|
||||
void Page::render(EpdRenderer& renderer) const {
|
||||
const auto start = millis();
|
||||
void Page::render(GfxRenderer& renderer, const int fontId) const {
|
||||
for (const auto element : elements) {
|
||||
element->render(renderer);
|
||||
element->render(renderer, fontId);
|
||||
}
|
||||
Serial.printf("Rendered page elements (%u) in %dms\n", elements.size(), millis() - start);
|
||||
}
|
||||
|
||||
void Page::serialize(std::ostream& os) const {
|
||||
serialization::writePod(os, nextY);
|
||||
serialization::writePod(os, PAGE_FILE_VERSION);
|
||||
|
||||
const uint32_t count = elements.size();
|
||||
serialization::writePod(os, count);
|
||||
@@ -42,9 +45,14 @@ void Page::serialize(std::ostream& os) const {
|
||||
}
|
||||
|
||||
Page* Page::deserialize(std::istream& is) {
|
||||
auto* page = new Page();
|
||||
uint8_t version;
|
||||
serialization::readPod(is, version);
|
||||
if (version != PAGE_FILE_VERSION) {
|
||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
serialization::readPod(is, page->nextY);
|
||||
auto* page = new Page();
|
||||
|
||||
uint32_t count;
|
||||
serialization::readPod(is, count);
|
||||
|
||||
@@ -8,10 +8,11 @@ enum PageElementTag : uint8_t {
|
||||
// represents something that has been added to a page
|
||||
class PageElement {
|
||||
public:
|
||||
int xPos;
|
||||
int yPos;
|
||||
explicit PageElement(const int yPos) : yPos(yPos) {}
|
||||
explicit PageElement(const int xPos, const int yPos) : xPos(xPos), yPos(yPos) {}
|
||||
virtual ~PageElement() = default;
|
||||
virtual void render(EpdRenderer& renderer) = 0;
|
||||
virtual void render(GfxRenderer& renderer, int fontId) = 0;
|
||||
virtual void serialize(std::ostream& os) = 0;
|
||||
};
|
||||
|
||||
@@ -20,24 +21,24 @@ class PageLine final : public PageElement {
|
||||
const TextBlock* block;
|
||||
|
||||
public:
|
||||
PageLine(const TextBlock* block, const int yPos) : PageElement(yPos), block(block) {}
|
||||
PageLine(const TextBlock* block, const int xPos, const int yPos) : PageElement(xPos, yPos), block(block) {}
|
||||
~PageLine() override { delete block; }
|
||||
void render(EpdRenderer& renderer) override;
|
||||
void render(GfxRenderer& renderer, int fontId) override;
|
||||
void serialize(std::ostream& os) override;
|
||||
static PageLine* deserialize(std::istream& is);
|
||||
};
|
||||
|
||||
class Page {
|
||||
public:
|
||||
int nextY = 0;
|
||||
// the list of block index and line numbers on this page
|
||||
std::vector<PageElement*> elements;
|
||||
void render(EpdRenderer& renderer) const;
|
||||
~Page() {
|
||||
for (const auto element : elements) {
|
||||
delete element;
|
||||
}
|
||||
}
|
||||
|
||||
// the list of block index and line numbers on this page
|
||||
std::vector<PageElement*> elements;
|
||||
void render(GfxRenderer& renderer, int fontId) const;
|
||||
void serialize(std::ostream& os) const;
|
||||
static Page* deserialize(std::istream& is);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "Section.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include <fstream>
|
||||
@@ -9,29 +9,37 @@
|
||||
#include "Page.h"
|
||||
#include "Serialization.h"
|
||||
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 2;
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 3;
|
||||
|
||||
void Section::onPageComplete(const Page* page) {
|
||||
Serial.printf("Page %d complete - free mem: %lu\n", pageCount, ESP.getFreeHeap());
|
||||
|
||||
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
|
||||
|
||||
std::ofstream outputFile("/sd" + filePath);
|
||||
page->serialize(outputFile);
|
||||
outputFile.close();
|
||||
|
||||
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
|
||||
|
||||
pageCount++;
|
||||
delete page;
|
||||
}
|
||||
|
||||
void Section::writeCacheMetadata() const {
|
||||
void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
||||
const int marginRight, const int marginBottom, const int marginLeft) const {
|
||||
std::ofstream outputFile(("/sd" + cachePath + "/section.bin").c_str());
|
||||
serialization::writePod(outputFile, SECTION_FILE_VERSION);
|
||||
serialization::writePod(outputFile, fontId);
|
||||
serialization::writePod(outputFile, lineCompression);
|
||||
serialization::writePod(outputFile, marginTop);
|
||||
serialization::writePod(outputFile, marginRight);
|
||||
serialization::writePod(outputFile, marginBottom);
|
||||
serialization::writePod(outputFile, marginLeft);
|
||||
serialization::writePod(outputFile, pageCount);
|
||||
outputFile.close();
|
||||
}
|
||||
|
||||
bool Section::loadCacheMetadata() {
|
||||
bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
||||
const int marginRight, const int marginBottom, const int marginLeft) {
|
||||
if (!SD.exists(cachePath.c_str())) {
|
||||
return false;
|
||||
}
|
||||
@@ -42,17 +50,39 @@ bool Section::loadCacheMetadata() {
|
||||
}
|
||||
|
||||
std::ifstream inputFile(("/sd" + sectionFilePath).c_str());
|
||||
uint8_t version;
|
||||
serialization::readPod(inputFile, version);
|
||||
if (version != SECTION_FILE_VERSION) {
|
||||
inputFile.close();
|
||||
SD.remove(sectionFilePath.c_str());
|
||||
Serial.printf("Section state file: Unknown version %u\n", version);
|
||||
return false;
|
||||
|
||||
// Match parameters
|
||||
{
|
||||
uint8_t version;
|
||||
serialization::readPod(inputFile, version);
|
||||
if (version != SECTION_FILE_VERSION) {
|
||||
inputFile.close();
|
||||
clearCache();
|
||||
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||
return false;
|
||||
}
|
||||
|
||||
int fileFontId, fileMarginTop, fileMarginRight, fileMarginBottom, fileMarginLeft;
|
||||
float fileLineCompression;
|
||||
serialization::readPod(inputFile, fileFontId);
|
||||
serialization::readPod(inputFile, fileLineCompression);
|
||||
serialization::readPod(inputFile, fileMarginTop);
|
||||
serialization::readPod(inputFile, fileMarginRight);
|
||||
serialization::readPod(inputFile, fileMarginBottom);
|
||||
serialization::readPod(inputFile, fileMarginLeft);
|
||||
|
||||
if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop ||
|
||||
marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft) {
|
||||
inputFile.close();
|
||||
clearCache();
|
||||
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
serialization::readPod(inputFile, pageCount);
|
||||
inputFile.close();
|
||||
Serial.printf("Loaded cache: %d pages\n", pageCount);
|
||||
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -63,7 +93,8 @@ void Section::setupCacheDir() const {
|
||||
|
||||
void Section::clearCache() const { SD.rmdir(cachePath.c_str()); }
|
||||
|
||||
bool Section::persistPageDataToSD() {
|
||||
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
|
||||
const int marginRight, const int marginBottom, const int marginLeft) {
|
||||
const auto localPath = epub->getSpineItem(spineIndex);
|
||||
|
||||
// TODO: Should we get rid of this file all together?
|
||||
@@ -75,44 +106,38 @@ bool Section::persistPageDataToSD() {
|
||||
f.close();
|
||||
|
||||
if (!success) {
|
||||
Serial.println("Failed to stream item contents");
|
||||
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("Streamed HTML to %s\n", tmpHtmlPath.c_str());
|
||||
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s\n", millis(), tmpHtmlPath.c_str());
|
||||
|
||||
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
|
||||
|
||||
auto visitor =
|
||||
EpubHtmlParserSlim(sdTmpHtmlPath.c_str(), renderer, [this](const Page* page) { this->onPageComplete(page); });
|
||||
auto visitor = EpubHtmlParserSlim(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
|
||||
marginBottom, marginLeft, [this](const Page* page) { this->onPageComplete(page); });
|
||||
success = visitor.parseAndBuildPages();
|
||||
|
||||
SD.remove(tmpHtmlPath.c_str());
|
||||
if (!success) {
|
||||
Serial.println("Failed to parse and build pages");
|
||||
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
writeCacheMetadata();
|
||||
writeCacheMetadata(fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft);
|
||||
|
||||
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("[%lu] [SCT] Page file does not exist: %s\n", millis(), filePath.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::ifstream inputFile(filePath);
|
||||
Page* p = Page::deserialize(inputFile);
|
||||
inputFile.close();
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -2,29 +2,32 @@
|
||||
#include "Epub.h"
|
||||
|
||||
class Page;
|
||||
class EpdRenderer;
|
||||
class GfxRenderer;
|
||||
|
||||
class Section {
|
||||
Epub* epub;
|
||||
const int spineIndex;
|
||||
EpdRenderer& renderer;
|
||||
GfxRenderer& renderer;
|
||||
std::string cachePath;
|
||||
|
||||
void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
||||
int marginLeft) const;
|
||||
void onPageComplete(const Page* page);
|
||||
|
||||
public:
|
||||
int pageCount = 0;
|
||||
int currentPage = 0;
|
||||
|
||||
explicit Section(Epub* epub, const int spineIndex, EpdRenderer& renderer)
|
||||
explicit Section(Epub* epub, const int spineIndex, GfxRenderer& renderer)
|
||||
: epub(epub), spineIndex(spineIndex), renderer(renderer) {
|
||||
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
|
||||
}
|
||||
~Section() = default;
|
||||
void writeCacheMetadata() const;
|
||||
bool loadCacheMetadata();
|
||||
bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
||||
int marginLeft);
|
||||
void setupCacheDir() const;
|
||||
void clearCache() const;
|
||||
bool persistPageDataToSD();
|
||||
void renderPage() const;
|
||||
bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
||||
int marginLeft);
|
||||
Page* loadPageFromSD() const;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
class EpdRenderer;
|
||||
class GfxRenderer;
|
||||
|
||||
typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
||||
|
||||
@@ -8,7 +8,7 @@ typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
||||
class Block {
|
||||
public:
|
||||
virtual ~Block() = default;
|
||||
virtual void layout(EpdRenderer& renderer) = 0;
|
||||
virtual void layout(GfxRenderer& renderer) = 0;
|
||||
virtual BlockType getType() = 0;
|
||||
virtual bool isEmpty() = 0;
|
||||
virtual void finish() {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "TextBlock.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
void TextBlock::addWord(const std::string& word, const bool is_bold, const bool is_italic) {
|
||||
@@ -10,10 +10,11 @@ void TextBlock::addWord(const std::string& word, const bool is_bold, const bool
|
||||
wordStyles.push_back((is_bold ? BOLD_SPAN : 0) | (is_italic ? ITALIC_SPAN : 0));
|
||||
}
|
||||
|
||||
std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer& renderer) {
|
||||
std::list<TextBlock*> TextBlock::splitIntoLines(const GfxRenderer& renderer, const int fontId,
|
||||
const int horizontalMargin) {
|
||||
const int totalWordCount = words.size();
|
||||
const int pageWidth = renderer.getPageWidth();
|
||||
const int spaceWidth = renderer.getSpaceWidth();
|
||||
const int pageWidth = GfxRenderer::getScreenWidth() - horizontalMargin;
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||
|
||||
words.shrink_to_fit();
|
||||
wordStyles.shrink_to_fit();
|
||||
@@ -21,7 +22,7 @@ std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer& renderer) {
|
||||
|
||||
// measure each word
|
||||
uint16_t wordWidths[totalWordCount];
|
||||
for (int i = 0; i < words.size(); i++) {
|
||||
for (int i = 0; i < totalWordCount; i++) {
|
||||
// measure the word
|
||||
EpdFontStyle fontStyle = REGULAR;
|
||||
if (wordStyles[i] & BOLD_SPAN) {
|
||||
@@ -33,7 +34,7 @@ std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer& renderer) {
|
||||
} else if (wordStyles[i] & ITALIC_SPAN) {
|
||||
fontStyle = ITALIC;
|
||||
}
|
||||
const int width = renderer.getTextWidth(words[i].c_str(), fontStyle);
|
||||
const int width = renderer.getTextWidth(fontId, words[i].c_str(), fontStyle);
|
||||
wordWidths[i] = width;
|
||||
}
|
||||
|
||||
@@ -154,7 +155,7 @@ std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer& renderer) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
void TextBlock::render(const EpdRenderer& renderer, const int x, const int y) const {
|
||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||
for (int i = 0; i < words.size(); i++) {
|
||||
// render the word
|
||||
EpdFontStyle fontStyle = REGULAR;
|
||||
@@ -165,7 +166,7 @@ void TextBlock::render(const EpdRenderer& renderer, const int x, const int y) co
|
||||
} else if (wordStyles[i] & ITALIC_SPAN) {
|
||||
fontStyle = ITALIC;
|
||||
}
|
||||
renderer.drawText(x + wordXpos[i], y, words[i].c_str(), 1, fontStyle);
|
||||
renderer.drawText(fontId, x + wordXpos[i], y, words[i].c_str(), true, fontStyle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,10 +40,10 @@ class TextBlock final : public Block {
|
||||
void setStyle(const BLOCK_STYLE style) { this->style = style; }
|
||||
BLOCK_STYLE getStyle() const { return style; }
|
||||
bool isEmpty() override { return words.empty(); }
|
||||
void layout(EpdRenderer& renderer) override {};
|
||||
void layout(GfxRenderer& renderer) override {};
|
||||
// given a renderer works out where to break the words into lines
|
||||
std::list<TextBlock*> splitIntoLines(const EpdRenderer& renderer);
|
||||
void render(const EpdRenderer& renderer, int x, int y) const;
|
||||
std::list<TextBlock*> splitIntoLines(const GfxRenderer& renderer, int fontId, int horizontalMargin);
|
||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||
BlockType getType() override { return TEXT_BLOCK; }
|
||||
void serialize(std::ostream& os) const;
|
||||
static TextBlock* deserialize(std::istream& is);
|
||||
|
||||
229
lib/GfxRenderer/GfxRenderer.cpp
Normal file
229
lib/GfxRenderer/GfxRenderer.cpp
Normal file
@@ -0,0 +1,229 @@
|
||||
#include "GfxRenderer.h"
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||
|
||||
void GfxRenderer::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("[%lu] [GFX] !! No framebuffer\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
// Rotate coordinates: portrait (480x800) -> landscape (800x480)
|
||||
// Rotation: 90 degrees clockwise
|
||||
const int rotatedX = y;
|
||||
const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||
|
||||
// Bounds checking (portrait: 480x800)
|
||||
if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 ||
|
||||
rotatedY >= EInkDisplay::DISPLAY_HEIGHT) {
|
||||
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d)\n", millis(), x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontStyle style) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int w = 0, h = 0;
|
||||
fontMap.at(fontId).getTextDimensions(text, &w, &h, style);
|
||||
return w;
|
||||
}
|
||||
|
||||
void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* text, const bool black,
|
||||
const EpdFontStyle style) const {
|
||||
const int x = (getScreenWidth() - getTextWidth(fontId, text, style)) / 2;
|
||||
drawText(fontId, x, y, text, black, style);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black,
|
||||
const EpdFontStyle style) const {
|
||||
const int yPos = y + getLineHeight(fontId);
|
||||
int xpos = x;
|
||||
|
||||
// cannot draw a NULL / empty string
|
||||
if (text == nullptr || *text == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
return;
|
||||
}
|
||||
const auto font = fontMap.at(fontId);
|
||||
|
||||
// no printable characters
|
||||
if (!font.hasPrintableChars(text, style)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
renderChar(font, cp, &xpos, &yPos, black, style);
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
|
||||
if (x1 == x2) {
|
||||
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.printf("[%lu] [GFX] Line drawing not supported\n", millis());
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||
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 GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||
for (int fillY = y; fillY < y + height; fillY++) {
|
||||
drawLine(x, fillY, x + width - 1, fillY, state);
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
||||
// Flip X and Y for portrait mode
|
||||
einkDisplay.drawImage(bitmap, y, x, height, width);
|
||||
}
|
||||
|
||||
void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); }
|
||||
|
||||
void GfxRenderer::invertScreen() const {
|
||||
uint8_t* buffer = einkDisplay.getFrameBuffer();
|
||||
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
|
||||
buffer[i] = ~buffer[i];
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const {
|
||||
einkDisplay.displayBuffer(refreshMode);
|
||||
}
|
||||
|
||||
// TODO: Support partial window update
|
||||
// void GfxRenderer::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);
|
||||
// }
|
||||
|
||||
// Note: Internal driver treats screen in command orientation, this library treats in portrait orientation
|
||||
int GfxRenderer::getScreenWidth() { return EInkDisplay::DISPLAY_HEIGHT; }
|
||||
int GfxRenderer::getScreenHeight() { return EInkDisplay::DISPLAY_WIDTH; }
|
||||
|
||||
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return fontMap.at(fontId).getGlyph(' ', REGULAR)->advanceX;
|
||||
}
|
||||
|
||||
int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return fontMap.at(fontId).getData(REGULAR)->advanceY;
|
||||
}
|
||||
|
||||
void GfxRenderer::swapBuffers() const { einkDisplay.swapBuffers(); }
|
||||
|
||||
void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }
|
||||
|
||||
void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); }
|
||||
|
||||
void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); }
|
||||
|
||||
void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y,
|
||||
const bool pixelState, const EpdFontStyle style) const {
|
||||
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
// TODO: Replace with fallback glyph property?
|
||||
glyph = fontFamily.getGlyph('?', style);
|
||||
}
|
||||
|
||||
// no glyph?
|
||||
if (!glyph) {
|
||||
Serial.printf("[%lu] [GFX] No glyph for codepoint %d\n", millis(), cp);
|
||||
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;
|
||||
const int left = glyph->left;
|
||||
|
||||
const uint8_t* bitmap = nullptr;
|
||||
bitmap = &fontFamily.getData(style)->bitmap[offset];
|
||||
|
||||
if (bitmap != nullptr) {
|
||||
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;
|
||||
|
||||
const uint8_t val = (byte >> bit_index) & 0x3;
|
||||
if (fontRenderMode == BW && val > 0) {
|
||||
drawPixel(screenX, screenY, pixelState);
|
||||
} else if (fontRenderMode == GRAYSCALE_MSB && val == 1) {
|
||||
// TODO: Not sure how this anti-aliasing goes on black backgrounds
|
||||
drawPixel(screenX, screenY, false);
|
||||
} else if (fontRenderMode == 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*x += glyph->advanceX;
|
||||
}
|
||||
54
lib/GfxRenderer/GfxRenderer.h
Normal file
54
lib/GfxRenderer/GfxRenderer.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <EInkDisplay.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "EpdFontFamily.h"
|
||||
|
||||
class GfxRenderer {
|
||||
public:
|
||||
enum FontRenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
|
||||
|
||||
private:
|
||||
EInkDisplay& einkDisplay;
|
||||
FontRenderMode fontRenderMode;
|
||||
std::map<int, EpdFontFamily> fontMap;
|
||||
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
|
||||
EpdFontStyle style) const;
|
||||
|
||||
public:
|
||||
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), fontRenderMode(BW) {}
|
||||
~GfxRenderer() = default;
|
||||
|
||||
// Setup
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
|
||||
// Screen ops
|
||||
static int getScreenWidth();
|
||||
static int getScreenHeight();
|
||||
void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
|
||||
void invertScreen() const;
|
||||
void clearScreen(uint8_t color = 0xFF) const;
|
||||
|
||||
// Drawing
|
||||
void drawPixel(int x, int y, bool state = true) 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, bool state = true) const;
|
||||
void fillRect(int x, int y, int width, int height, bool state = true) const;
|
||||
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||
|
||||
// Text
|
||||
int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const;
|
||||
void drawCenteredText(int fontId, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
|
||||
void drawText(int fontId, int x, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
|
||||
void setFontRenderMode(const FontRenderMode mode) { this->fontRenderMode = mode; }
|
||||
int getSpaceWidth(int fontId) const;
|
||||
int getLineHeight(int fontId) const;
|
||||
|
||||
// Low level functions
|
||||
void swapBuffers() const;
|
||||
void copyGrayscaleLsbBuffers() const;
|
||||
void copyGrayscaleMsbBuffers() const;
|
||||
void displayGrayBuffer() const;
|
||||
};
|
||||
@@ -7,7 +7,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
|
||||
// Setup inflator
|
||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||
if (!inflator) {
|
||||
Serial.println("Failed to allocate memory for inflator");
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
||||
return false;
|
||||
}
|
||||
memset(inflator, 0, sizeof(tinfl_decompressor));
|
||||
@@ -20,7 +20,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
|
||||
free(inflator);
|
||||
|
||||
if (status != TINFL_STATUS_DONE) {
|
||||
Serial.printf("tinfl_decompress() failed with status %d\n", status);
|
||||
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -32,20 +32,22 @@ bool ZipFile::loadFileStat(const char* filename, mz_zip_archive_file_stat* fileS
|
||||
const bool status = mz_zip_reader_init_file(&zipArchive, filePath.c_str(), 0);
|
||||
|
||||
if (!status) {
|
||||
Serial.printf("mz_zip_reader_init_file() failed!\nError %s\n", mz_zip_get_error_string(zipArchive.m_last_error));
|
||||
Serial.printf("[%lu] [ZIP] mz_zip_reader_init_file() failed! Error: %s\n", millis(),
|
||||
mz_zip_get_error_string(zipArchive.m_last_error));
|
||||
return false;
|
||||
}
|
||||
|
||||
// find the file
|
||||
mz_uint32 fileIndex = 0;
|
||||
if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) {
|
||||
Serial.printf("Could not find file %s\n", filename);
|
||||
Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis, filename);
|
||||
mz_zip_reader_end(&zipArchive);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mz_zip_reader_file_stat(&zipArchive, fileIndex, fileStat)) {
|
||||
Serial.printf("mz_zip_reader_file_stat() failed!\nError %s\n", mz_zip_get_error_string(zipArchive.m_last_error));
|
||||
Serial.printf("[%lu] [ZIP] mz_zip_reader_file_stat() failed! Error: %s\n", millis(),
|
||||
mz_zip_get_error_string(zipArchive.m_last_error));
|
||||
mz_zip_reader_end(&zipArchive);
|
||||
return false;
|
||||
}
|
||||
@@ -65,13 +67,13 @@ long ZipFile::getDataOffset(const mz_zip_archive_file_stat& fileStat) const {
|
||||
fclose(file);
|
||||
|
||||
if (read != localHeaderSize) {
|
||||
Serial.println("Something went wrong reading the local header");
|
||||
Serial.printf("[%lu] [ZIP] Something went wrong reading the local header\n", millis());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
||||
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
|
||||
Serial.println("Not a valid zip file header");
|
||||
Serial.printf("[%lu] [ZIP] Not a valid zip file header\n", millis());
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -105,7 +107,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
fclose(file);
|
||||
|
||||
if (dataRead != inflatedDataSize) {
|
||||
Serial.println("Failed to read data");
|
||||
Serial.printf("[%lu] [ZIP] Failed to read data\n", millis());
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -115,7 +117,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
// Read out deflated content from file
|
||||
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
||||
if (deflatedData == nullptr) {
|
||||
Serial.println("Failed to allocate memory for decompression buffer");
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for decompression buffer\n", millis());
|
||||
fclose(file);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -124,7 +126,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
fclose(file);
|
||||
|
||||
if (dataRead != deflatedDataSize) {
|
||||
Serial.printf("Failed to read data, expected %d got %d\n", deflatedDataSize, dataRead);
|
||||
Serial.printf("[%lu] [ZIP] Failed to read data, expected %d got %d\n", millis(), deflatedDataSize, dataRead);
|
||||
free(deflatedData);
|
||||
free(data);
|
||||
return nullptr;
|
||||
@@ -134,14 +136,14 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
free(deflatedData);
|
||||
|
||||
if (!success) {
|
||||
Serial.println("Failed to inflate file");
|
||||
Serial.printf("[%lu] [ZIP] Failed to inflate file\n", millis());
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Continue out of block with data set
|
||||
} else {
|
||||
Serial.println("Unsupported compression method");
|
||||
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
||||
fclose(file);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -172,7 +174,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
// no deflation, just read content
|
||||
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||
if (!buffer) {
|
||||
Serial.println("Failed to allocate memory for buffer");
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for buffer\n", millis());
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
@@ -181,7 +183,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
while (remaining > 0) {
|
||||
const size_t dataRead = fread(buffer, 1, remaining < chunkSize ? remaining : chunkSize, file);
|
||||
if (dataRead == 0) {
|
||||
Serial.println("Could not read more bytes");
|
||||
Serial.printf("[%lu] [ZIP] Could not read more bytes\n", millis());
|
||||
free(buffer);
|
||||
fclose(file);
|
||||
return false;
|
||||
@@ -200,7 +202,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
// Setup inflator
|
||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||
if (!inflator) {
|
||||
Serial.println("Failed to allocate memory for inflator");
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
@@ -210,7 +212,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
// Setup file read buffer
|
||||
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||
if (!fileReadBuffer) {
|
||||
Serial.println("Failed to allocate memory for zip file read buffer");
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for zip file read buffer\n", millis());
|
||||
free(inflator);
|
||||
fclose(file);
|
||||
return false;
|
||||
@@ -218,7 +220,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
|
||||
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
|
||||
if (!outputBuffer) {
|
||||
Serial.println("Failed to allocate memory for dictionary");
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for dictionary\n", millis());
|
||||
free(inflator);
|
||||
free(fileReadBuffer);
|
||||
fclose(file);
|
||||
@@ -271,11 +273,8 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
outputCursor = (outputCursor + outBytes) & (TINFL_LZ_DICT_SIZE - 1);
|
||||
}
|
||||
|
||||
Serial.printf("Decompressing - %d/%d deflated into %d/%d inflated\n", deflatedDataSize - fileRemainingBytes,
|
||||
deflatedDataSize, processedOutputBytes, inflatedDataSize);
|
||||
|
||||
if (status < 0) {
|
||||
Serial.printf("tinfl_decompress() failed with status %d\n", status);
|
||||
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
||||
fclose(file);
|
||||
free(outputBuffer);
|
||||
free(fileReadBuffer);
|
||||
@@ -284,7 +283,8 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
}
|
||||
|
||||
if (status == TINFL_STATUS_DONE) {
|
||||
Serial.println("Decompression finished");
|
||||
Serial.printf("[%lu] [ZIP] Decompressed %d bytes into %d bytes\n", millis(), deflatedDataSize,
|
||||
inflatedDataSize);
|
||||
fclose(file);
|
||||
free(inflator);
|
||||
free(fileReadBuffer);
|
||||
@@ -294,7 +294,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
}
|
||||
|
||||
// If we get here, EOF reached without TINFL_STATUS_DONE
|
||||
Serial.println("Unexpected EOF");
|
||||
Serial.printf("[%lu] [ZIP] Unexpected EOF\n", millis());
|
||||
fclose(file);
|
||||
free(outputBuffer);
|
||||
free(fileReadBuffer);
|
||||
@@ -302,6 +302,6 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.println("Unsupported compression method");
|
||||
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
Submodule open-x4-sdk updated: 8224d278c5...a126d4b0bf
@@ -1,14 +1,8 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
[platformio]
|
||||
crosspoint_version = 0.3.0
|
||||
default_envs = default
|
||||
|
||||
[env:esp32-c3-devkitm-1]
|
||||
[base]
|
||||
platform = espressif32
|
||||
board = esp32-c3-devkitm-1
|
||||
framework = arduino
|
||||
@@ -19,22 +13,34 @@ board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
board_upload.offset_address = 0x10000
|
||||
|
||||
build_flags =
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
|
||||
# https://libexpat.github.io/doc/api/latest/#XML_GE
|
||||
-DXML_GE=0
|
||||
-DXML_CONTEXT_BYTES=1024
|
||||
|
||||
; Board configuration
|
||||
board_build.flash_mode = dio
|
||||
board_build.flash_size = 16MB
|
||||
board_build.partitions = partitions.csv
|
||||
|
||||
build_flags =
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
|
||||
# https://libexpat.github.io/doc/api/latest/#XML_GE
|
||||
-DXML_GE=0
|
||||
-DXML_CONTEXT_BYTES=1024
|
||||
|
||||
; Libraries
|
||||
lib_deps =
|
||||
zinggjm/GxEPD2@^1.6.5
|
||||
https://github.com/leethomason/tinyxml2.git#11.0.0
|
||||
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
||||
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
||||
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
|
||||
|
||||
[env:default]
|
||||
extends = base
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DCROSSPOINT_VERSION=\"${platformio.crosspoint_version}-dev\"
|
||||
|
||||
[env:gh_release]
|
||||
extends = base
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DCROSSPOINT_VERSION=\"${platformio.crosspoint_version}\"
|
||||
|
||||
@@ -23,7 +23,7 @@ bool CrossPointState::loadFromFile() {
|
||||
uint8_t version;
|
||||
serialization::readPod(inputFile, version);
|
||||
if (version != STATE_FILE_VERSION) {
|
||||
Serial.printf("CrossPointState: Unknown version %u\n", version);
|
||||
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||
inputFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
29
src/config.h
Normal file
29
src/config.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Generated with:
|
||||
* ruby -rdigest -e 'puts [
|
||||
* "./lib/EpdFont/builtinFonts/bookerly_2b.h",
|
||||
* "./lib/EpdFont/builtinFonts/bookerly_bold_2b.h",
|
||||
* "./lib/EpdFont/builtinFonts/bookerly_bold_italic_2b.h",
|
||||
* "./lib/EpdFont/builtinFonts/bookerly_italic_2b.h",
|
||||
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
|
||||
*/
|
||||
#define READER_FONT_ID 1747632454
|
||||
|
||||
/**
|
||||
* Generated with:
|
||||
* ruby -rdigest -e 'puts [
|
||||
* "./lib/EpdFont/builtinFonts/ubuntu_10.h",
|
||||
* "./lib/EpdFont/builtinFonts/ubuntu_bold_10.h",
|
||||
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
|
||||
*/
|
||||
#define UI_FONT_ID 225955604
|
||||
|
||||
/**
|
||||
* Generated with:
|
||||
* ruby -rdigest -e 'puts [
|
||||
* "./lib/EpdFont/builtinFonts/babyblue.h",
|
||||
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
|
||||
*/
|
||||
#define SMALL_FONT_ID 141891058
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#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,
|
||||
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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
72
src/main.cpp
72
src/main.cpp
@@ -1,13 +1,21 @@
|
||||
#include <Arduino.h>
|
||||
#include <EpdRenderer.h>
|
||||
#include <EInkDisplay.h>
|
||||
#include <Epub.h>
|
||||
#include <GxEPD2_BW.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <InputManager.h>
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
|
||||
#include "Battery.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "builtinFonts/babyblue.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"
|
||||
#include "config.h"
|
||||
#include "screens/BootLogoScreen.h"
|
||||
#include "screens/EpubReaderScreen.h"
|
||||
#include "screens/FileSelectionScreen.h"
|
||||
@@ -28,13 +36,26 @@
|
||||
#define SD_SPI_CS 12
|
||||
#define SD_SPI_MISO 7
|
||||
|
||||
GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC,
|
||||
EPD_RST, EPD_BUSY));
|
||||
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
|
||||
InputManager inputManager;
|
||||
EpdRenderer renderer(display);
|
||||
GfxRenderer renderer(einkDisplay);
|
||||
Screen* currentScreen;
|
||||
CrossPointState appState;
|
||||
|
||||
// Fonts
|
||||
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);
|
||||
EpdFontFamily smallFontFamily(&smallFont);
|
||||
|
||||
EpdFont ubuntu10Font(&ubuntu_10);
|
||||
EpdFont ubuntuBold10Font(&ubuntu_bold_10);
|
||||
EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ubuntuBold10Font);
|
||||
|
||||
// Power button timing
|
||||
// Time required to confirm boot from sleep
|
||||
constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000;
|
||||
@@ -43,7 +64,7 @@ constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000;
|
||||
|
||||
Epub* loadEpub(const std::string& path) {
|
||||
if (!SD.exists(path.c_str())) {
|
||||
Serial.printf("File does not exist: %s\n", path.c_str());
|
||||
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -52,7 +73,7 @@ Epub* loadEpub(const std::string& path) {
|
||||
return epub;
|
||||
}
|
||||
|
||||
Serial.println("Failed to load epub");
|
||||
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
|
||||
delete epub;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -75,15 +96,13 @@ void verifyWakeupLongPress() {
|
||||
const auto start = millis();
|
||||
bool abort = false;
|
||||
|
||||
Serial.println("Verifying power button press");
|
||||
Serial.printf("[%lu] [ ] Verifying power button press\n", millis());
|
||||
inputManager.update();
|
||||
while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) {
|
||||
delay(50);
|
||||
inputManager.update();
|
||||
Serial.println("Waiting...");
|
||||
}
|
||||
|
||||
Serial.printf("Made it? %s\n", inputManager.isPressed(InputManager::BTN_POWER) ? "yes" : "no");
|
||||
if (inputManager.isPressed(InputManager::BTN_POWER)) {
|
||||
do {
|
||||
delay(50);
|
||||
@@ -94,8 +113,6 @@ void verifyWakeupLongPress() {
|
||||
abort = true;
|
||||
}
|
||||
|
||||
Serial.printf("held for %lu\n", inputManager.getHeldTime());
|
||||
|
||||
if (abort) {
|
||||
// Button released too early. Returning to sleep.
|
||||
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
||||
@@ -117,13 +134,13 @@ void enterDeepSleep() {
|
||||
exitScreen();
|
||||
enterNewScreen(new SleepScreen(renderer, inputManager));
|
||||
|
||||
Serial.println("Power button released after a long press. Entering deep sleep.");
|
||||
Serial.printf("[%lu] [ ] Power button released after a long press. Entering deep sleep.\n", millis());
|
||||
delay(1000); // Allow Serial buffer to empty and display to update
|
||||
|
||||
// Enable Wakeup on LOW (button press)
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
|
||||
display.hibernate();
|
||||
einkDisplay.deepSleep();
|
||||
|
||||
// Enter Deep Sleep
|
||||
esp_deep_sleep_start();
|
||||
@@ -142,7 +159,8 @@ void onSelectEpubFile(const std::string& path) {
|
||||
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
|
||||
} else {
|
||||
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);
|
||||
onGoHome();
|
||||
}
|
||||
@@ -154,15 +172,17 @@ void onGoHome() {
|
||||
}
|
||||
|
||||
void setup() {
|
||||
inputManager.begin();
|
||||
verifyWakeupLongPress();
|
||||
|
||||
// Begin serial only if USB connected
|
||||
pinMode(UART0_RXD, INPUT);
|
||||
if (digitalRead(UART0_RXD) == HIGH) {
|
||||
Serial.begin(115200);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
|
||||
|
||||
inputManager.begin();
|
||||
verifyWakeupLongPress();
|
||||
|
||||
// Initialize pins
|
||||
pinMode(BAT_GPIO0, INPUT);
|
||||
|
||||
@@ -170,11 +190,13 @@ void setup() {
|
||||
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
|
||||
|
||||
// Initialize display
|
||||
const SPISettings spi_settings(SPI_FQ, MSBFIRST, SPI_MODE0);
|
||||
display.init(115200, true, 2, false, SPI, spi_settings);
|
||||
display.setRotation(3); // 270 degrees
|
||||
display.setTextColor(GxEPD_BLACK);
|
||||
Serial.println("Display initialized");
|
||||
einkDisplay.begin();
|
||||
Serial.printf("[%lu] [ ] Display initialized\n", millis());
|
||||
|
||||
renderer.insertFont(READER_FONT_ID, bookerlyFontFamily);
|
||||
renderer.insertFont(UI_FONT_ID, ubuntuFontFamily);
|
||||
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
|
||||
Serial.printf("[%lu] [ ] Fonts setup\n", millis());
|
||||
|
||||
exitScreen();
|
||||
enterNewScreen(new BootLogoScreen(renderer, inputManager));
|
||||
@@ -205,8 +227,8 @@ void loop() {
|
||||
delay(10);
|
||||
|
||||
static unsigned long lastMemPrint = 0;
|
||||
if (Serial && millis() - lastMemPrint >= 5000) {
|
||||
Serial.printf("[%lu] Memory - Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
|
||||
if (Serial && millis() - lastMemPrint >= 10000) {
|
||||
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
|
||||
ESP.getHeapSize(), ESP.getMinFreeHeap());
|
||||
lastMemPrint = millis();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
#include "BootLogoScreen.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "images/CrossLarge.h"
|
||||
|
||||
void BootLogoScreen::onEnter() {
|
||||
const auto pageWidth = renderer.getPageWidth();
|
||||
const auto pageHeight = renderer.getPageHeight();
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
|
||||
renderer.clearScreen();
|
||||
// Location for images is from top right in landscape orientation
|
||||
renderer.drawImage(CrossLarge, (pageHeight - 128) / 2, (pageWidth - 128) / 2, 128, 128);
|
||||
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
|
||||
renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING");
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION);
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
class BootLogoScreen final : public Screen {
|
||||
public:
|
||||
explicit BootLogoScreen(EpdRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
||||
explicit BootLogoScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
#include "EpubReaderScreen.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <Epub/Page.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include "Battery.h"
|
||||
#include "config.h"
|
||||
|
||||
constexpr int PAGES_PER_REFRESH = 20;
|
||||
constexpr int PAGES_PER_REFRESH = 15;
|
||||
constexpr unsigned long SKIP_CHAPTER_MS = 700;
|
||||
constexpr float lineCompression = 0.95f;
|
||||
constexpr int marginTop = 11;
|
||||
constexpr int marginRight = 10;
|
||||
constexpr int marginBottom = 30;
|
||||
constexpr int marginLeft = 10;
|
||||
|
||||
void EpubReaderScreen::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<EpubReaderScreen*>(param);
|
||||
@@ -29,7 +36,7 @@ void EpubReaderScreen::onEnter() {
|
||||
f.read(data, 4);
|
||||
currentSpineIndex = data[0] + (data[1] << 8);
|
||||
nextPageNumber = data[2] + (data[3] << 8);
|
||||
Serial.printf("Loaded cache: %d, %d\n", currentSpineIndex, nextPageNumber);
|
||||
Serial.printf("[%lu] [ERS] Loaded cache: %d, %d\n", millis(), currentSpineIndex, nextPageNumber);
|
||||
f.close();
|
||||
}
|
||||
|
||||
@@ -128,7 +135,7 @@ void EpubReaderScreen::displayTaskLoop() {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
renderPage();
|
||||
renderScreen();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
@@ -136,7 +143,7 @@ void EpubReaderScreen::displayTaskLoop() {
|
||||
}
|
||||
|
||||
// TODO: Failure handling
|
||||
void EpubReaderScreen::renderPage() {
|
||||
void EpubReaderScreen::renderScreen() {
|
||||
if (!epub) {
|
||||
return;
|
||||
}
|
||||
@@ -147,33 +154,37 @@ void EpubReaderScreen::renderPage() {
|
||||
|
||||
if (!section) {
|
||||
const auto filepath = epub->getSpineItem(currentSpineIndex);
|
||||
Serial.printf("Loading file: %s, index: %d\n", filepath.c_str(), currentSpineIndex);
|
||||
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
|
||||
section = new Section(epub, currentSpineIndex, renderer);
|
||||
if (!section->loadCacheMetadata()) {
|
||||
Serial.println("Cache not found, building...");
|
||||
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
|
||||
marginLeft)) {
|
||||
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
|
||||
|
||||
{
|
||||
const int textWidth = renderer.getTextWidth("Indexing...");
|
||||
const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing...");
|
||||
constexpr int margin = 20;
|
||||
const int x = (renderer.getPageWidth() - textWidth - margin * 2) / 2;
|
||||
const int x = (GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2;
|
||||
constexpr int y = 50;
|
||||
const int w = textWidth + margin * 2;
|
||||
const int h = renderer.getLineHeight() + margin * 2;
|
||||
const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2;
|
||||
renderer.swapBuffers();
|
||||
renderer.fillRect(x, y, w, h, 0);
|
||||
renderer.drawText(x + margin, y + margin, "Indexing...");
|
||||
renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing...");
|
||||
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
|
||||
renderer.flushArea(x, y, w, h);
|
||||
renderer.displayBuffer();
|
||||
pagesUntilFullRefresh = 0;
|
||||
}
|
||||
|
||||
section->setupCacheDir();
|
||||
if (!section->persistPageDataToSD()) {
|
||||
Serial.println("Failed to persist page data to SD");
|
||||
if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
|
||||
marginLeft)) {
|
||||
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
|
||||
delete section;
|
||||
section = nullptr;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Serial.println("Cache found, skipping build...");
|
||||
Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
|
||||
}
|
||||
|
||||
if (nextPageNumber == UINT16_MAX) {
|
||||
@@ -184,16 +195,29 @@ void EpubReaderScreen::renderPage() {
|
||||
}
|
||||
|
||||
renderer.clearScreen();
|
||||
section->renderPage();
|
||||
renderStatusBar();
|
||||
if (pagesUntilFullRefresh <= 1) {
|
||||
renderer.flushDisplay(false);
|
||||
pagesUntilFullRefresh = PAGES_PER_REFRESH;
|
||||
} else {
|
||||
renderer.flushDisplay();
|
||||
pagesUntilFullRefresh--;
|
||||
|
||||
if (section->pageCount == 0) {
|
||||
Serial.printf("[%lu] [ERS] No pages to render\n", millis());
|
||||
renderer.drawCenteredText(READER_FONT_ID, 300, "Empty chapter", true, BOLD);
|
||||
renderStatusBar();
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
|
||||
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount);
|
||||
renderer.drawCenteredText(READER_FONT_ID, 300, "Out of bounds", true, BOLD);
|
||||
renderStatusBar();
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
const Page* p = section->loadPageFromSD();
|
||||
const auto start = millis();
|
||||
renderContents(p);
|
||||
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
|
||||
delete p;
|
||||
|
||||
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
|
||||
uint8_t data[4];
|
||||
data[0] = currentSpineIndex & 0xFF;
|
||||
@@ -204,23 +228,55 @@ void EpubReaderScreen::renderPage() {
|
||||
f.close();
|
||||
}
|
||||
|
||||
void EpubReaderScreen::renderContents(const Page* p) {
|
||||
p->render(renderer, READER_FONT_ID);
|
||||
renderStatusBar();
|
||||
if (pagesUntilFullRefresh <= 1) {
|
||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||
pagesUntilFullRefresh = PAGES_PER_REFRESH;
|
||||
} else {
|
||||
renderer.displayBuffer();
|
||||
pagesUntilFullRefresh--;
|
||||
}
|
||||
|
||||
// grayscale rendering
|
||||
// TODO: Only do this if font supports it
|
||||
{
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
p->render(renderer, READER_FONT_ID);
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
|
||||
// Render and copy to MSB buffer
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
p->render(renderer, READER_FONT_ID);
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
|
||||
// display grayscale part
|
||||
renderer.displayGrayBuffer();
|
||||
renderer.setFontRenderMode(GfxRenderer::BW);
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderScreen::renderStatusBar() const {
|
||||
const auto pageWidth = renderer.getPageWidth();
|
||||
|
||||
// Right aligned text for progress counter
|
||||
const std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount);
|
||||
const auto progressTextWidth = renderer.getSmallTextWidth(progress.c_str());
|
||||
renderer.drawSmallText(pageWidth - progressTextWidth, 765, progress.c_str());
|
||||
const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str());
|
||||
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, 776,
|
||||
progress.c_str());
|
||||
|
||||
// Left aligned battery icon and percentage
|
||||
const uint16_t percentage = battery.readPercentage();
|
||||
const auto percentageText = std::to_string(percentage) + "%";
|
||||
const auto percentageTextWidth = renderer.getSmallTextWidth(percentageText.c_str());
|
||||
renderer.drawSmallText(20, 765, percentageText.c_str());
|
||||
const auto percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
|
||||
renderer.drawText(SMALL_FONT_ID, 20 + marginLeft, 776, percentageText.c_str());
|
||||
|
||||
// 1 column on left, 2 columns on right, 5 columns of battery body
|
||||
constexpr int batteryWidth = 15;
|
||||
constexpr int batteryHeight = 10;
|
||||
constexpr int x = 0;
|
||||
constexpr int y = 772;
|
||||
constexpr int x = marginLeft;
|
||||
constexpr int y = 783;
|
||||
|
||||
// Top line
|
||||
renderer.drawLine(x, y, x + batteryWidth - 4, y);
|
||||
@@ -241,17 +297,18 @@ void EpubReaderScreen::renderStatusBar() const {
|
||||
}
|
||||
renderer.fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2);
|
||||
|
||||
// Centered chatper title text
|
||||
// Page width minus existing content with 30px padding on each side
|
||||
const int leftMargin = 20 + percentageTextWidth + 30;
|
||||
const int rightMargin = progressTextWidth + 30;
|
||||
const int availableTextWidth = pageWidth - leftMargin - rightMargin;
|
||||
const int titleMarginLeft = 20 + percentageTextWidth + 30 + marginLeft;
|
||||
const int titleMarginRight = progressTextWidth + 30 + marginRight;
|
||||
const int availableTextWidth = GfxRenderer::getScreenWidth() - titleMarginLeft - titleMarginRight;
|
||||
const auto tocItem = epub->getTocItem(epub->getTocIndexForSpineIndex(currentSpineIndex));
|
||||
auto title = tocItem.title;
|
||||
int titleWidth = renderer.getSmallTextWidth(title.c_str());
|
||||
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||
while (titleWidth > availableTextWidth) {
|
||||
title = title.substr(0, title.length() - 8) + "...";
|
||||
titleWidth = renderer.getSmallTextWidth(title.c_str());
|
||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||
}
|
||||
|
||||
renderer.drawSmallText(leftMargin + (availableTextWidth - titleWidth) / 2, 765, title.c_str());
|
||||
renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, 777, title.c_str());
|
||||
}
|
||||
|
||||
@@ -20,11 +20,12 @@ class EpubReaderScreen final : public Screen {
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void renderPage();
|
||||
void renderScreen();
|
||||
void renderContents(const Page* p);
|
||||
void renderStatusBar() const;
|
||||
|
||||
public:
|
||||
explicit EpubReaderScreen(EpdRenderer& renderer, InputManager& inputManager, Epub* epub,
|
||||
explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, Epub* epub,
|
||||
const std::function<void()>& onGoHome)
|
||||
: Screen(renderer, inputManager), epub(epub), onGoHome(onGoHome) {}
|
||||
void onEnter() override;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "FileSelectionScreen.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
void sortFileList(std::vector<std::string>& strs) {
|
||||
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
||||
if (str1.back() == '/' && str2.back() != '/') return true;
|
||||
@@ -51,7 +53,7 @@ void FileSelectionScreen::onEnter() {
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask",
|
||||
1024, // Stack size
|
||||
2048, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
@@ -118,21 +120,20 @@ void FileSelectionScreen::displayTaskLoop() {
|
||||
void FileSelectionScreen::render() const {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getPageWidth();
|
||||
const auto titleWidth = renderer.getTextWidth("CrossPoint Reader", BOLD);
|
||||
renderer.drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", 1, BOLD);
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD);
|
||||
|
||||
if (files.empty()) {
|
||||
renderer.drawUiText(10, 50, "No EPUBs found");
|
||||
renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found");
|
||||
} else {
|
||||
// Draw selection
|
||||
renderer.fillRect(0, 50 + selectorIndex * 30 + 2, pageWidth - 1, 30);
|
||||
renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30);
|
||||
|
||||
for (size_t i = 0; i < files.size(); i++) {
|
||||
const auto file = files[i];
|
||||
renderer.drawUiText(10, 50 + i * 30, file.c_str(), i == selectorIndex ? 0 : 1);
|
||||
renderer.drawText(UI_FONT_ID, 20, 60 + i * 30, file.c_str(), i != selectorIndex);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.flushDisplay();
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class FileSelectionScreen final : public Screen {
|
||||
void loadFiles();
|
||||
|
||||
public:
|
||||
explicit FileSelectionScreen(EpdRenderer& renderer, InputManager& inputManager,
|
||||
explicit FileSelectionScreen(GfxRenderer& renderer, InputManager& inputManager,
|
||||
const std::function<void(const std::string&)>& onSelect)
|
||||
: Screen(renderer, inputManager), onSelect(onSelect) {}
|
||||
void onEnter() override;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#include "FullScreenMessageScreen.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
void FullScreenMessageScreen::onEnter() {
|
||||
const auto width = renderer.getUiTextWidth(text.c_str(), style);
|
||||
const auto height = renderer.getLineHeight();
|
||||
const auto left = (renderer.getPageWidth() - width) / 2;
|
||||
const auto top = (renderer.getPageHeight() - height) / 2;
|
||||
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
||||
const auto top = (GfxRenderer::getScreenHeight() - height) / 2;
|
||||
|
||||
renderer.clearScreen(invert);
|
||||
renderer.drawUiText(left, top, text.c_str(), invert ? 0 : 1, style);
|
||||
renderer.flushDisplay(partialUpdate);
|
||||
renderer.clearScreen();
|
||||
renderer.drawCenteredText(UI_FONT_ID, top, text.c_str(), true, style);
|
||||
renderer.displayBuffer(refreshMode);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
#pragma once
|
||||
#include <EInkDisplay.h>
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "EpdFontFamily.h"
|
||||
#include "Screen.h"
|
||||
|
||||
class FullScreenMessageScreen final : public Screen {
|
||||
std::string text;
|
||||
EpdFontStyle style;
|
||||
bool invert;
|
||||
bool partialUpdate;
|
||||
EInkDisplay::RefreshMode refreshMode;
|
||||
|
||||
public:
|
||||
explicit FullScreenMessageScreen(EpdRenderer& renderer, InputManager& inputManager, std::string text,
|
||||
const EpdFontStyle style = REGULAR, const bool invert = false,
|
||||
const bool partialUpdate = true)
|
||||
: Screen(renderer, inputManager),
|
||||
text(std::move(text)),
|
||||
style(style),
|
||||
invert(invert),
|
||||
partialUpdate(partialUpdate) {}
|
||||
explicit FullScreenMessageScreen(GfxRenderer& renderer, InputManager& inputManager, std::string text,
|
||||
const EpdFontStyle style = REGULAR,
|
||||
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
|
||||
: Screen(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#pragma once
|
||||
#include <InputManager.h>
|
||||
|
||||
class EpdRenderer;
|
||||
class GfxRenderer;
|
||||
|
||||
class Screen {
|
||||
protected:
|
||||
EpdRenderer& renderer;
|
||||
GfxRenderer& renderer;
|
||||
InputManager& inputManager;
|
||||
|
||||
public:
|
||||
explicit Screen(EpdRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {}
|
||||
explicit Screen(GfxRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {}
|
||||
virtual ~Screen() = default;
|
||||
virtual void onEnter() {}
|
||||
virtual void onExit() {}
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
#include "SleepScreen.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include "images/SleepScreenImg.h"
|
||||
#include "config.h"
|
||||
#include "images/CrossLarge.h"
|
||||
|
||||
void SleepScreen::onEnter() { renderer.drawImageNoMargin(SleepScreenImg, 0, 0, 800, 480, false, true); }
|
||||
void SleepScreen::onEnter() {
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
|
||||
renderer.clearScreen();
|
||||
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
|
||||
renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
|
||||
renderer.invertScreen();
|
||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
class SleepScreen final : public Screen {
|
||||
public:
|
||||
explicit SleepScreen(EpdRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
||||
explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user