A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove tinyxml2 dependency replace with expat parsers (#9)

authored by

Dave Allie and committed by
GitHub
c7a32fe4 d450f362

+677 -234
+78 -170
lib/Epub/Epub.cpp
··· 7 7 #include <map> 8 8 9 9 #include "Epub/FsHelpers.h" 10 + #include "Epub/parsers/ContainerParser.h" 11 + #include "Epub/parsers/ContentOpfParser.h" 12 + #include "Epub/parsers/TocNcxParser.h" 10 13 11 - bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) { 12 - // open up the meta data to find where the content.opf file lives 13 - size_t s; 14 - const auto metaInfo = reinterpret_cast<char*>(zip.readFileToMemory("META-INF/container.xml", &s, true)); 15 - if (!metaInfo) { 16 - Serial.printf("[%lu] [EBP] Could not find META-INF/container.xml\n", millis()); 14 + bool Epub::findContentOpfFile(std::string* contentOpfFile) const { 15 + const auto containerPath = "META-INF/container.xml"; 16 + size_t containerSize; 17 + 18 + // Get file size without loading it all into heap 19 + if (!getItemSize(containerPath, &containerSize)) { 20 + Serial.printf("[%lu] [EBP] Could not find or size META-INF/container.xml\n", millis()); 17 21 return false; 18 22 } 19 23 20 - // parse the meta data 21 - tinyxml2::XMLDocument metaDataDoc; 22 - const auto result = metaDataDoc.Parse(metaInfo); 23 - free(metaInfo); 24 + ContainerParser containerParser(containerSize); 24 25 25 - if (result != tinyxml2::XML_SUCCESS) { 26 - Serial.printf("[%lu] [EBP] Could not parse META-INF/container.xml. Error: %d\n", millis(), result); 26 + if (!containerParser.setup()) { 27 27 return false; 28 28 } 29 29 30 - const auto container = metaDataDoc.FirstChildElement("container"); 31 - if (!container) { 32 - Serial.printf("[%lu] [EBP] Could not find container element in META-INF/container.xml\n", millis()); 30 + // Stream read (reusing your existing stream logic) 31 + if (!readItemContentsToStream(containerPath, containerParser, 512)) { 32 + Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis()); 33 + containerParser.teardown(); 33 34 return false; 34 35 } 35 36 36 - const auto rootfiles = container->FirstChildElement("rootfiles"); 37 - if (!rootfiles) { 38 - Serial.printf("[%lu] [EBP] Could not find rootfiles element in META-INF/container.xml\n", millis()); 37 + // Extract the result 38 + if (containerParser.fullPath.empty()) { 39 + Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis()); 40 + containerParser.teardown(); 39 41 return false; 40 42 } 41 43 42 - // find the root file that has the media-type="application/oebps-package+xml" 43 - auto rootfile = rootfiles->FirstChildElement("rootfile"); 44 - while (rootfile) { 45 - const char* mediaType = rootfile->Attribute("media-type"); 46 - if (mediaType && strcmp(mediaType, "application/oebps-package+xml") == 0) { 47 - const char* full_path = rootfile->Attribute("full-path"); 48 - if (full_path) { 49 - contentOpfFile = full_path; 50 - return true; 51 - } 52 - } 53 - rootfile = rootfile->NextSiblingElement("rootfile"); 54 - } 44 + *contentOpfFile = std::move(containerParser.fullPath); 55 45 56 - Serial.printf("[%lu] [EBP] Could not get path to content.opf file\n", millis()); 57 - return false; 46 + containerParser.teardown(); 47 + return true; 58 48 } 59 49 60 - bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) { 61 - // read in the content.opf file and parse it 62 - auto contents = reinterpret_cast<char*>(zip.readFileToMemory(content_opf_file.c_str(), nullptr, true)); 63 - 64 - // parse the contents 65 - tinyxml2::XMLDocument doc; 66 - auto result = doc.Parse(contents); 67 - free(contents); 68 - 69 - if (result != tinyxml2::XML_SUCCESS) { 70 - Serial.printf("[%lu] [EBP] Error parsing content.opf - %s\n", millis(), 71 - tinyxml2::XMLDocument::ErrorIDToName(result)); 50 + bool Epub::parseContentOpf(const std::string& contentOpfFilePath) { 51 + size_t contentOpfSize; 52 + if (!getItemSize(contentOpfFilePath, &contentOpfSize)) { 53 + Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis()); 72 54 return false; 73 55 } 74 56 75 - auto package = doc.FirstChildElement("package"); 76 - if (!package) package = doc.FirstChildElement("opf:package"); 57 + ContentOpfParser opfParser(getBasePath(), contentOpfSize); 77 58 78 - if (!package) { 79 - Serial.printf("[%lu] [EBP] Could not find package element in content.opf\n", millis()); 59 + if (!opfParser.setup()) { 60 + Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis()); 80 61 return false; 81 62 } 82 63 83 - // get the metadata - title and cover image 84 - auto metadata = package->FirstChildElement("metadata"); 85 - if (!metadata) metadata = package->FirstChildElement("opf:metadata"); 86 - if (!metadata) { 87 - Serial.printf("[%lu] [EBP] Missing metadata\n", millis()); 64 + if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) { 65 + Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis()); 66 + opfParser.teardown(); 88 67 return false; 89 68 } 90 69 91 - auto titleEl = metadata->FirstChildElement("dc:title"); 92 - if (!titleEl) { 93 - Serial.printf("[%lu] [EBP] Missing title\n", millis()); 94 - return false; 95 - } 96 - this->title = titleEl->GetText(); 70 + // Grab data from opfParser into epub 71 + title = opfParser.title; 97 72 98 - auto cover = metadata->FirstChildElement("meta"); 99 - if (!cover) cover = metadata->FirstChildElement("opf:meta"); 100 - while (cover && cover->Attribute("name") && strcmp(cover->Attribute("name"), "cover") != 0) { 101 - cover = cover->NextSiblingElement("meta"); 102 - } 103 - if (!cover) { 104 - Serial.printf("[%lu] [EBP] Missing cover\n", millis()); 73 + if (opfParser.items.count("ncx")) { 74 + tocNcxItem = opfParser.items.at("ncx"); 75 + } else if (opfParser.items.count("ncxtoc")) { 76 + tocNcxItem = opfParser.items.at("ncxtoc"); 105 77 } 106 - auto coverItem = cover ? cover->Attribute("content") : nullptr; 107 78 108 - // read the manifest and spine 109 - // the manifest gives us the names of the files 110 - // the spine gives us the order of the files 111 - // we can then read the files in the order they are in the spine 112 - auto manifest = package->FirstChildElement("manifest"); 113 - if (!manifest) manifest = package->FirstChildElement("opf:manifest"); 114 - if (!manifest) { 115 - Serial.printf("[%lu] [EBP] Missing manifest\n", millis()); 116 - return false; 117 - } 118 - 119 - // create a mapping from id to file name 120 - auto item = manifest->FirstChildElement("item"); 121 - if (!item) item = manifest->FirstChildElement("opf:item"); 122 - std::map<std::string, std::string> items; 123 - 124 - while (item) { 125 - std::string itemId = item->Attribute("id"); 126 - std::string href = contentBasePath + item->Attribute("href"); 127 - 128 - // grab the cover image 129 - if (coverItem && itemId == coverItem) { 130 - coverImageItem = href; 131 - } 132 - 133 - // grab the ncx file 134 - if (itemId == "ncx" || itemId == "ncxtoc") { 135 - tocNcxItem = href; 79 + for (auto& spineRef : opfParser.spineRefs) { 80 + if (opfParser.items.count(spineRef)) { 81 + spine.emplace_back(spineRef, opfParser.items.at(spineRef)); 136 82 } 137 - 138 - items[itemId] = href; 139 - auto nextItem = item->NextSiblingElement("item"); 140 - if (!nextItem) nextItem = item->NextSiblingElement("opf:item"); 141 - item = nextItem; 142 83 } 143 84 144 - // find the spine 145 - auto spineEl = package->FirstChildElement("spine"); 146 - if (!spineEl) spineEl = package->FirstChildElement("opf:spine"); 147 - if (!spineEl) { 148 - Serial.printf("[%lu] [EBP] Missing spine\n", millis()); 149 - return false; 150 - } 85 + Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis()); 151 86 152 - // read the spine 153 - auto itemref = spineEl->FirstChildElement("itemref"); 154 - if (!itemref) itemref = spineEl->FirstChildElement("opf:itemref"); 155 - while (itemref) { 156 - auto id = itemref->Attribute("idref"); 157 - if (items.find(id) != items.end()) { 158 - spine.emplace_back(id, items[id]); 159 - } 160 - auto nextItemRef = itemref->NextSiblingElement("itemref"); 161 - if (!nextItemRef) nextItemRef = itemref->NextSiblingElement("opf:itemref"); 162 - itemref = nextItemRef; 163 - } 87 + opfParser.teardown(); 164 88 return true; 165 89 } 166 90 167 - bool Epub::parseTocNcxFile(const ZipFile& zip) { 91 + bool Epub::parseTocNcxFile() { 168 92 // the ncx file should have been specified in the content.opf file 169 93 if (tocNcxItem.empty()) { 170 94 Serial.printf("[%lu] [EBP] No ncx file specified\n", millis()); 171 95 return false; 172 96 } 173 97 174 - const auto ncxData = reinterpret_cast<char*>(zip.readFileToMemory(tocNcxItem.c_str(), nullptr, true)); 175 - if (!ncxData) { 176 - Serial.printf("[%lu] [EBP] Could not find %s\n", millis(), tocNcxItem.c_str()); 98 + size_t tocSize; 99 + if (!getItemSize(tocNcxItem, &tocSize)) { 100 + Serial.printf("[%lu] [EBP] Could not get size of toc ncx\n", millis()); 177 101 return false; 178 102 } 179 103 180 - // Parse the Toc contents 181 - tinyxml2::XMLDocument doc; 182 - const auto result = doc.Parse(ncxData); 183 - free(ncxData); 104 + TocNcxParser ncxParser(contentBasePath, tocSize); 184 105 185 - if (result != tinyxml2::XML_SUCCESS) { 186 - Serial.printf("[%lu] [EBP] Error parsing toc %s\n", millis(), tinyxml2::XMLDocument::ErrorIDToName(result)); 187 - return false; 188 - } 189 - 190 - const auto ncx = doc.FirstChildElement("ncx"); 191 - if (!ncx) { 192 - Serial.printf("[%lu] [EBP] Could not find first child ncx in toc\n", millis()); 106 + if (!ncxParser.setup()) { 107 + Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis()); 193 108 return false; 194 109 } 195 110 196 - const auto navMap = ncx->FirstChildElement("navMap"); 197 - if (!navMap) { 198 - Serial.printf("[%lu] [EBP] Could not find navMap child in ncx\n", millis()); 111 + if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) { 112 + Serial.printf("[%lu] [EBP] Could not read toc ncx stream\n", millis()); 113 + ncxParser.teardown(); 199 114 return false; 200 115 } 201 116 202 - recursivelyParseNavMap(navMap->FirstChildElement("navPoint")); 203 - return true; 204 - } 205 - 206 - void Epub::recursivelyParseNavMap(tinyxml2::XMLElement* element) { 207 - // Fills toc map 208 - while (element) { 209 - std::string navTitle = element->FirstChildElement("navLabel")->FirstChildElement("text")->FirstChild()->Value(); 210 - const auto content = element->FirstChildElement("content"); 211 - std::string href = contentBasePath + content->Attribute("src"); 212 - // split the href on the # to get the href and the anchor 213 - const size_t pos = href.find('#'); 214 - std::string anchor; 117 + this->toc = std::move(ncxParser.toc); 215 118 216 - if (pos != std::string::npos) { 217 - anchor = href.substr(pos + 1); 218 - href = href.substr(0, pos); 219 - } 119 + Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size()); 220 120 221 - toc.emplace_back(navTitle, href, anchor, 0); 222 - 223 - tinyxml2::XMLElement* nestedNavPoint = element->FirstChildElement("navPoint"); 224 - if (nestedNavPoint) { 225 - recursivelyParseNavMap(nestedNavPoint); 226 - } 227 - element = element->NextSiblingElement("navPoint"); 228 - } 121 + ncxParser.teardown(); 122 + return true; 229 123 } 230 124 231 125 // load in the meta data for the epub file 232 126 bool Epub::load() { 127 + Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str()); 233 128 ZipFile zip("/sd" + filepath); 234 129 235 - std::string contentOpfFile; 236 - if (!findContentOpfFile(zip, contentOpfFile)) { 237 - Serial.printf("[%lu] [EBP] Could not open ePub\n", millis()); 130 + std::string contentOpfFilePath; 131 + if (!findContentOpfFile(&contentOpfFilePath)) { 132 + Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis()); 238 133 return false; 239 134 } 240 135 241 - contentBasePath = contentOpfFile.substr(0, contentOpfFile.find_last_of('/') + 1); 136 + Serial.printf("[%lu] [EBP] Found content.opf at: %s\n", millis(), contentOpfFilePath.c_str()); 137 + 138 + contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1); 242 139 243 - if (!parseContentOpf(zip, contentOpfFile)) { 140 + if (!parseContentOpf(contentOpfFilePath)) { 141 + Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis()); 244 142 return false; 245 143 } 246 144 247 - if (!parseTocNcxFile(zip)) { 145 + if (!parseTocNcxFile()) { 146 + Serial.printf("[%lu] [EBP] Could not parse toc\n", millis()); 248 147 return false; 249 148 } 149 + 150 + Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str()); 250 151 251 152 return true; 252 153 } ··· 342 243 const std::string path = normalisePath(itemHref); 343 244 344 245 return zip.readFileToStream(path.c_str(), out, chunkSize); 246 + } 247 + 248 + bool Epub::getItemSize(const std::string& itemHref, size_t* size) const { 249 + const ZipFile zip("/sd" + filepath); 250 + const std::string path = normalisePath(itemHref); 251 + 252 + return zip.getInflatedFileSize(path.c_str(), size); 345 253 } 346 254 347 255 int Epub::getSpineItemsCount() const { return spine.size(); }
+6 -16
lib/Epub/Epub.h
··· 1 1 #pragma once 2 2 #include <Print.h> 3 - #include <tinyxml2.h> 4 3 5 4 #include <string> 6 5 #include <unordered_map> 7 6 #include <vector> 8 7 9 - class ZipFile; 8 + #include "Epub/EpubTocEntry.h" 10 9 11 - class EpubTocEntry { 12 - public: 13 - std::string title; 14 - std::string href; 15 - std::string anchor; 16 - int level; 17 - EpubTocEntry(std::string title, std::string href, std::string anchor, const int level) 18 - : title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {} 19 - }; 10 + class ZipFile; 20 11 21 12 class Epub { 22 13 // the title read from the EPUB meta data ··· 36 27 // Uniq cache key based on filepath 37 28 std::string cachePath; 38 29 39 - // find the path for the content.opf file 40 - static bool findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile); 41 - bool parseContentOpf(ZipFile& zip, std::string& content_opf_file); 42 - bool parseTocNcxFile(const ZipFile& zip); 43 - void recursivelyParseNavMap(tinyxml2::XMLElement* element); 30 + bool findContentOpfFile(std::string* contentOpfFile) const; 31 + bool parseContentOpf(const std::string& contentOpfFilePath); 32 + bool parseTocNcxFile(); 44 33 45 34 public: 46 35 explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) { ··· 59 48 uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr, 60 49 bool trailingNullByte = false) const; 61 50 bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const; 51 + bool getItemSize(const std::string& itemHref, size_t* size) const; 62 52 std::string& getSpineItem(int spineIndex); 63 53 int getSpineItemsCount() const; 64 54 EpubTocEntry& getTocItem(int tocTndex);
+13 -29
lib/Epub/Epub/EpubHtmlParserSlim.cpp lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
··· 1 - #include "EpubHtmlParserSlim.h" 1 + #include "ChapterHtmlSlimParser.h" 2 2 3 3 #include <GfxRenderer.h> 4 4 #include <HardwareSerial.h> 5 5 #include <expat.h> 6 6 7 - #include "Page.h" 8 - #include "htmlEntities.h" 7 + #include "../Page.h" 8 + #include "../htmlEntities.h" 9 9 10 10 const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"}; 11 11 constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]); ··· 38 38 } 39 39 40 40 // start a new text block if needed 41 - void EpubHtmlParserSlim::startNewTextBlock(const TextBlock::BLOCK_STYLE style) { 41 + void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::BLOCK_STYLE style) { 42 42 if (currentTextBlock) { 43 43 // already have a text block running and it is empty - just reuse it 44 44 if (currentTextBlock->isEmpty()) { ··· 51 51 currentTextBlock.reset(new ParsedText(style)); 52 52 } 53 53 54 - void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { 55 - auto* self = static_cast<EpubHtmlParserSlim*>(userData); 54 + void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { 55 + auto* self = static_cast<ChapterHtmlSlimParser*>(userData); 56 56 (void)atts; 57 57 58 58 // Middle of skip ··· 62 62 } 63 63 64 64 if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) { 65 - // const char* src = element.Attribute("src"); 66 - // if (src) { 67 - // // don't leave an empty text block in the list 68 - // // const BLOCK_STYLE style = currentTextBlock->get_style(); 69 - // if (currentTextBlock->isEmpty()) { 70 - // delete currentTextBlock; 71 - // currentTextBlock = nullptr; 72 - // } 73 - // // TODO: Fix this 74 - // // blocks.push_back(new ImageBlock(m_base_path + src)); 75 - // // start a new text block - with the same style as before 76 - // // startNewTextBlock(style); 77 - // } else { 78 - // // ESP_LOGE(TAG, "Could not find src attribute"); 79 - // } 80 - 81 - // start skip 65 + // TODO: Start processing image tags 82 66 self->skipUntilDepth = self->depth; 83 67 self->depth += 1; 84 68 return; ··· 109 93 self->depth += 1; 110 94 } 111 95 112 - void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s, const int len) { 113 - auto* self = static_cast<EpubHtmlParserSlim*>(userData); 96 + void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char* s, const int len) { 97 + auto* self = static_cast<ChapterHtmlSlimParser*>(userData); 114 98 115 99 // Middle of skip 116 100 if (self->skipUntilDepth < self->depth) { ··· 149 133 } 150 134 } 151 135 152 - void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name) { 153 - auto* self = static_cast<EpubHtmlParserSlim*>(userData); 136 + void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) { 137 + auto* self = static_cast<ChapterHtmlSlimParser*>(userData); 154 138 (void)name; 155 139 156 140 if (self->partWordBufferIndex > 0) { ··· 196 180 } 197 181 } 198 182 199 - bool EpubHtmlParserSlim::parseAndBuildPages() { 183 + bool ChapterHtmlSlimParser::parseAndBuildPages() { 200 184 startNewTextBlock(TextBlock::JUSTIFIED); 201 185 202 186 const XML_Parser parser = XML_ParserCreate(nullptr); ··· 261 245 return true; 262 246 } 263 247 264 - void EpubHtmlParserSlim::makePages() { 248 + void ChapterHtmlSlimParser::makePages() { 265 249 if (!currentTextBlock) { 266 250 Serial.printf("[%lu] [EHP] !! No text block to make pages for !!\n", millis()); 267 251 return;
+8 -8
lib/Epub/Epub/EpubHtmlParserSlim.h lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
··· 6 6 #include <functional> 7 7 #include <memory> 8 8 9 - #include "ParsedText.h" 10 - #include "blocks/TextBlock.h" 9 + #include "../ParsedText.h" 10 + #include "../blocks/TextBlock.h" 11 11 12 12 class Page; 13 13 class GfxRenderer; 14 14 15 15 #define MAX_WORD_SIZE 200 16 16 17 - class EpubHtmlParserSlim { 17 + class ChapterHtmlSlimParser { 18 18 const char* filepath; 19 19 GfxRenderer& renderer; 20 20 std::function<void(std::unique_ptr<Page>)> completePageFn; ··· 44 44 static void XMLCALL endElement(void* userData, const XML_Char* name); 45 45 46 46 public: 47 - explicit EpubHtmlParserSlim(const char* filepath, GfxRenderer& renderer, const int fontId, 48 - const float lineCompression, const int marginTop, const int marginRight, 49 - const int marginBottom, const int marginLeft, 50 - const std::function<void(std::unique_ptr<Page>)>& completePageFn) 47 + explicit ChapterHtmlSlimParser(const char* filepath, GfxRenderer& renderer, const int fontId, 48 + const float lineCompression, const int marginTop, const int marginRight, 49 + const int marginBottom, const int marginLeft, 50 + const std::function<void(std::unique_ptr<Page>)>& completePageFn) 51 51 : filepath(filepath), 52 52 renderer(renderer), 53 53 fontId(fontId), ··· 57 57 marginBottom(marginBottom), 58 58 marginLeft(marginLeft), 59 59 completePageFn(completePageFn) {} 60 - ~EpubHtmlParserSlim() = default; 60 + ~ChapterHtmlSlimParser() = default; 61 61 bool parseAndBuildPages(); 62 62 };
+13
lib/Epub/Epub/EpubTocEntry.h
··· 1 + #pragma once 2 + 3 + #include <string> 4 + 5 + class EpubTocEntry { 6 + public: 7 + std::string title; 8 + std::string href; 9 + std::string anchor; 10 + int level; 11 + EpubTocEntry(std::string title, std::string href, std::string anchor, const int level) 12 + : title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {} 13 + };
+4 -4
lib/Epub/Epub/Section.cpp
··· 5 5 6 6 #include <fstream> 7 7 8 - #include "EpubHtmlParserSlim.h" 9 8 #include "FsHelpers.h" 10 9 #include "Page.h" 10 + #include "parsers/ChapterHtmlSlimParser.h" 11 11 12 12 constexpr uint8_t SECTION_FILE_VERSION = 4; 13 13 ··· 127 127 128 128 const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath; 129 129 130 - EpubHtmlParserSlim visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight, 131 - marginBottom, marginLeft, 132 - [this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); }); 130 + ChapterHtmlSlimParser visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight, 131 + marginBottom, marginLeft, 132 + [this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); }); 133 133 success = visitor.parseAndBuildPages(); 134 134 135 135 SD.remove(tmpHtmlPath.c_str());
+96
lib/Epub/Epub/parsers/ContainerParser.cpp
··· 1 + #include "ContainerParser.h" 2 + 3 + #include <HardwareSerial.h> 4 + 5 + bool ContainerParser::setup() { 6 + parser = XML_ParserCreate(nullptr); 7 + if (!parser) { 8 + Serial.printf("[%lu] [CTR] Couldn't allocate memory for parser\n", millis()); 9 + return false; 10 + } 11 + 12 + XML_SetUserData(parser, this); 13 + XML_SetElementHandler(parser, startElement, endElement); 14 + return true; 15 + } 16 + 17 + bool ContainerParser::teardown() { 18 + if (parser) { 19 + XML_ParserFree(parser); 20 + parser = nullptr; 21 + } 22 + return true; 23 + } 24 + 25 + size_t ContainerParser::write(const uint8_t data) { return write(&data, 1); } 26 + 27 + size_t ContainerParser::write(const uint8_t* buffer, const size_t size) { 28 + if (!parser) return 0; 29 + 30 + const uint8_t* currentBufferPos = buffer; 31 + auto remainingInBuffer = size; 32 + 33 + while (remainingInBuffer > 0) { 34 + void* const buf = XML_GetBuffer(parser, 1024); 35 + if (!buf) { 36 + Serial.printf("[%lu] [CTR] Couldn't allocate buffer\n", millis()); 37 + return 0; 38 + } 39 + 40 + const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024; 41 + memcpy(buf, currentBufferPos, toRead); 42 + 43 + if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) { 44 + Serial.printf("[%lu] [CTR] Parse error: %s\n", millis(), XML_ErrorString(XML_GetErrorCode(parser))); 45 + return 0; 46 + } 47 + 48 + currentBufferPos += toRead; 49 + remainingInBuffer -= toRead; 50 + remainingSize -= toRead; 51 + } 52 + return size; 53 + } 54 + 55 + void XMLCALL ContainerParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { 56 + auto* self = static_cast<ContainerParser*>(userData); 57 + 58 + // Simple state tracking to ensure we are looking at the valid schema structure 59 + if (self->state == START && strcmp(name, "container") == 0) { 60 + self->state = IN_CONTAINER; 61 + return; 62 + } 63 + 64 + if (self->state == IN_CONTAINER && strcmp(name, "rootfiles") == 0) { 65 + self->state = IN_ROOTFILES; 66 + return; 67 + } 68 + 69 + if (self->state == IN_ROOTFILES && strcmp(name, "rootfile") == 0) { 70 + const char* mediaType = nullptr; 71 + const char* path = nullptr; 72 + 73 + for (int i = 0; atts[i]; i += 2) { 74 + if (strcmp(atts[i], "media-type") == 0) { 75 + mediaType = atts[i + 1]; 76 + } else if (strcmp(atts[i], "full-path") == 0) { 77 + path = atts[i + 1]; 78 + } 79 + } 80 + 81 + // Check if this is the standard OEBPS package 82 + if (mediaType && path && strcmp(mediaType, "application/oebps-package+xml") == 0) { 83 + self->fullPath = path; 84 + } 85 + } 86 + } 87 + 88 + void XMLCALL ContainerParser::endElement(void* userData, const XML_Char* name) { 89 + auto* self = static_cast<ContainerParser*>(userData); 90 + 91 + if (self->state == IN_ROOTFILES && strcmp(name, "rootfiles") == 0) { 92 + self->state = IN_CONTAINER; 93 + } else if (self->state == IN_CONTAINER && strcmp(name, "container") == 0) { 94 + self->state = START; 95 + } 96 + }
+32
lib/Epub/Epub/parsers/ContainerParser.h
··· 1 + #pragma once 2 + #include <Print.h> 3 + 4 + #include <string> 5 + 6 + #include "expat.h" 7 + 8 + class ContainerParser final : public Print { 9 + enum ParserState { 10 + START, 11 + IN_CONTAINER, 12 + IN_ROOTFILES, 13 + }; 14 + 15 + size_t remainingSize; 16 + XML_Parser parser = nullptr; 17 + ParserState state = START; 18 + 19 + static void startElement(void* userData, const XML_Char* name, const XML_Char** atts); 20 + static void endElement(void* userData, const XML_Char* name); 21 + 22 + public: 23 + std::string fullPath; 24 + 25 + explicit ContainerParser(const size_t xmlSize) : remainingSize(xmlSize) {} 26 + 27 + bool setup(); 28 + bool teardown(); 29 + 30 + size_t write(uint8_t) override; 31 + size_t write(const uint8_t* buffer, size_t size) override; 32 + };
+161
lib/Epub/Epub/parsers/ContentOpfParser.cpp
··· 1 + #include "ContentOpfParser.h" 2 + 3 + #include <HardwareSerial.h> 4 + #include <ZipFile.h> 5 + 6 + bool ContentOpfParser::setup() { 7 + parser = XML_ParserCreate(nullptr); 8 + if (!parser) { 9 + Serial.printf("[%lu] [COF] Couldn't allocate memory for parser\n", millis()); 10 + return false; 11 + } 12 + 13 + XML_SetUserData(parser, this); 14 + XML_SetElementHandler(parser, startElement, endElement); 15 + XML_SetCharacterDataHandler(parser, characterData); 16 + return true; 17 + } 18 + 19 + bool ContentOpfParser::teardown() { 20 + if (parser) { 21 + XML_ParserFree(parser); 22 + parser = nullptr; 23 + } 24 + return true; 25 + } 26 + 27 + size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); } 28 + 29 + size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) { 30 + if (!parser) return 0; 31 + 32 + const uint8_t* currentBufferPos = buffer; 33 + auto remainingInBuffer = size; 34 + 35 + while (remainingInBuffer > 0) { 36 + void* const buf = XML_GetBuffer(parser, 1024); 37 + 38 + if (!buf) { 39 + Serial.printf("[%lu] [COF] Couldn't allocate memory for buffer\n", millis()); 40 + XML_ParserFree(parser); 41 + parser = nullptr; 42 + return 0; 43 + } 44 + 45 + const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024; 46 + memcpy(buf, currentBufferPos, toRead); 47 + 48 + if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) { 49 + Serial.printf("[%lu] [COF] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser), 50 + XML_ErrorString(XML_GetErrorCode(parser))); 51 + XML_ParserFree(parser); 52 + parser = nullptr; 53 + return 0; 54 + } 55 + 56 + currentBufferPos += toRead; 57 + remainingInBuffer -= toRead; 58 + remainingSize -= toRead; 59 + } 60 + 61 + return size; 62 + } 63 + 64 + void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { 65 + auto* self = static_cast<ContentOpfParser*>(userData); 66 + (void)atts; 67 + 68 + if (self->state == START && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) { 69 + self->state = IN_PACKAGE; 70 + return; 71 + } 72 + 73 + if (self->state == IN_PACKAGE && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) { 74 + self->state = IN_METADATA; 75 + return; 76 + } 77 + 78 + if (self->state == IN_METADATA && strcmp(name, "dc:title") == 0) { 79 + self->state = IN_BOOK_TITLE; 80 + return; 81 + } 82 + 83 + if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) { 84 + self->state = IN_MANIFEST; 85 + return; 86 + } 87 + 88 + if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) { 89 + self->state = IN_SPINE; 90 + return; 91 + } 92 + 93 + // TODO: Support book cover 94 + // if (self->state == IN_METADATA && (strcmp(name, "meta") == 0 || strcmp(name, "opf:meta") == 0)) { 95 + // } 96 + 97 + if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) { 98 + std::string itemId; 99 + std::string href; 100 + 101 + for (int i = 0; atts[i]; i += 2) { 102 + if (strcmp(atts[i], "id") == 0) { 103 + itemId = atts[i + 1]; 104 + } else if (strcmp(atts[i], "href") == 0) { 105 + href = self->baseContentPath + atts[i + 1]; 106 + } 107 + } 108 + 109 + self->items[itemId] = href; 110 + return; 111 + } 112 + 113 + if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) { 114 + for (int i = 0; atts[i]; i += 2) { 115 + if (strcmp(atts[i], "idref") == 0) { 116 + self->spineRefs.emplace_back(atts[i + 1]); 117 + break; 118 + } 119 + } 120 + return; 121 + } 122 + } 123 + 124 + void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, const int len) { 125 + auto* self = static_cast<ContentOpfParser*>(userData); 126 + 127 + if (self->state == IN_BOOK_TITLE) { 128 + self->title.append(s, len); 129 + return; 130 + } 131 + } 132 + 133 + void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) { 134 + auto* self = static_cast<ContentOpfParser*>(userData); 135 + (void)name; 136 + 137 + if (self->state == IN_SPINE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) { 138 + self->state = IN_PACKAGE; 139 + return; 140 + } 141 + 142 + if (self->state == IN_MANIFEST && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) { 143 + self->state = IN_PACKAGE; 144 + return; 145 + } 146 + 147 + if (self->state == IN_BOOK_TITLE && strcmp(name, "dc:title") == 0) { 148 + self->state = IN_METADATA; 149 + return; 150 + } 151 + 152 + if (self->state == IN_METADATA && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) { 153 + self->state = IN_PACKAGE; 154 + return; 155 + } 156 + 157 + if (self->state == IN_PACKAGE && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) { 158 + self->state = START; 159 + return; 160 + } 161 + }
+42
lib/Epub/Epub/parsers/ContentOpfParser.h
··· 1 + #pragma once 2 + #include <Print.h> 3 + 4 + #include <map> 5 + 6 + #include "Epub.h" 7 + #include "expat.h" 8 + 9 + class ContentOpfParser final : public Print { 10 + enum ParserState { 11 + START, 12 + IN_PACKAGE, 13 + IN_METADATA, 14 + IN_BOOK_TITLE, 15 + IN_MANIFEST, 16 + IN_SPINE, 17 + }; 18 + 19 + const std::string& baseContentPath; 20 + size_t remainingSize; 21 + XML_Parser parser = nullptr; 22 + ParserState state = START; 23 + 24 + static void startElement(void* userData, const XML_Char* name, const XML_Char** atts); 25 + static void characterData(void* userData, const XML_Char* s, int len); 26 + static void endElement(void* userData, const XML_Char* name); 27 + 28 + public: 29 + std::string title; 30 + std::string tocNcxPath; 31 + std::map<std::string, std::string> items; 32 + std::vector<std::string> spineRefs; 33 + 34 + explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize) 35 + : baseContentPath(baseContentPath), remainingSize(xmlSize) {} 36 + 37 + bool setup(); 38 + bool teardown(); 39 + 40 + size_t write(uint8_t) override; 41 + size_t write(const uint8_t* buffer, size_t size) override; 42 + };
+165
lib/Epub/Epub/parsers/TocNcxParser.cpp
··· 1 + #include "TocNcxParser.h" 2 + 3 + #include <HardwareSerial.h> 4 + 5 + bool TocNcxParser::setup() { 6 + parser = XML_ParserCreate(nullptr); 7 + if (!parser) { 8 + Serial.printf("[%lu] [TOC] Couldn't allocate memory for parser\n", millis()); 9 + return false; 10 + } 11 + 12 + XML_SetUserData(parser, this); 13 + XML_SetElementHandler(parser, startElement, endElement); 14 + XML_SetCharacterDataHandler(parser, characterData); 15 + return true; 16 + } 17 + 18 + bool TocNcxParser::teardown() { 19 + if (parser) { 20 + XML_ParserFree(parser); 21 + parser = nullptr; 22 + } 23 + return true; 24 + } 25 + 26 + size_t TocNcxParser::write(const uint8_t data) { return write(&data, 1); } 27 + 28 + size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) { 29 + if (!parser) return 0; 30 + 31 + const uint8_t* currentBufferPos = buffer; 32 + auto remainingInBuffer = size; 33 + 34 + while (remainingInBuffer > 0) { 35 + void* const buf = XML_GetBuffer(parser, 1024); 36 + if (!buf) { 37 + Serial.printf("[%lu] [TOC] Couldn't allocate memory for buffer\n", millis()); 38 + return 0; 39 + } 40 + 41 + const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024; 42 + memcpy(buf, currentBufferPos, toRead); 43 + 44 + if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) { 45 + Serial.printf("[%lu] [TOC] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser), 46 + XML_ErrorString(XML_GetErrorCode(parser))); 47 + return 0; 48 + } 49 + 50 + currentBufferPos += toRead; 51 + remainingInBuffer -= toRead; 52 + remainingSize -= toRead; 53 + } 54 + return size; 55 + } 56 + 57 + void XMLCALL TocNcxParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { 58 + // NOTE: We rely on navPoint label and content coming before any nested navPoints, this will be fine: 59 + // <navPoint> 60 + // <navLabel><text>Chapter 1</text></navLabel> 61 + // <content src="ch1.html"/> 62 + // <navPoint> ...nested... </navPoint> 63 + // </navPoint> 64 + // 65 + // This will NOT: 66 + // <navPoint> 67 + // <navPoint> ...nested... </navPoint> 68 + // <navLabel><text>Chapter 1</text></navLabel> 69 + // <content src="ch1.html"/> 70 + // </navPoint> 71 + 72 + auto* self = static_cast<TocNcxParser*>(userData); 73 + 74 + if (self->state == START && strcmp(name, "ncx") == 0) { 75 + self->state = IN_NCX; 76 + return; 77 + } 78 + 79 + if (self->state == IN_NCX && strcmp(name, "navMap") == 0) { 80 + self->state = IN_NAV_MAP; 81 + return; 82 + } 83 + 84 + // Handles both top-level and nested navPoints 85 + if ((self->state == IN_NAV_MAP || self->state == IN_NAV_POINT) && strcmp(name, "navPoint") == 0) { 86 + self->state = IN_NAV_POINT; 87 + self->currentDepth++; 88 + 89 + self->currentLabel.clear(); 90 + self->currentSrc.clear(); 91 + return; 92 + } 93 + 94 + if (self->state == IN_NAV_POINT && strcmp(name, "navLabel") == 0) { 95 + self->state = IN_NAV_LABEL; 96 + return; 97 + } 98 + 99 + if (self->state == IN_NAV_LABEL && strcmp(name, "text") == 0) { 100 + self->state = IN_NAV_LABEL_TEXT; 101 + return; 102 + } 103 + 104 + if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) { 105 + for (int i = 0; atts[i]; i += 2) { 106 + if (strcmp(atts[i], "src") == 0) { 107 + self->currentSrc = atts[i + 1]; 108 + break; 109 + } 110 + } 111 + return; 112 + } 113 + } 114 + 115 + void XMLCALL TocNcxParser::characterData(void* userData, const XML_Char* s, const int len) { 116 + auto* self = static_cast<TocNcxParser*>(userData); 117 + if (self->state == IN_NAV_LABEL_TEXT) { 118 + self->currentLabel.append(s, len); 119 + } 120 + } 121 + 122 + void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) { 123 + auto* self = static_cast<TocNcxParser*>(userData); 124 + 125 + if (self->state == IN_NAV_LABEL_TEXT && strcmp(name, "text") == 0) { 126 + self->state = IN_NAV_LABEL; 127 + return; 128 + } 129 + 130 + if (self->state == IN_NAV_LABEL && strcmp(name, "navLabel") == 0) { 131 + self->state = IN_NAV_POINT; 132 + return; 133 + } 134 + 135 + if (self->state == IN_NAV_POINT && strcmp(name, "navPoint") == 0) { 136 + self->currentDepth--; 137 + if (self->currentDepth == 0) { 138 + self->state = IN_NAV_MAP; 139 + } 140 + return; 141 + } 142 + 143 + if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) { 144 + // At this point (end of content tag), we likely have both Label (from previous tags) and Src. 145 + // This is the safest place to push the data, assuming <navLabel> always comes before <content>. 146 + // NCX spec says navLabel comes before content. 147 + if (!self->currentLabel.empty() && !self->currentSrc.empty()) { 148 + std::string href = self->baseContentPath + self->currentSrc; 149 + std::string anchor; 150 + 151 + const size_t pos = href.find('#'); 152 + if (pos != std::string::npos) { 153 + anchor = href.substr(pos + 1); 154 + href = href.substr(0, pos); 155 + } 156 + 157 + // Push to vector 158 + self->toc.emplace_back(self->currentLabel, href, anchor, self->currentDepth); 159 + 160 + // Clear them so we don't re-add them if there are weird XML structures 161 + self->currentLabel.clear(); 162 + self->currentSrc.clear(); 163 + } 164 + } 165 + }
+37
lib/Epub/Epub/parsers/TocNcxParser.h
··· 1 + #pragma once 2 + #include <Print.h> 3 + 4 + #include <string> 5 + #include <vector> 6 + 7 + #include "Epub/EpubTocEntry.h" 8 + #include "expat.h" 9 + 10 + class TocNcxParser final : public Print { 11 + enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT }; 12 + 13 + const std::string& baseContentPath; 14 + size_t remainingSize; 15 + XML_Parser parser = nullptr; 16 + ParserState state = START; 17 + 18 + std::string currentLabel; 19 + std::string currentSrc; 20 + size_t currentDepth = 0; 21 + 22 + static void startElement(void* userData, const XML_Char* name, const XML_Char** atts); 23 + static void characterData(void* userData, const XML_Char* s, int len); 24 + static void endElement(void* userData, const XML_Char* name); 25 + 26 + public: 27 + std::vector<EpubTocEntry> toc; 28 + 29 + explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize) 30 + : baseContentPath(baseContentPath), remainingSize(xmlSize) {} 31 + 32 + bool setup(); 33 + bool teardown(); 34 + 35 + size_t write(uint8_t) override; 36 + size_t write(const uint8_t* buffer, size_t size) override; 37 + };
+1 -3
lib/GfxRenderer/GfxRenderer.cpp
··· 162 162 return fontMap.at(fontId).getData(REGULAR)->advanceY; 163 163 } 164 164 165 - uint8_t *GfxRenderer::getFrameBuffer() const { 166 - return einkDisplay.getFrameBuffer(); 167 - } 165 + uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } 168 166 169 167 void GfxRenderer::swapBuffers() const { einkDisplay.swapBuffers(); } 170 168
+19 -2
lib/ZipFile/ZipFile.cpp
··· 40 40 // find the file 41 41 mz_uint32 fileIndex = 0; 42 42 if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) { 43 - Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis, filename); 43 + Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis(), filename); 44 44 mz_zip_reader_end(&zipArchive); 45 45 return false; 46 46 } ··· 80 80 const uint16_t filenameLength = pLocalHeader[26] + (pLocalHeader[27] << 8); 81 81 const uint16_t extraOffset = pLocalHeader[28] + (pLocalHeader[29] << 8); 82 82 return fileOffset + localHeaderSize + filenameLength + extraOffset; 83 + } 84 + 85 + bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) const { 86 + mz_zip_archive_file_stat fileStat; 87 + if (!loadFileStat(filename, &fileStat)) { 88 + return false; 89 + } 90 + 91 + *size = static_cast<size_t>(fileStat.m_uncomp_size); 92 + return true; 83 93 } 84 94 85 95 uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) const { ··· 268 278 // Write output chunk 269 279 if (outBytes > 0) { 270 280 processedOutputBytes += outBytes; 271 - out.write(outputBuffer + outputCursor, outBytes); 281 + if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) { 282 + Serial.printf("[%lu] [ZIP] Failed to write all output bytes to stream\n", millis()); 283 + fclose(file); 284 + free(outputBuffer); 285 + free(fileReadBuffer); 286 + free(inflator); 287 + return false; 288 + } 272 289 // Update output position in buffer (with wraparound) 273 290 outputCursor = (outputCursor + outBytes) & (TINFL_LZ_DICT_SIZE - 1); 274 291 }
+1
lib/ZipFile/ZipFile.h
··· 14 14 public: 15 15 explicit ZipFile(std::string filePath) : filePath(std::move(filePath)) {} 16 16 ~ZipFile() = default; 17 + bool getInflatedFileSize(const char* filename, size_t* size) const; 17 18 uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false) const; 18 19 bool readFileToStream(const char* filename, Print& out, size_t chunkSize) const; 19 20 };
-1
platformio.ini
··· 29 29 30 30 ; Libraries 31 31 lib_deps = 32 - https://github.com/leethomason/tinyxml2.git#11.0.0 33 32 BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor 34 33 InputManager=symlink://open-x4-sdk/libs/hardware/InputManager 35 34 EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
+1 -1
src/screens/EpubReaderScreen.cpp
··· 163 163 const int w = textWidth + margin * 2; 164 164 const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2; 165 165 renderer.grayscaleRevert(); 166 - uint8_t *fb1 = renderer.getFrameBuffer(); 166 + uint8_t* fb1 = renderer.getFrameBuffer(); 167 167 renderer.swapBuffers(); 168 168 memcpy(fb1, renderer.getFrameBuffer(), EInkDisplay::BUFFER_SIZE); 169 169 renderer.fillRect(x, y, w, h, 0);