Compare commits

...

8 Commits

Author SHA1 Message Date
Dave Allie
0d32d21d75
Small code cleanup (#83)
Some checks are pending
CI / build (push) Waiting to run
## Summary

* Fix cppcheck low violations
* Remove teardown method on parsers, use destructor
* Code cleanup
2025-12-21 15:43:53 +11:00
Dave Allie
9b4dfbd180
Allow any file to be uploaded (#84)
## Summary

- Allow any file to be uploaded
- Removes .epub restriction

## Additional Context

- Fixes #74
2025-12-21 15:43:17 +11:00
Jonas Diemer
926c786705
Keep ZipFile open to speed up getting file stats. (#76)
Still a bit raw, but gets the time required to determine the size of
each chapter (for reading progress) down from ~25ms to 0-1ms.

This is done by keeping the zipArchive open (so simple ;)).

Probably we don't need to cache the spine sizes anymore then...

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-21 14:38:51 +11:00
Dave Allie
299623927e
Build out lines when parsing html and holding >750 words in buffer (#73)
## Summary

* Build out lines for pages when holding over 750 buffered words
* Should fix issues with parsing long blocks of text causing memory
crashes
2025-12-21 13:43:19 +11:00
IFAKA
9a3bb81337
fix: add NULL checks after malloc in drawBmp() (#80)
## Problem
`drawBmp()` allocates two row buffers via `malloc()` but doesn't check
if allocations succeed. On low memory, this causes a crash when the NULL
pointers are dereferenced.

## Fix
Add NULL check after both `malloc()` calls. If either fails, log error
and return early.

Changed `lib/GfxRenderer/GfxRenderer.cpp`.

## Test
- Defensive addition only - no logic changes
- Manual device testing appreciated
2025-12-21 13:36:59 +11:00
IFAKA
73d1839ddd
fix: add bounds checks to Epub getter functions (#82)
## Problem
Three Epub getter functions can throw exceptions:
- `getCumulativeSpineItemSize()`: No bounds check before
`.at(spineIndex)`
- `getSpineItem()`: If spine is empty and index invalid, `.at(0)` throws
- `getTocItem()`: If toc is empty and index invalid, `.at(0)` throws

## Fix
- Add bounds check to `getCumulativeSpineItemSize()`, return 0 on error
- Add empty container checks to `getSpineItem()` and `getTocItem()`
- Use static fallback objects for safe reference returns on empty
containers

Changed `lib/Epub/Epub.cpp`.

## Test
- Defensive additions - follows existing bounds check patterns
- No logic changes for valid inputs
- Manual device testing appreciated
2025-12-21 13:36:30 +11:00
IFAKA
cc86533e86
fix: add NULL check after malloc in readFileToMemory() (#81)
## Problem
`readFileToMemory()` allocates an output buffer via `malloc()` at line
120 but doesn't check if allocation succeeds. On low memory, the NULL
pointer is passed to `fread()` causing a crash.

## Fix
Add NULL check after `malloc()` for the output buffer. Follows the
existing pattern already used for `deflatedData` at line 141.

Changed `lib/ZipFile/ZipFile.cpp`.

## Test
- Follows existing validated pattern from same function
- Defensive addition only - no logic changes
2025-12-21 13:35:37 +11:00
IFAKA
bf3f270067
fix: add NULL checks for frameBuffer in GfxRenderer (#79)
## Problem
`invertScreen()`, `storeBwBuffer()`, and `restoreBwBuffer()` dereference
`frameBuffer` without NULL validation. If the display isn't initialized,
these functions will crash.

## Fix
Add NULL checks before using `frameBuffer` in all three functions.
Follows the existing pattern from `drawPixel()` (line 11) which already
validates the pointer.

Changed `lib/GfxRenderer/GfxRenderer.cpp`.

## Test
- Follows existing validated pattern from `drawPixel()`
- No logic changes - only adds early return on NULL
- Manual device testing appreciated
2025-12-21 13:34:58 +11:00
27 changed files with 273 additions and 266 deletions

View File

@ -34,7 +34,7 @@ jobs:
sudo apt-get install -y clang-format-21 sudo apt-get install -y clang-format-21
- name: Run cppcheck - name: Run cppcheck
run: pio check --fail-on-defect medium --fail-on-defect high run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
- name: Run clang-format - name: Run clang-format
run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1) run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)

View File

@ -30,24 +30,22 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
// Stream read (reusing your existing stream logic) // Stream read (reusing your existing stream logic)
if (!readItemContentsToStream(containerPath, containerParser, 512)) { if (!readItemContentsToStream(containerPath, containerParser, 512)) {
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis()); Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
containerParser.teardown();
return false; return false;
} }
// Extract the result // Extract the result
if (containerParser.fullPath.empty()) { if (containerParser.fullPath.empty()) {
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis()); Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
containerParser.teardown();
return false; return false;
} }
*contentOpfFile = std::move(containerParser.fullPath); *contentOpfFile = std::move(containerParser.fullPath);
containerParser.teardown();
return true; return true;
} }
bool Epub::parseContentOpf(const std::string& contentOpfFilePath) { bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str());
size_t contentOpfSize; size_t contentOpfSize;
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) { if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis()); Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
@ -63,7 +61,6 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) { if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis()); Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
opfParser.teardown();
return false; return false;
} }
@ -84,8 +81,6 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
} }
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis()); Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
opfParser.teardown();
return true; return true;
} }
@ -96,6 +91,8 @@ bool Epub::parseTocNcxFile() {
return false; return false;
} }
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
size_t tocSize; size_t tocSize;
if (!getItemSize(tocNcxItem, &tocSize)) { if (!getItemSize(tocNcxItem, &tocSize)) {
Serial.printf("[%lu] [EBP] Could not get size of toc ncx\n", millis()); Serial.printf("[%lu] [EBP] Could not get size of toc ncx\n", millis());
@ -111,22 +108,18 @@ bool Epub::parseTocNcxFile() {
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) { if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
Serial.printf("[%lu] [EBP] Could not read toc ncx stream\n", millis()); Serial.printf("[%lu] [EBP] Could not read toc ncx stream\n", millis());
ncxParser.teardown();
return false; return false;
} }
this->toc = std::move(ncxParser.toc); this->toc = std::move(ncxParser.toc);
Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size()); Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size());
ncxParser.teardown();
return true; return true;
} }
// load in the meta data for the epub file // load in the meta data for the epub file
bool Epub::load() { bool Epub::load() {
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str()); Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
ZipFile zip("/sd" + filepath);
std::string contentOpfFilePath; std::string contentOpfFilePath;
if (!findContentOpfFile(&contentOpfFilePath)) { if (!findContentOpfFile(&contentOpfFilePath)) {
@ -155,44 +148,20 @@ bool Epub::load() {
} }
void Epub::initializeSpineItemSizes() { void Epub::initializeSpineItemSizes() {
setupCacheDir(); Serial.printf("[%lu] [EBP] Calculating book size\n", millis());
size_t spineItemsCount = getSpineItemsCount(); const size_t spineItemsCount = getSpineItemsCount();
size_t cumSpineItemSize = 0; size_t cumSpineItemSize = 0;
if (SD.exists((getCachePath() + "/spine_size.bin").c_str())) { const ZipFile zip("/sd" + filepath);
File f = SD.open((getCachePath() + "/spine_size.bin").c_str());
uint8_t data[4];
for (size_t i = 0; i < spineItemsCount; i++) {
f.read(data, 4);
cumSpineItemSize = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
cumulativeSpineItemSize.emplace_back(cumSpineItemSize);
// Serial.printf("[%lu] [EBP] Loading item %d size %u to %u %u\n", millis(),
// i, cumSpineItemSize, data[1], data[0]);
}
f.close();
} else {
File f = SD.open((getCachePath() + "/spine_size.bin").c_str(), FILE_WRITE);
uint8_t data[4];
// determine size of spine items
for (size_t i = 0; i < spineItemsCount; i++) {
std::string spineItem = getSpineItem(i);
size_t s = 0;
getItemSize(spineItem, &s);
cumSpineItemSize += s;
cumulativeSpineItemSize.emplace_back(cumSpineItemSize);
// and persist to cache for (size_t i = 0; i < spineItemsCount; i++) {
data[0] = cumSpineItemSize & 0xFF; std::string spineItem = getSpineItem(i);
data[1] = (cumSpineItemSize >> 8) & 0xFF; size_t s = 0;
data[2] = (cumSpineItemSize >> 16) & 0xFF; getItemSize(zip, spineItem, &s);
data[3] = (cumSpineItemSize >> 24) & 0xFF; cumSpineItemSize += s;
// Serial.printf("[%lu] [EBP] Persisting item %d size %u to %u %u\n", millis(), cumulativeSpineItemSize.emplace_back(cumSpineItemSize);
// i, cumSpineItemSize, data[1], data[0]);
f.write(data, 4);
}
f.close();
} }
Serial.printf("[%lu] [EBP] Book size: %lu\n", millis(), cumSpineItemSize); Serial.printf("[%lu] [EBP] Book size: %lu\n", millis(), cumSpineItemSize);
} }
@ -291,17 +260,31 @@ bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, con
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const { bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
const ZipFile zip("/sd" + filepath); const ZipFile zip("/sd" + filepath);
const std::string path = normalisePath(itemHref); return getItemSize(zip, itemHref, size);
}
bool Epub::getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size) {
const std::string path = normalisePath(itemHref);
return zip.getInflatedFileSize(path.c_str(), size); return zip.getInflatedFileSize(path.c_str(), size);
} }
int Epub::getSpineItemsCount() const { return spine.size(); } int Epub::getSpineItemsCount() const { return spine.size(); }
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return cumulativeSpineItemSize.at(spineIndex); } size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const {
if (spineIndex < 0 || spineIndex >= static_cast<int>(cumulativeSpineItemSize.size())) {
Serial.printf("[%lu] [EBP] getCumulativeSpineItemSize index:%d is out of range\n", millis(), spineIndex);
return 0;
}
return cumulativeSpineItemSize.at(spineIndex);
}
std::string& Epub::getSpineItem(const int spineIndex) { std::string& Epub::getSpineItem(const int spineIndex) {
if (spineIndex < 0 || spineIndex >= spine.size()) { static std::string emptyString;
if (spine.empty()) {
Serial.printf("[%lu] [EBP] getSpineItem called but spine is empty\n", millis());
return emptyString;
}
if (spineIndex < 0 || spineIndex >= static_cast<int>(spine.size())) {
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex); Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
return spine.at(0).second; return spine.at(0).second;
} }
@ -310,7 +293,12 @@ std::string& Epub::getSpineItem(const int spineIndex) {
} }
EpubTocEntry& Epub::getTocItem(const int tocTndex) { EpubTocEntry& Epub::getTocItem(const int tocTndex) {
if (tocTndex < 0 || tocTndex >= toc.size()) { static EpubTocEntry emptyEntry("", "", "", 0);
if (toc.empty()) {
Serial.printf("[%lu] [EBP] getTocItem called but toc is empty\n", millis());
return emptyEntry;
}
if (tocTndex < 0 || tocTndex >= static_cast<int>(toc.size())) {
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocTndex); Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocTndex);
return toc.at(0); return toc.at(0);
} }

