Merge branch 'master' into feat-supsub
This commit is contained in:
@@ -49,6 +49,25 @@ std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
|
||||
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
|
||||
}
|
||||
|
||||
void PageHR::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
renderer.drawLine(xPos + xOffset, yPos + yOffset, xPos + xOffset + width - 1, yPos + yOffset);
|
||||
}
|
||||
|
||||
bool PageHR::serialize(FsFile& file) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
serialization::writePod(file, width);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<PageHR> PageHR::deserialize(FsFile& file) {
|
||||
int16_t xPos, yPos, width;
|
||||
serialization::readPod(file, xPos);
|
||||
serialization::readPod(file, yPos);
|
||||
serialization::readPod(file, width);
|
||||
return std::unique_ptr<PageHR>(new PageHR(xPos, yPos, width));
|
||||
}
|
||||
|
||||
void PageTableFragment::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
const int drawX = xPos + xOffset;
|
||||
const int drawY = yPos + yOffset;
|
||||
@@ -237,6 +256,10 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
||||
auto pt = PageTableFragment::deserialize(file);
|
||||
if (!pt) return nullptr;
|
||||
page->elements.push_back(std::move(pt));
|
||||
} else if (tag == TAG_PageHR) {
|
||||
auto hr = PageHR::deserialize(file);
|
||||
if (!hr) return nullptr;
|
||||
page->elements.push_back(std::move(hr));
|
||||
} else {
|
||||
LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
|
||||
return nullptr;
|
||||
|
||||
@@ -21,6 +21,7 @@ enum PageElementTag : uint8_t {
|
||||
TAG_PageLine = 1,
|
||||
TAG_PageImage = 2,
|
||||
TAG_PageTable = 3,
|
||||
TAG_PageHR = 4,
|
||||
};
|
||||
|
||||
// represents something that has been added to a page
|
||||
@@ -63,6 +64,17 @@ class PageImage final : public PageElement {
|
||||
const ImageBlock& getImageBlock() const { return *imageBlock; }
|
||||
};
|
||||
|
||||
class PageHR final : public PageElement {
|
||||
int16_t width;
|
||||
|
||||
public:
|
||||
PageHR(const int16_t xPos, const int16_t yPos, const int16_t width) : PageElement(xPos, yPos), width(width) {}
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(FsFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageHR; }
|
||||
static std::unique_ptr<PageHR> deserialize(FsFile& file);
|
||||
};
|
||||
|
||||
struct TableCell {
|
||||
std::vector<std::shared_ptr<TextBlock>> lines;
|
||||
bool isHeader = false;
|
||||
|
||||
+33
-36
@@ -14,30 +14,27 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 27;
|
||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
|
||||
sizeof(int) + // fontId
|
||||
sizeof(float) + // lineCompression
|
||||
sizeof(bool) + // extraParagraphSpacing
|
||||
sizeof(uint8_t) + // paragraphAlignment
|
||||
sizeof(uint16_t) + // viewportWidth
|
||||
sizeof(uint16_t) + // viewportHeight
|
||||
sizeof(bool) + // parseComplete
|
||||
sizeof(uint16_t) + // pageCount (stored as 16-bit in header)
|
||||
sizeof(bool) + // hyphenationEnabled
|
||||
sizeof(bool) + // embeddedStyle
|
||||
sizeof(bool) + // bionicReadingEnabled
|
||||
sizeof(uint8_t) + // imageRendering
|
||||
sizeof(uint32_t) + // page LUT offset
|
||||
sizeof(uint32_t) + // anchor map offset
|
||||
sizeof(uint32_t); // paragraph LUT offset
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 28;
|
||||
|
||||
constexpr uint32_t HEADER_TAIL_PARSE_COMPLETE_OFFSET =
|
||||
HEADER_SIZE - sizeof(uint32_t) * 3 - sizeof(uint16_t) - sizeof(bool);
|
||||
constexpr uint32_t HEADER_TAIL_PAGE_COUNT_OFFSET = HEADER_SIZE - sizeof(uint32_t) * 3 - sizeof(uint16_t);
|
||||
constexpr uint32_t HEADER_TAIL_PAGE_LUT_OFFSET = HEADER_SIZE - sizeof(uint32_t) * 3;
|
||||
constexpr uint32_t HEADER_TAIL_ANCHOR_OFFSET = HEADER_SIZE - sizeof(uint32_t) * 2;
|
||||
constexpr uint32_t HEADER_TAIL_PARAGRAPH_LUT_OFFSET = HEADER_SIZE - sizeof(uint32_t);
|
||||
namespace header {
|
||||
constexpr uint32_t kVersion = 0;
|
||||
constexpr uint32_t kFontId = kVersion + sizeof(uint8_t);
|
||||
constexpr uint32_t kLineCompression = kFontId + sizeof(int);
|
||||
constexpr uint32_t kExtraParagraphSpacing = kLineCompression + sizeof(float);
|
||||
constexpr uint32_t kParagraphAlignment = kExtraParagraphSpacing + sizeof(bool);
|
||||
constexpr uint32_t kViewportWidth = kParagraphAlignment + sizeof(uint8_t);
|
||||
constexpr uint32_t kViewportHeight = kViewportWidth + sizeof(uint16_t);
|
||||
constexpr uint32_t kHyphenationEnabled = kViewportHeight + sizeof(uint16_t);
|
||||
constexpr uint32_t kEmbeddedStyle = kHyphenationEnabled + sizeof(bool);
|
||||
constexpr uint32_t kBionicReadingEnabled = kEmbeddedStyle + sizeof(bool);
|
||||
constexpr uint32_t kImageRendering = kBionicReadingEnabled + sizeof(bool);
|
||||
constexpr uint32_t kParseComplete = kImageRendering + sizeof(uint8_t);
|
||||
constexpr uint32_t kPageCount = kParseComplete + sizeof(bool);
|
||||
constexpr uint32_t kPageLut = kPageCount + sizeof(uint16_t);
|
||||
constexpr uint32_t kAnchorMap = kPageLut + sizeof(uint32_t);
|
||||
constexpr uint32_t kParagraphLut = kAnchorMap + sizeof(uint32_t);
|
||||
constexpr uint32_t kSize = kParagraphLut + sizeof(uint32_t);
|
||||
} // namespace header
|
||||
|
||||
// On-disk paragraph LUT entry: u32 xhtmlByteOffset + u16 paragraphIndex + u16 listItemIndex.
|
||||
// listItemIndex is the running <li> count at page-break time; together with
|
||||
@@ -217,11 +214,12 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
LOG_DBG("SCT", "File not open for writing header");
|
||||
return;
|
||||
}
|
||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
||||
sizeof(viewportHeight) + sizeof(bool) + sizeof(pageCount) +
|
||||
sizeof(hyphenationEnabled) + sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) +
|
||||
sizeof(imageRendering) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||
static_assert(header::kSize == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) +
|
||||
sizeof(viewportWidth) + sizeof(viewportHeight) + sizeof(hyphenationEnabled) +
|
||||
sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) + sizeof(imageRendering) +
|
||||
sizeof(bool) + sizeof(pageCount) + sizeof(uint32_t) + sizeof(uint32_t) +
|
||||
sizeof(uint32_t),
|
||||
"Header size mismatch");
|
||||
serialization::writePod(file, SECTION_FILE_VERSION);
|
||||
serialization::writePod(file, fontId);
|
||||
@@ -338,8 +336,8 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
}
|
||||
for (uint32_t& pos : lut) {
|
||||
serialization::readPod(file, pos);
|
||||
if (pos < HEADER_SIZE || pos >= lutOffset) {
|
||||
LOG_ERR("SCT", "Deserialization failed: LUT entry %u out of range [%u, %u)", pos, HEADER_SIZE, lutOffset);
|
||||
if (pos < header::kSize || pos >= lutOffset) {
|
||||
LOG_ERR("SCT", "Deserialization failed: LUT entry %u out of range [%u, %u)", pos, header::kSize, lutOffset);
|
||||
clearCache();
|
||||
return false;
|
||||
}
|
||||
@@ -572,9 +570,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
}
|
||||
|
||||
// Patch header with final parseComplete/pageCount and offsets.
|
||||
const size_t headerPatchStart = HEADER_TAIL_PARSE_COMPLETE_OFFSET;
|
||||
const size_t headerPatchStart = header::kParseComplete;
|
||||
if (!file.seek(headerPatchStart)) {
|
||||
LOG_ERR("SCT", "Failed to seek to section header patch offset %u", HEADER_TAIL_PARSE_COMPLETE_OFFSET);
|
||||
LOG_ERR("SCT", "Failed to seek to section header patch offset %u", header::kParseComplete);
|
||||
file.close();
|
||||
Storage.remove(filePath.c_str());
|
||||
return false;
|
||||
@@ -730,8 +728,7 @@ void Section::buildTocBoundariesFromFile(FsFile& f) {
|
||||
|
||||
// Single pass through on-disk anchors, matching against cached TOC anchors.
|
||||
// Stop early once all TOC anchors are resolved.
|
||||
// Header layout: ... | lutOffset (u32) | anchorMapOffset (u32) | paragraphLutOffset (u32) |
|
||||
f.seek(HEADER_TAIL_ANCHOR_OFFSET);
|
||||
f.seek(header::kAnchorMap);
|
||||
uint32_t anchorMapOffset;
|
||||
serialization::readPod(f, anchorMapOffset);
|
||||
|
||||
@@ -801,7 +798,7 @@ std::optional<uint16_t> Section::getPageForAnchor(const std::string& anchor) con
|
||||
}
|
||||
|
||||
const uint32_t fileSize = f.size();
|
||||
f.seek(HEADER_TAIL_ANCHOR_OFFSET);
|
||||
f.seek(header::kAnchorMap);
|
||||
uint32_t anchorMapOffset;
|
||||
serialization::readPod(f, anchorMapOffset);
|
||||
if (anchorMapOffset == 0 || anchorMapOffset >= fileSize) {
|
||||
@@ -834,7 +831,7 @@ bool Section::readParagraphLutHeader(FsFile& outFile, uint16_t& outCount, uint32
|
||||
|
||||
const uint32_t fileSize = outFile.size();
|
||||
|
||||
outFile.seek(HEADER_TAIL_PARAGRAPH_LUT_OFFSET);
|
||||
outFile.seek(header::kParagraphLut);
|
||||
uint32_t paragraphLutOffset;
|
||||
serialization::readPod(outFile, paragraphLutOffset);
|
||||
if (fileSize < sizeof(uint16_t) || paragraphLutOffset == 0 || paragraphLutOffset > fileSize - sizeof(uint16_t)) {
|
||||
|
||||
@@ -1064,6 +1064,28 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
self->preUntilDepth = std::min(self->preUntilDepth, self->depth);
|
||||
}
|
||||
}
|
||||
} else if (strcmp(name, "hr") == 0) {
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
if (!self->flushPartWordBuffer()) return;
|
||||
}
|
||||
self->makePages();
|
||||
if (!self->currentPage) {
|
||||
self->currentPage.reset(new Page());
|
||||
self->currentPageNextY = 0;
|
||||
}
|
||||
const int lineHeight = static_cast<int>(self->renderer.getLineHeight(self->fontId) * self->lineCompression + 0.5f);
|
||||
const int16_t marginV = static_cast<int16_t>(lineHeight / 2);
|
||||
self->currentPageNextY += marginV;
|
||||
if (self->currentPageNextY + 1 + marginV > self->viewportHeight) {
|
||||
self->emitPage(self->lastBodyChildByteOffset);
|
||||
self->currentPage.reset(new Page());
|
||||
self->currentPageNextY = 0;
|
||||
}
|
||||
self->currentPage->elements.push_back(
|
||||
std::make_shared<PageHR>(0, self->currentPageNextY, static_cast<int16_t>(self->viewportWidth)));
|
||||
self->currentPageNextY += 1 + marginV;
|
||||
BlockStyle emptyStyle;
|
||||
self->startNewTextBlock(emptyStyle);
|
||||
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) ||
|
||||
matches(name, STRIKETHROUGH_TAGS, NUM_STRIKETHROUGH_TAGS)) {
|
||||
// Flush buffer before style change so preceding text gets current style
|
||||
|
||||
@@ -30,6 +30,8 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title
|
||||
int8_t bionicReadingOverride = -1;
|
||||
int8_t paragraphAlignmentOverride = -1;
|
||||
|
||||
pruneMissing();
|
||||
|
||||
// Remove existing entry if present
|
||||
auto it =
|
||||
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
|
||||
@@ -66,6 +68,14 @@ void RecentBooksStore::removeBook(const std::string& path) {
|
||||
}
|
||||
}
|
||||
|
||||
bool RecentBooksStore::isMissing(const RecentBook& book) { return !Storage.exists(book.path.c_str()); }
|
||||
|
||||
bool RecentBooksStore::pruneMissing() {
|
||||
const size_t before = recentBooks.size();
|
||||
recentBooks.erase(std::remove_if(recentBooks.begin(), recentBooks.end(), &isMissing), recentBooks.end());
|
||||
return recentBooks.size() != before;
|
||||
}
|
||||
|
||||
void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author,
|
||||
const std::string& series, const std::string& coverBmpPath) {
|
||||
auto it =
|
||||
|
||||
@@ -62,6 +62,13 @@ class RecentBooksStore {
|
||||
// Get the count of recent books
|
||||
int getCount() const { return static_cast<int>(recentBooks.size()); }
|
||||
|
||||
// Returns true if the book's file is missing from storage
|
||||
static bool isMissing(const RecentBook& book);
|
||||
|
||||
// Remove entries whose backing file is no longer on the SD card.
|
||||
// Returns true if any entry was removed. Does not persist — caller decides.
|
||||
bool pruneMissing();
|
||||
|
||||
bool saveToFile() const;
|
||||
|
||||
bool loadFromFile();
|
||||
|
||||
@@ -168,8 +168,7 @@ void HomeActivity::loadRecentBooks(int maxBooks) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip if file no longer exists
|
||||
if (!Storage.exists(book.path.c_str())) {
|
||||
if (RecentBooksStore::isMissing(book)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,15 +68,7 @@ std::string gridThumbPath(const std::string& coverBmpPath, int tw, int th) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void RecentBooksActivity::loadRecentBooks() {
|
||||
recentBooks.clear();
|
||||
const auto& books = RECENT_BOOKS.getBooks();
|
||||
recentBooks.reserve(books.size());
|
||||
for (const auto& book : books) {
|
||||
if (!Storage.exists(book.path.c_str())) continue;
|
||||
recentBooks.push_back(book);
|
||||
}
|
||||
}
|
||||
void RecentBooksActivity::loadRecentBooks() { recentBooks = RECENT_BOOKS.getBooks(); }
|
||||
|
||||
bool RecentBooksActivity::loadNextCover() {
|
||||
const Rect contentRect = UITheme::getContentRect(renderer, true, true);
|
||||
@@ -134,6 +126,10 @@ bool RecentBooksActivity::loadNextCover() {
|
||||
void RecentBooksActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
if (RECENT_BOOKS.pruneMissing()) {
|
||||
RECENT_BOOKS.saveToFile();
|
||||
}
|
||||
|
||||
loadRecentBooks();
|
||||
|
||||
selectorIndex = 0;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "FontInstaller.h"
|
||||
#include "HttpFileStreamer.h"
|
||||
#include "OpdsServerStore.h"
|
||||
#include "SdCardFontGlobals.h"
|
||||
#include "SdCardFontRegistry.h"
|
||||
@@ -681,27 +682,12 @@ void CrossPointWebServer::handleDownload() const {
|
||||
server->send(200, contentType.c_str(), "");
|
||||
|
||||
NetworkClient client = server->client();
|
||||
const size_t chunkSize = 4096;
|
||||
uint8_t buffer[chunkSize];
|
||||
|
||||
bool downloadOk = true;
|
||||
while (downloadOk && file.available()) {
|
||||
int result = file.read(buffer, chunkSize);
|
||||
if (result <= 0) break;
|
||||
size_t bytesRead = static_cast<size_t>(result);
|
||||
size_t totalWritten = 0;
|
||||
while (totalWritten < bytesRead) {
|
||||
esp_task_wdt_reset();
|
||||
size_t wrote = client.write(buffer + totalWritten, bytesRead - totalWritten);
|
||||
if (wrote == 0) {
|
||||
downloadOk = false;
|
||||
break;
|
||||
}
|
||||
totalWritten += wrote;
|
||||
}
|
||||
}
|
||||
bool downloadOk = HttpFileStreamer::streamFileToClient(file, client);
|
||||
client.clear();
|
||||
file.close();
|
||||
|
||||
if (!downloadOk) {
|
||||
LOG_DBG("WEB", "Download interrupted while streaming: %s", itemPath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Diagnostic counters for upload performance analysis
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "HttpFileStreamer.h"
|
||||
|
||||
#include <Logging.h>
|
||||
#include <esp_task_wdt.h>
|
||||
|
||||
namespace {
|
||||
constexpr size_t DOWNLOAD_CHUNK_SIZE = 4096;
|
||||
}
|
||||
|
||||
namespace HttpFileStreamer {
|
||||
bool streamFileToClient(FsFile& file, NetworkClient& client) {
|
||||
auto* buffer = static_cast<uint8_t*>(malloc(DOWNLOAD_CHUNK_SIZE));
|
||||
if (!buffer) {
|
||||
LOG_ERR("HTTP", "malloc failed: %zu bytes", DOWNLOAD_CHUNK_SIZE);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
while (ok) {
|
||||
esp_task_wdt_reset();
|
||||
int result = file.read(buffer, DOWNLOAD_CHUNK_SIZE);
|
||||
if (result < 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (result == 0) break;
|
||||
|
||||
size_t bytesRead = static_cast<size_t>(result);
|
||||
size_t totalWritten = 0;
|
||||
while (totalWritten < bytesRead) {
|
||||
esp_task_wdt_reset();
|
||||
size_t wrote = client.write(buffer + totalWritten, bytesRead - totalWritten);
|
||||
if (wrote == 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
totalWritten += wrote;
|
||||
}
|
||||
}
|
||||
|
||||
free(buffer);
|
||||
return ok;
|
||||
}
|
||||
} // namespace HttpFileStreamer
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <NetworkClient.h>
|
||||
|
||||
namespace HttpFileStreamer {
|
||||
bool streamFileToClient(FsFile& file, NetworkClient& client);
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <Xtc.h>
|
||||
#include <esp_task_wdt.h>
|
||||
|
||||
#include "HttpFileStreamer.h"
|
||||
|
||||
namespace {
|
||||
const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"};
|
||||
constexpr size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]);
|
||||
@@ -330,8 +332,12 @@ void WebDAVHandler::handleGet(WebServer& s) {
|
||||
s.send(200, contentType.c_str(), "");
|
||||
|
||||
NetworkClient client = s.client();
|
||||
client.write(file);
|
||||
file.close();
|
||||
bool downloadOk = HttpFileStreamer::streamFileToClient(file, client);
|
||||
client.clear();
|
||||
|
||||
if (!downloadOk) {
|
||||
LOG_DBG("DAV", "GET interrupted while streaming: %s", path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// ── HEAD ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user