View File

@ -33,6 +33,7 @@ class Epub {
bool parseContentOpf(const std::string& contentOpfFilePath); bool parseContentOpf(const std::string& contentOpfFilePath);
bool parseTocNcxFile(); bool parseTocNcxFile();
void initializeSpineItemSizes(); void initializeSpineItemSizes();
static bool getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size);
public: public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) { explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {

View File

@ -19,14 +19,25 @@ void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) {
// Consumes data to minimize memory usage // Consumes data to minimize memory usage
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const int horizontalMargin, void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const int horizontalMargin,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) { const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
const bool includeLastLine) {
if (words.empty()) { if (words.empty()) {
return; return;
} }
const size_t totalWordCount = words.size();
const int pageWidth = renderer.getScreenWidth() - horizontalMargin; const int pageWidth = renderer.getScreenWidth() - horizontalMargin;
const int spaceWidth = renderer.getSpaceWidth(fontId); const int spaceWidth = renderer.getSpaceWidth(fontId);
const auto wordWidths = calculateWordWidths(renderer, fontId);
const auto lineBreakIndices = computeLineBreaks(pageWidth, spaceWidth, wordWidths);
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
for (size_t i = 0; i < lineCount; ++i) {
extractLine(i, pageWidth, spaceWidth, wordWidths, lineBreakIndices, processLine);
}
}
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
const size_t totalWordCount = words.size();
std::vector<uint16_t> wordWidths; std::vector<uint16_t> wordWidths;
wordWidths.reserve(totalWordCount); wordWidths.reserve(totalWordCount);
@ -47,6 +58,13 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
std::advance(wordStylesIt, 1); std::advance(wordStylesIt, 1);
} }
return wordWidths;
}
std::vector<size_t> ParsedText::computeLineBreaks(const int pageWidth, const int spaceWidth,
const std::vector<uint16_t>& wordWidths) const {
const size_t totalWordCount = words.size();
// DP table to store the minimum badness (cost) of lines starting at index i // DP table to store the minimum badness (cost) of lines starting at index i
std::vector<int> dp(totalWordCount); std::vector<int> dp(totalWordCount);
// 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i' // 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i'
@ -106,66 +124,59 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
currentWordIndex = nextBreakIndex; currentWordIndex = nextBreakIndex;
} }
// Initialize iterators for consumption return lineBreakIndices;
auto wordStartIt = words.begin(); }
auto wordStyleStartIt = wordStyles.begin();
size_t wordWidthIndex = 0; void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
size_t lastBreakAt = 0; const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
for (const size_t lineBreak : lineBreakIndices) { const size_t lineBreak = lineBreakIndices[breakIndex];
const size_t lineWordCount = lineBreak - lastBreakAt; const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
const size_t lineWordCount = lineBreak - lastBreakAt;
// Calculate end iterators for the range to splice
auto wordEndIt = wordStartIt; // Calculate total word width for this line
auto wordStyleEndIt = wordStyleStartIt; int lineWordWidthSum = 0;
std::advance(wordEndIt, lineWordCount); for (size_t i = lastBreakAt; i < lineBreak; i++) {
std::advance(wordStyleEndIt, lineWordCount); lineWordWidthSum += wordWidths[i];
}
// Calculate total word width for this line
int lineWordWidthSum = 0; // Calculate spacing
for (size_t i = 0; i < lineWordCount; ++i) { const int spareSpace = pageWidth - lineWordWidthSum;
lineWordWidthSum += wordWidths[wordWidthIndex + i];
} int spacing = spaceWidth;
const bool isLastLine = lineBreak == words.size();
// Calculate spacing
int spareSpace = pageWidth - lineWordWidthSum; if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
spacing = spareSpace / (lineWordCount - 1);
int spacing = spaceWidth; }
const bool isLastLine = lineBreak == totalWordCount;
// Calculate initial x position
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) { uint16_t xpos = 0;
spacing = spareSpace / (lineWordCount - 1); if (style == TextBlock::RIGHT_ALIGN) {
} xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
} else if (style == TextBlock::CENTER_ALIGN) {
// Calculate initial x position xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
uint16_t xpos = 0; }
if (style == TextBlock::RIGHT_ALIGN) {
xpos = spareSpace - (lineWordCount - 1) * spaceWidth; // Pre-calculate X positions for words
} else if (style == TextBlock::CENTER_ALIGN) { std::list<uint16_t> lineXPos;
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2; for (size_t i = lastBreakAt; i < lineBreak; i++) {
} const uint16_t currentWordWidth = wordWidths[i];
lineXPos.push_back(xpos);
// Pre-calculate X positions for words xpos += currentWordWidth + spacing;
std::list<uint16_t> lineXPos; }
for (size_t i = 0; i < lineWordCount; ++i) {
const uint16_t currentWordWidth = wordWidths[wordWidthIndex + i]; // Iterators always start at the beginning as we are moving content with splice below
lineXPos.push_back(xpos); auto wordEndIt = words.begin();
xpos += currentWordWidth + spacing; auto wordStyleEndIt = wordStyles.begin();
} std::advance(wordEndIt, lineWordCount);
std::advance(wordStyleEndIt, lineWordCount);
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
std::list<std::string> lineWords; // *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
lineWords.splice(lineWords.begin(), words, wordStartIt, wordEndIt); std::list<std::string> lineWords;
std::list<EpdFontStyle> lineWordStyles; lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyleStartIt, wordStyleEndIt); std::list<EpdFontStyle> lineWordStyles;
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
processLine(
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style)); processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
// Update pointers/indices for the next line
wordStartIt = wordEndIt;
wordStyleStartIt = wordStyleEndIt;
wordWidthIndex += lineWordCount;
lastBreakAt = lineBreak;
}
} }

View File

@ -2,11 +2,11 @@
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <cstdint>
#include <functional> #include <functional>
#include <list> #include <list>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector>
#include "blocks/TextBlock.h" #include "blocks/TextBlock.h"
@ -18,6 +18,12 @@ class ParsedText {
TextBlock::BLOCK_STYLE style; TextBlock::BLOCK_STYLE style;
bool extraParagraphSpacing; bool extraParagraphSpacing;
std::vector<size_t> computeLineBreaks(int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths) const;
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
public: public:
explicit ParsedText(const TextBlock::BLOCK_STYLE style, const bool extraParagraphSpacing) explicit ParsedText(const TextBlock::BLOCK_STYLE style, const bool extraParagraphSpacing)
: style(style), extraParagraphSpacing(extraParagraphSpacing) {} : style(style), extraParagraphSpacing(extraParagraphSpacing) {}
@ -26,7 +32,9 @@ class ParsedText {
void addWord(std::string word, EpdFontStyle fontStyle); void addWord(std::string word, EpdFontStyle fontStyle);
void setStyle(const TextBlock::BLOCK_STYLE style) { this->style = style; } void setStyle(const TextBlock::BLOCK_STYLE style) { this->style = style; }
TextBlock::BLOCK_STYLE getStyle() const { return style; } TextBlock::BLOCK_STYLE getStyle() const { return style; }
size_t size() const { return words.size(); }
bool isEmpty() const { return words.empty(); } bool isEmpty() const { return words.empty(); }
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, int horizontalMargin, void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, int horizontalMargin,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine); const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
bool includeLastLine = true);
}; };

View File

@ -21,9 +21,10 @@ class Section {
int currentPage = 0; int currentPage = 0;
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer) explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
: epub(epub), spineIndex(spineIndex), renderer(renderer) { : epub(epub),
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex); spineIndex(spineIndex),
} renderer(renderer),
cachePath(epub->getCachePath() + "/" + std::to_string(spineIndex)) {}
~Section() = default; ~Section() = default;
bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft, bool extraParagraphSpacing); int marginLeft, bool extraParagraphSpacing);

View File

@ -143,6 +143,17 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
self->partWordBuffer[self->partWordBufferIndex++] = s[i]; self->partWordBuffer[self->partWordBufferIndex++] = s[i];
} }
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
// memory.
// Spotted when reading Intermezzo, there are some really long text blocks in there.
if (self->currentTextBlock->size() > 750) {
Serial.printf("[%lu] [EHP] Text block too long, splitting into multiple pages\n", millis());
self->currentTextBlock->layoutAndExtractLines(
self->renderer, self->fontId, self->marginLeft + self->marginRight,
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
}
} }
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) { void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {

View File

@ -14,12 +14,11 @@ bool ContainerParser::setup() {
return true; return true;
} }
bool ContainerParser::teardown() { ContainerParser::~ContainerParser() {
if (parser) { if (parser) {
XML_ParserFree(parser); XML_ParserFree(parser);
parser = nullptr; parser = nullptr;
} }
return true;
} }
size_t ContainerParser::write(const uint8_t data) { return write(&data, 1); } size_t ContainerParser::write(const uint8_t data) { return write(&data, 1); }

View File

@ -23,9 +23,9 @@ class ContainerParser final : public Print {
std::string fullPath; std::string fullPath;
explicit ContainerParser(const size_t xmlSize) : remainingSize(xmlSize) {} explicit ContainerParser(const size_t xmlSize) : remainingSize(xmlSize) {}
~ContainerParser() override;
bool setup(); bool setup();
bool teardown();
size_t write(uint8_t) override; size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override; size_t write(const uint8_t* buffer, size_t size) override;

View File

@ -4,7 +4,7 @@
#include <ZipFile.h> #include <ZipFile.h>
namespace { namespace {
constexpr const char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml"; constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
} }
bool ContentOpfParser::setup() { bool ContentOpfParser::setup() {
@ -20,12 +20,11 @@ bool ContentOpfParser::setup() {
return true; return true;
} }
bool ContentOpfParser::teardown() { ContentOpfParser::~ContentOpfParser() {
if (parser) { if (parser) {
XML_ParserFree(parser); XML_ParserFree(parser);
parser = nullptr; parser = nullptr;
} }
return true;
} }
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); } size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }

View File

@ -34,9 +34,9 @@ class ContentOpfParser final : public Print {
explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize) explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize)
: baseContentPath(baseContentPath), remainingSize(xmlSize) {} : baseContentPath(baseContentPath), remainingSize(xmlSize) {}
~ContentOpfParser() override;
bool setup(); bool setup();
bool teardown();
size_t write(uint8_t) override; size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override; size_t write(const uint8_t* buffer, size_t size) override;

View File

@ -1,5 +1,6 @@
#include "TocNcxParser.h" #include "TocNcxParser.h"
#include <Esp.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
bool TocNcxParser::setup() { bool TocNcxParser::setup() {
@ -15,12 +16,11 @@ bool TocNcxParser::setup() {
return true; return true;
} }
bool TocNcxParser::teardown() { TocNcxParser::~TocNcxParser() {
if (parser) { if (parser) {
XML_ParserFree(parser); XML_ParserFree(parser);
parser = nullptr; parser = nullptr;
} }
return true;
} }
size_t TocNcxParser::write(const uint8_t data) { return write(&data, 1); } size_t TocNcxParser::write(const uint8_t data) { return write(&data, 1); }

View File

@ -28,9 +28,9 @@ class TocNcxParser final : public Print {
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize) explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize)
: baseContentPath(baseContentPath), remainingSize(xmlSize) {} : baseContentPath(baseContentPath), remainingSize(xmlSize) {}
~TocNcxParser() override;
bool setup(); bool setup();
bool teardown();
size_t write(uint8_t) override; size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override; size_t write(const uint8_t* buffer, size_t size) override;

View File

@ -136,6 +136,13 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize)); auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes())); auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
if (!outputRow || !rowBytes) {
Serial.printf("[%lu] [GFX] !! Failed to allocate BMP row buffers\n", millis());
free(outputRow);
free(rowBytes);
return;
}
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) { for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
// The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative). // The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative).
// Screen's (0, 0) is the top-left corner. // Screen's (0, 0) is the top-left corner.
@ -183,6 +190,10 @@ void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScre
void GfxRenderer::invertScreen() const { void GfxRenderer::invertScreen() const {
uint8_t* buffer = einkDisplay.getFrameBuffer(); uint8_t* buffer = einkDisplay.getFrameBuffer();
if (!buffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis());
return;
}
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) { for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
buffer[i] = ~buffer[i]; buffer[i] = ~buffer[i];
} }
@ -256,6 +267,10 @@ void GfxRenderer::freeBwBufferChunks() {
*/ */
void GfxRenderer::storeBwBuffer() { void GfxRenderer::storeBwBuffer() {
const uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); const uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
if (!frameBuffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis());
return;
}
// Allocate and copy each chunk // Allocate and copy each chunk
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) { for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
@ -306,6 +321,12 @@ void GfxRenderer::restoreBwBuffer() {
} }
uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
if (!frameBuffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis());
freeBwBufferChunks();
return;
}
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) { for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
// Check if chunk is missing // Check if chunk is missing
if (!bwBufferChunks[i]) { if (!bwBufferChunks[i]) {

View File

@ -27,31 +27,28 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
return true; return true;
} }
bool ZipFile::loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const { ZipFile::ZipFile(std::string filePath) : filePath(std::move(filePath)) {
mz_zip_archive zipArchive = {}; const bool status = mz_zip_reader_init_file(&zipArchive, this->filePath.c_str(), 0);
const bool status = mz_zip_reader_init_file(&zipArchive, filePath.c_str(), 0);
if (!status) { if (!status) {
Serial.printf("[%lu] [ZIP] mz_zip_reader_init_file() failed! Error: %s\n", millis(), Serial.printf("[%lu] [ZIP] mz_zip_reader_init_file() failed for %s! Error: %s\n", millis(), this->filePath.c_str(),
mz_zip_get_error_string(zipArchive.m_last_error)); mz_zip_get_error_string(zipArchive.m_last_error));
return false;
} }
}
bool ZipFile::loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const {
// find the file // find the file
mz_uint32 fileIndex = 0; mz_uint32 fileIndex = 0;
if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) { if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) {
Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis(), filename); Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis(), filename);
mz_zip_reader_end(&zipArchive);
return false; return false;
} }
if (!mz_zip_reader_file_stat(&zipArchive, fileIndex, fileStat)) { if (!mz_zip_reader_file_stat(&zipArchive, fileIndex, fileStat)) {
Serial.printf("[%lu] [ZIP] mz_zip_reader_file_stat() failed! Error: %s\n", millis(), 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_get_error_string(zipArchive.m_last_error));
mz_zip_reader_end(&zipArchive);
return false; return false;
} }
mz_zip_reader_end(&zipArchive);
return true; return true;
} }
@ -118,6 +115,11 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
const auto inflatedDataSize = static_cast<size_t>(fileStat.m_uncomp_size); const auto inflatedDataSize = static_cast<size_t>(fileStat.m_uncomp_size);
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize; const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
const auto data = static_cast<uint8_t*>(malloc(dataSize)); const auto data = static_cast<uint8_t*>(malloc(dataSize));
if (data == nullptr) {
Serial.printf("[%lu] [ZIP] Failed to allocate memory for output buffer (%zu bytes)\n", millis(), dataSize);
fclose(file);
return nullptr;
}
if (fileStat.m_method == MZ_NO_COMPRESSION) { if (fileStat.m_method == MZ_NO_COMPRESSION) {
// no deflation, just read content // no deflation, just read content

View File

@ -1,19 +1,19 @@
#pragma once #pragma once
#include <Print.h> #include <Print.h>
#include <functional>
#include <string> #include <string>
#include "miniz.h" #include "miniz.h"
class ZipFile { class ZipFile {
std::string filePath; std::string filePath;
mutable mz_zip_archive zipArchive = {};
bool loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const; bool loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const;
long getDataOffset(const mz_zip_archive_file_stat& fileStat) const; long getDataOffset(const mz_zip_archive_file_stat& fileStat) const;
public: public:
explicit ZipFile(std::string filePath) : filePath(std::move(filePath)) {} explicit ZipFile(std::string filePath);
~ZipFile() = default; ~ZipFile() { mz_zip_reader_end(&zipArchive); }
bool getInflatedFileSize(const char* filename, size_t* size) const; bool getInflatedFileSize(const char* filename, size_t* size) const;
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false) const; uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false) const;
bool readFileToStream(const char* filename, Print& out, size_t chunkSize) const; bool readFileToStream(const char* filename, Print& out, size_t chunkSize) const;

View File

@ -9,8 +9,8 @@ framework = arduino
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 921600 upload_speed = 921600
check_tool = cppcheck check_tool = cppcheck
check_flags = --enable=all --suppress=missingIncludeSystem --suppress=unusedFunction --suppress=unmatchedSuppression --inline-suppr
check_skip_packages = yes check_skip_packages = yes
check_severity = medium, high
board_upload.flash_size = 16MB board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216 board_upload.maximum_size = 16777216

View File

@ -111,12 +111,12 @@ bool WifiCredentialStore::loadFromFile() {
bool WifiCredentialStore::addCredential(const std::string& ssid, const std::string& password) { bool WifiCredentialStore::addCredential(const std::string& ssid, const std::string& password) {
// Check if this SSID already exists and update it // Check if this SSID already exists and update it
for (auto& cred : credentials) { const auto cred = find_if(credentials.begin(), credentials.end(),
if (cred.ssid == ssid) { [&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
cred.password = password; if (cred != credentials.end()) {
Serial.printf("[%lu] [WCS] Updated credentials for: %s\n", millis(), ssid.c_str()); cred->password = password;
return saveToFile(); Serial.printf("[%lu] [WCS] Updated credentials for: %s\n", millis(), ssid.c_str());
} return saveToFile();
} }
// Check if we've reached the limit // Check if we've reached the limit
@ -132,22 +132,24 @@ bool WifiCredentialStore::addCredential(const std::string& ssid, const std::stri
} }
bool WifiCredentialStore::removeCredential(const std::string& ssid) { bool WifiCredentialStore::removeCredential(const std::string& ssid) {
for (auto it = credentials.begin(); it != credentials.end(); ++it) { const auto cred = find_if(credentials.begin(), credentials.end(),
if (it->ssid == ssid) { [&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
credentials.erase(it); if (cred != credentials.end()) {
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str()); credentials.erase(cred);
return saveToFile(); Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
} return saveToFile();
} }
return false; // Not found return false; // Not found
} }
const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssid) const { const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssid) const {
for (const auto& cred : credentials) { const auto cred = find_if(credentials.begin(), credentials.end(),
if (cred.ssid == ssid) { [&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
return &cred;
} if (cred != credentials.end()) {
return &*cred;
} }
return nullptr; return nullptr;
} }

View File

@ -1,11 +1,11 @@
#include "SleepActivity.h" #include "SleepActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <SD.h>
#include <vector> #include <vector>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "SD.h"
#include "config.h" #include "config.h"
#include "images/CrossLarge.h" #include "images/CrossLarge.h"

View File

@ -217,8 +217,7 @@ void CrossPointWebServerActivity::render() const {
} }
void CrossPointWebServerActivity::renderServerRunning() const { void CrossPointWebServerActivity::renderServerRunning() const {
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageHeight = renderer.getScreenHeight();
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 5) / 2; const auto top = (pageHeight - height * 5) / 2;
@ -226,7 +225,7 @@ void CrossPointWebServerActivity::renderServerRunning() const {
std::string ssidInfo = "Network: " + connectedSSID; std::string ssidInfo = "Network: " + connectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo = ssidInfo.substr(0, 25) + "..."; ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR);

View File

@ -138,6 +138,7 @@ void WifiSelectionActivity::processWifiScanResults() {
// Convert map to vector // Convert map to vector
networks.clear(); networks.clear();
for (const auto& pair : uniqueNetworks) { for (const auto& pair : uniqueNetworks) {
// cppcheck-suppress useStlAlgorithm
networks.push_back(pair.second); networks.push_back(pair.second);
} }
@ -334,11 +335,10 @@ void WifiSelectionActivity::loop() {
// User chose "Yes" - forget the network // User chose "Yes" - forget the network
WIFI_STORE.removeCredential(selectedSSID); WIFI_STORE.removeCredential(selectedSSID);
// Update the network list to reflect the change // Update the network list to reflect the change
for (auto& network : networks) { const auto network = find_if(networks.begin(), networks.end(),
if (network.ssid == selectedSSID) { [this](const WifiNetworkInfo& net) { return net.ssid == selectedSSID; });
network.hasSavedPassword = false; if (network != networks.end()) {
break; network->hasSavedPassword = false;
}
} }
} }
// Go back to network list // Go back to network list
@ -468,8 +468,8 @@ void WifiSelectionActivity::render() const {
} }
void WifiSelectionActivity::renderNetworkList() const { void WifiSelectionActivity::renderNetworkList() const {
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(READER_FONT_ID, 10, "WiFi Networks", true, BOLD); renderer.drawCenteredText(READER_FONT_ID, 10, "WiFi Networks", true, BOLD);
@ -506,7 +506,7 @@ void WifiSelectionActivity::renderNetworkList() const {
// Draw network name (truncate if too long) // Draw network name (truncate if too long)
std::string displayName = network.ssid; std::string displayName = network.ssid;
if (displayName.length() > 16) { if (displayName.length() > 16) {
displayName = displayName.substr(0, 13) + "..."; displayName.replace(13, displayName.length() - 13, "...");
} }
renderer.drawText(UI_FONT_ID, 20, networkY, displayName.c_str()); renderer.drawText(UI_FONT_ID, 20, networkY, displayName.c_str());
@ -544,15 +544,13 @@ void WifiSelectionActivity::renderNetworkList() const {
} }
void WifiSelectionActivity::renderPasswordEntry() const { void WifiSelectionActivity::renderPasswordEntry() const {
const auto pageHeight = GfxRenderer::getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(READER_FONT_ID, 5, "WiFi Password", true, BOLD); renderer.drawCenteredText(READER_FONT_ID, 5, "WiFi Password", true, BOLD);
// Draw network name with good spacing from header // Draw network name with good spacing from header
std::string networkInfo = "Network: " + selectedSSID; std::string networkInfo = "Network: " + selectedSSID;
if (networkInfo.length() > 30) { if (networkInfo.length() > 30) {
networkInfo = networkInfo.substr(0, 27) + "..."; networkInfo.replace(27, networkInfo.length() - 27, "...");
} }
renderer.drawCenteredText(UI_FONT_ID, 38, networkInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, 38, networkInfo.c_str(), true, REGULAR);
@ -563,7 +561,7 @@ void WifiSelectionActivity::renderPasswordEntry() const {
} }
void WifiSelectionActivity::renderConnecting() const { void WifiSelectionActivity::renderConnecting() const {
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height) / 2; const auto top = (pageHeight - height) / 2;
@ -574,15 +572,14 @@ void WifiSelectionActivity::renderConnecting() const {
std::string ssidInfo = "to " + selectedSSID; std::string ssidInfo = "to " + selectedSSID;
if (ssidInfo.length() > 25) { if (ssidInfo.length() > 25) {
ssidInfo = ssidInfo.substr(0, 22) + "..."; ssidInfo.replace(22, ssidInfo.length() - 22, "...");
} }
renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR);
} }
} }
void WifiSelectionActivity::renderConnected() const { void WifiSelectionActivity::renderConnected() const {
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageHeight = renderer.getScreenHeight();
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 4) / 2; const auto top = (pageHeight - height * 4) / 2;
@ -590,7 +587,7 @@ void WifiSelectionActivity::renderConnected() const {
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = "Network: " + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo = ssidInfo.substr(0, 25) + "..."; ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR);
@ -601,8 +598,8 @@ void WifiSelectionActivity::renderConnected() const {
} }
void WifiSelectionActivity::renderSavePrompt() const { void WifiSelectionActivity::renderSavePrompt() const {
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 3) / 2; const auto top = (pageHeight - height * 3) / 2;
@ -610,7 +607,7 @@ void WifiSelectionActivity::renderSavePrompt() const {
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = "Network: " + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo = ssidInfo.substr(0, 25) + "..."; ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR);
@ -641,7 +638,7 @@ void WifiSelectionActivity::renderSavePrompt() const {
} }
void WifiSelectionActivity::renderConnectionFailed() const { void WifiSelectionActivity::renderConnectionFailed() const {
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 2) / 2; const auto top = (pageHeight - height * 2) / 2;
@ -651,8 +648,8 @@ void WifiSelectionActivity::renderConnectionFailed() const {
} }
void WifiSelectionActivity::renderForgetPrompt() const { void WifiSelectionActivity::renderForgetPrompt() const {
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 3) / 2; const auto top = (pageHeight - height * 3) / 2;
@ -660,7 +657,7 @@ void WifiSelectionActivity::renderForgetPrompt() const {
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = "Network: " + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo = ssidInfo.substr(0, 25) + "..."; ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR);

View File

@ -383,9 +383,7 @@ void CrossPointWebServer::handleFileList() {
// Folders come first // Folders come first
if (a.isDirectory != b.isDirectory) return a.isDirectory > b.isDirectory; if (a.isDirectory != b.isDirectory) return a.isDirectory > b.isDirectory;
// Then sort by epub status (epubs first among files) // Then sort by epub status (epubs first among files)
if (!a.isDirectory && !b.isDirectory) { if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub;
if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub;
}
// Then alphabetically // Then alphabetically
return a.name < b.name; return a.name < b.name;
}); });
@ -513,13 +511,6 @@ void CrossPointWebServer::handleUpload() {
Serial.printf("[%lu] [WEB] [UPLOAD] START: %s to path: %s\n", millis(), uploadFileName.c_str(), uploadPath.c_str()); Serial.printf("[%lu] [WEB] [UPLOAD] START: %s to path: %s\n", millis(), uploadFileName.c_str(), uploadPath.c_str());
Serial.printf("[%lu] [WEB] [UPLOAD] Free heap: %d bytes\n", millis(), ESP.getFreeHeap()); Serial.printf("[%lu] [WEB] [UPLOAD] Free heap: %d bytes\n", millis(), ESP.getFreeHeap());
// Validate file extension
if (!isEpubFile(uploadFileName)) {
uploadError = "Only .epub files are allowed";
Serial.printf("[%lu] [WEB] [UPLOAD] REJECTED - not an epub file\n", millis());
return;
}
// Create file path // Create file path
String filePath = uploadPath; String filePath = uploadPath;
if (!filePath.endsWith("/")) filePath += "/"; if (!filePath.endsWith("/")) filePath += "/";

View File

@ -383,7 +383,7 @@ void EpubReaderActivity::renderStatusBar() const {
title = tocItem.title; title = tocItem.title;
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
while (titleWidth > availableTextWidth && title.length() > 11) { while (titleWidth > availableTextWidth && title.length() > 11) {
title = title.substr(0, title.length() - 8) + "..."; title.replace(title.length() - 8, 8, "...");
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
} }
} }

View File

@ -93,7 +93,7 @@ void FileSelectionActivity::loop() {
} }
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) { } else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (basepath != "/") { if (basepath != "/") {
basepath = basepath.substr(0, basepath.rfind('/')); basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
if (basepath.empty()) basepath = "/"; if (basepath.empty()) basepath = "/";
loadFiles(); loadFiles();
updateRequired = true; updateRequired = true;

View File

@ -20,7 +20,7 @@ KeyboardEntryActivity::KeyboardEntryActivity(GfxRenderer& renderer, InputManager
void KeyboardEntryActivity::setText(const std::string& newText) { void KeyboardEntryActivity::setText(const std::string& newText) {
text = newText; text = newText;
if (maxLength > 0 && text.length() > maxLength) { if (maxLength > 0 && text.length() > maxLength) {
text = text.substr(0, maxLength); text.resize(maxLength);
} }
} }

View File

@ -3,15 +3,15 @@
CrossPoint E-Reader • Open Source CrossPoint E-Reader • Open Source
</p> </p>
</div> </div>
<!-- Upload Modal --> <!-- Upload Modal -->
<div class="modal-overlay" id="uploadModal"> <div class="modal-overlay" id="uploadModal">
<div class="modal"> <div class="modal">
<button class="modal-close" onclick="closeUploadModal()">&times;</button> <button class="modal-close" onclick="closeUploadModal()">&times;</button>
<h3>📤 Upload eBook</h3> <h3>📤 Upload file</h3>
<div class="upload-form"> <div class="upload-form">
<p class="file-info">Select an .epub file to upload to <strong id="uploadPathDisplay"></strong></p> <p class="file-info">Select a file to upload to <strong id="uploadPathDisplay"></strong></p>
<input type="file" id="fileInput" accept=".epub" onchange="validateFile()"> <input type="file" id="fileInput" onchange="validateFile()">
<button id="uploadBtn" class="upload-btn" onclick="uploadFile()" disabled>Upload</button> <button id="uploadBtn" class="upload-btn" onclick="uploadFile()" disabled>Upload</button>
<div id="progress-container"> <div id="progress-container">
<div id="progress-bar"><div id="progress-fill"></div></div> <div id="progress-bar"><div id="progress-fill"></div></div>
@ -20,7 +20,7 @@
</div> </div>
</div> </div>
</div> </div>
<!-- New Folder Modal --> <!-- New Folder Modal -->
<div class="modal-overlay" id="folderModal"> <div class="modal-overlay" id="folderModal">
<div class="modal"> <div class="modal">
@ -33,7 +33,7 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
<div class="modal-overlay" id="deleteModal"> <div class="modal-overlay" id="deleteModal">
<div class="modal"> <div class="modal">
@ -50,7 +50,7 @@
</div> </div>
</div> </div>
</div> </div>
<script> <script>
// Modal functions // Modal functions
function openUploadModal() { function openUploadModal() {
@ -58,7 +58,7 @@
document.getElementById('uploadPathDisplay').textContent = currentPath === '/' ? '/ 🏠' : currentPath; document.getElementById('uploadPathDisplay').textContent = currentPath === '/' ? '/ 🏠' : currentPath;
document.getElementById('uploadModal').classList.add('open'); document.getElementById('uploadModal').classList.add('open');
} }
function closeUploadModal() { function closeUploadModal() {
document.getElementById('uploadModal').classList.remove('open'); document.getElementById('uploadModal').classList.remove('open');
document.getElementById('fileInput').value = ''; document.getElementById('fileInput').value = '';
@ -67,18 +67,18 @@
document.getElementById('progress-fill').style.width = '0%'; document.getElementById('progress-fill').style.width = '0%';
document.getElementById('progress-fill').style.backgroundColor = '#27ae60'; document.getElementById('progress-fill').style.backgroundColor = '#27ae60';
} }
function openFolderModal() { function openFolderModal() {
const currentPath = document.getElementById('currentPath').value; const currentPath = document.getElementById('currentPath').value;
document.getElementById('folderPathDisplay').textContent = currentPath === '/' ? '/ 🏠' : currentPath; document.getElementById('folderPathDisplay').textContent = currentPath === '/' ? '/ 🏠' : currentPath;
document.getElementById('folderModal').classList.add('open'); document.getElementById('folderModal').classList.add('open');
document.getElementById('folderName').value = ''; document.getElementById('folderName').value = '';
} }
function closeFolderModal() { function closeFolderModal() {
document.getElementById('folderModal').classList.remove('open'); document.getElementById('folderModal').classList.remove('open');
} }
// Close modals when clicking overlay // Close modals when clicking overlay
document.querySelectorAll('.modal-overlay').forEach(function(overlay) { document.querySelectorAll('.modal-overlay').forEach(function(overlay) {
overlay.addEventListener('click', function(e) { overlay.addEventListener('click', function(e) {
@ -87,58 +87,40 @@
} }
}); });
}); });
function validateFile() { function validateFile() {
const fileInput = document.getElementById('fileInput'); const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn'); const uploadBtn = document.getElementById('uploadBtn');
const file = fileInput.files[0]; const file = fileInput.files[0];
uploadBtn.disabled = !file;
if (file) {
const fileName = file.name.toLowerCase();
if (!fileName.endsWith('.epub')) {
alert('Only .epub files are allowed!');
fileInput.value = '';
uploadBtn.disabled = true;
return;
}
uploadBtn.disabled = false;
} else {
uploadBtn.disabled = true;
}
} }
function uploadFile() { function uploadFile() {
const fileInput = document.getElementById('fileInput'); const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0]; const file = fileInput.files[0];
const currentPath = document.getElementById('currentPath').value; const currentPath = document.getElementById('currentPath').value;
if (!file) { if (!file) {
alert('Please select a file first!'); alert('Please select a file first!');
return; return;
} }
const fileName = file.name.toLowerCase();
if (!fileName.endsWith('.epub')) {
alert('Only .epub files are allowed!');
return;
}
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
const progressContainer = document.getElementById('progress-container'); const progressContainer = document.getElementById('progress-container');
const progressFill = document.getElementById('progress-fill'); const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text'); const progressText = document.getElementById('progress-text');
const uploadBtn = document.getElementById('uploadBtn'); const uploadBtn = document.getElementById('uploadBtn');
progressContainer.style.display = 'block'; progressContainer.style.display = 'block';
uploadBtn.disabled = true; uploadBtn.disabled = true;
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
// Include path as query parameter since multipart form data doesn't make // Include path as query parameter since multipart form data doesn't make
// form fields available until after file upload completes // form fields available until after file upload completes
xhr.open('POST', '/upload?path=' + encodeURIComponent(currentPath), true); xhr.open('POST', '/upload?path=' + encodeURIComponent(currentPath), true);
xhr.upload.onprogress = function(e) { xhr.upload.onprogress = function(e) {
if (e.lengthComputable) { if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100); const percent = Math.round((e.loaded / e.total) * 100);
@ -146,7 +128,7 @@
progressText.textContent = 'Uploading: ' + percent + '%'; progressText.textContent = 'Uploading: ' + percent + '%';
} }
}; };
xhr.onload = function() { xhr.onload = function() {
if (xhr.status === 200) { if (xhr.status === 200) {
progressText.textContent = 'Upload complete!'; progressText.textContent = 'Upload complete!';
@ -159,39 +141,39 @@
uploadBtn.disabled = false; uploadBtn.disabled = false;
} }
}; };
xhr.onerror = function() { xhr.onerror = function() {
progressText.textContent = 'Upload failed - network error'; progressText.textContent = 'Upload failed - network error';
progressFill.style.backgroundColor = '#e74c3c'; progressFill.style.backgroundColor = '#e74c3c';
uploadBtn.disabled = false; uploadBtn.disabled = false;
}; };
xhr.send(formData); xhr.send(formData);
} }
function createFolder() { function createFolder() {
const folderName = document.getElementById('folderName').value.trim(); const folderName = document.getElementById('folderName').value.trim();
const currentPath = document.getElementById('currentPath').value; const currentPath = document.getElementById('currentPath').value;
if (!folderName) { if (!folderName) {
alert('Please enter a folder name!'); alert('Please enter a folder name!');
return; return;
} }
// Validate folder name (no special characters except underscore and hyphen) // Validate folder name (no special characters except underscore and hyphen)
const validName = /^[a-zA-Z0-9_\-]+$/.test(folderName); const validName = /^[a-zA-Z0-9_\-]+$/.test(folderName);
if (!validName) { if (!validName) {
alert('Folder name can only contain letters, numbers, underscores, and hyphens.'); alert('Folder name can only contain letters, numbers, underscores, and hyphens.');
return; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.append('name', folderName); formData.append('name', folderName);
formData.append('path', currentPath); formData.append('path', currentPath);
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('POST', '/mkdir', true); xhr.open('POST', '/mkdir', true);
xhr.onload = function() { xhr.onload = function() {
if (xhr.status === 200) { if (xhr.status === 200) {
window.location.reload(); window.location.reload();
@ -199,14 +181,14 @@
alert('Failed to create folder: ' + xhr.responseText); alert('Failed to create folder: ' + xhr.responseText);
} }
}; };
xhr.onerror = function() { xhr.onerror = function() {
alert('Failed to create folder - network error'); alert('Failed to create folder - network error');
}; };
xhr.send(formData); xhr.send(formData);
} }
// Delete functions // Delete functions
function openDeleteModal(name, path, isFolder) { function openDeleteModal(name, path, isFolder) {
document.getElementById('deleteItemName').textContent = (isFolder ? '📁 ' : '📄 ') + name; document.getElementById('deleteItemName').textContent = (isFolder ? '📁 ' : '📄 ') + name;
@ -214,22 +196,22 @@
document.getElementById('deleteItemType').value = isFolder ? 'folder' : 'file'; document.getElementById('deleteItemType').value = isFolder ? 'folder' : 'file';
document.getElementById('deleteModal').classList.add('open'); document.getElementById('deleteModal').classList.add('open');
} }
function closeDeleteModal() { function closeDeleteModal() {
document.getElementById('deleteModal').classList.remove('open'); document.getElementById('deleteModal').classList.remove('open');
} }
function confirmDelete() { function confirmDelete() {
const path = document.getElementById('deleteItemPath').value; const path = document.getElementById('deleteItemPath').value;
const itemType = document.getElementById('deleteItemType').value; const itemType = document.getElementById('deleteItemType').value;
const formData = new FormData(); const formData = new FormData();
formData.append('path', path); formData.append('path', path);
formData.append('type', itemType); formData.append('type', itemType);
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('POST', '/delete', true); xhr.open('POST', '/delete', true);
xhr.onload = function() { xhr.onload = function() {
if (xhr.status === 200) { if (xhr.status === 200) {
window.location.reload(); window.location.reload();
@ -238,12 +220,12 @@
closeDeleteModal(); closeDeleteModal();
} }
}; };
xhr.onerror = function() { xhr.onerror = function() {
alert('Failed to delete - network error'); alert('Failed to delete - network error');
closeDeleteModal(); closeDeleteModal();
}; };
xhr.send(formData); xhr.send(formData);
} }
</script> </script>

View File

@ -5,7 +5,6 @@
#include <InputManager.h> #include <InputManager.h>
#include <SD.h> #include <SD.h>
#include <SPI.h> #include <SPI.h>
#include <WiFi.h>
#include <builtinFonts/bookerly_2b.h> #include <builtinFonts/bookerly_2b.h>
#include <builtinFonts/bookerly_bold_2b.h> #include <builtinFonts/bookerly_bold_2b.h>
#include <builtinFonts/bookerly_bold_italic_2b.h> #include <builtinFonts/bookerly_bold_italic_2b.h>
@ -203,20 +202,18 @@ void setup() {
} }
void loop() { void loop() {
static unsigned long lastLoopTime = 0;
static unsigned long maxLoopDuration = 0; static unsigned long maxLoopDuration = 0;
const unsigned long loopStartTime = millis();
unsigned long loopStartTime = millis();
static unsigned long lastMemPrint = 0; static unsigned long lastMemPrint = 0;
inputManager.update();
if (Serial && millis() - lastMemPrint >= 10000) { if (Serial && millis() - lastMemPrint >= 10000) {
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(), Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
ESP.getHeapSize(), ESP.getMinFreeHeap()); ESP.getHeapSize(), ESP.getMinFreeHeap());
lastMemPrint = millis(); lastMemPrint = millis();
} }
inputManager.update();
// Check for any user activity (button press or release) // Check for any user activity (button press or release)
static unsigned long lastActivityTime = millis(); static unsigned long lastActivityTime = millis();
if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased()) { if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased()) {
@ -237,13 +234,13 @@ void loop() {
return; return;
} }
unsigned long activityStartTime = millis(); const unsigned long activityStartTime = millis();
if (currentActivity) { if (currentActivity) {
currentActivity->loop(); currentActivity->loop();
} }
unsigned long activityDuration = millis() - activityStartTime; const unsigned long activityDuration = millis() - activityStartTime;
unsigned long loopDuration = millis() - loopStartTime; const unsigned long loopDuration = millis() - loopStartTime;
if (loopDuration > maxLoopDuration) { if (loopDuration > maxLoopDuration) {
maxLoopDuration = loopDuration; maxLoopDuration = loopDuration;
if (maxLoopDuration > 50) { if (maxLoopDuration > 50) {
@ -252,8 +249,6 @@ void loop() {
} }
} }
lastLoopTime = loopStartTime;
// Add delay at the end of the loop to prevent tight spinning // Add delay at the end of the loop to prevent tight spinning
// When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response // When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response
// Otherwise, use longer delay to save power // Otherwise, use longer delay to save power