diff --git a/README.md b/README.md index b9093ea59..c27ff1e2e 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,24 @@ back to the other partition using the "Swap boot partition" button here https:// To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug. +### Command line (specific firmware version) + +1. Install [`esptool`](https://github.com/espressif/esptool) : +```bash +pip install esptool +``` +2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases) +3. Connect your Xteink X4 to your computer via USB-C. +4. Note the device location. On Linux, run `dmesg` after connecting. On MacOS, run : +```bash +log stream --predicate 'subsystem == "com.apple.iokit"' --info +``` +5. Flash the firmware : +```bash +esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 921600 write_flash 0x10000 /path/to/firmware.bin +``` +Change `/dev/ttyACM0` to the device for your system. + ### Manual See [Development](#development) below. diff --git a/bin/clang-format-fix.ps1 b/bin/clang-format-fix.ps1 index 9eae1715f..f90a81b73 100644 --- a/bin/clang-format-fix.ps1 +++ b/bin/clang-format-fix.ps1 @@ -97,6 +97,7 @@ $exclude = @( 'lib\Epub\Epub\hyphenation\generated' 'lib\uzlib' '.pio' + '.venv' ) function Test-Excluded($fullPath) { diff --git a/lib/EpdFont/EpdFont.cpp b/lib/EpdFont/EpdFont.cpp index c80a8573a..fbcc32990 100644 --- a/lib/EpdFont/EpdFont.cpp +++ b/lib/EpdFont/EpdFont.cpp @@ -15,11 +15,11 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star return; } - int32_t cursorXFP = fp4::fromPixel(startX); // 12.4 fixed-point accumulator int lastBaseX = startX; - int lastBaseAdvanceFP = 0; // 12.4 fixed-point + int lastBaseLeft = 0; + int lastBaseWidth = 0; int lastBaseTop = 0; - constexpr int MIN_COMBINING_GAP_PX = 1; + int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap uint32_t cp; uint32_t prevCp = 0; while ((cp = utf8NextCodepoint(reinterpret_cast(&string)))) { @@ -31,24 +31,29 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star const EpdGlyph* glyph = getGlyph(cp); if (!glyph) { - prevCp = 0; + // Keep cursor movement stable when a base glyph is missing, but don't attach subsequent + // combining marks to stale base metrics. + if (!isCombining) { + lastBaseX += fp4::toPixel(prevAdvanceFP); // flush pending advance before resetting + prevCp = 0; + prevAdvanceFP = 0; + lastBaseLeft = 0; + lastBaseWidth = 0; + lastBaseTop = 0; + } continue; } - int raiseBy = 0; - if (isCombining) { - const int currentGap = glyph->top - glyph->height - lastBaseTop; - if (currentGap < MIN_COMBINING_GAP_PX) { - raiseBy = MIN_COMBINING_GAP_PX - currentGap; - } - } + const int raiseBy = isCombining ? combiningMark::raiseAboveBase(glyph->top, glyph->height, lastBaseTop) : 0; if (!isCombining && prevCp != 0) { - cursorXFP += getKerning(prevCp, cp); // 4.4 fixed-point kern + const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern + lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP); } - const int cursorXPixels = fp4::toPixel(cursorXFP); // snap 12.4 fixed-point to nearest pixel - const int glyphBaseX = isCombining ? (lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2)) : cursorXPixels; + const int glyphBaseX = + isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width) + : lastBaseX; const int glyphBaseY = startY - raiseBy; *minX = std::min(*minX, glyphBaseX + glyph->left); @@ -57,10 +62,10 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star *maxY = std::max(*maxY, glyphBaseY + glyph->top); if (!isCombining) { - lastBaseX = cursorXPixels; - lastBaseAdvanceFP = glyph->advanceX; // 12.4 fixed-point + lastBaseLeft = glyph->left; + lastBaseWidth = glyph->width; lastBaseTop = glyph->top; - cursorXFP += glyph->advanceX; // 12.4 fixed-point advance + prevAdvanceFP = glyph->advanceX; // 12.4 fixed-point prevCp = cp; } } diff --git a/lib/EpdFont/EpdFontData.h b/lib/EpdFont/EpdFontData.h index 9f3b691e1..380c5733d 100644 --- a/lib/EpdFont/EpdFontData.h +++ b/lib/EpdFont/EpdFontData.h @@ -7,10 +7,12 @@ /// Font metrics use "fixed-point 4" (4 fractional bits, i.e. 1/16-pixel /// resolution). Both the 12.4 glyph advances (uint16_t) and the 4.4 kern /// values (int8_t) share the same 4 fractional bits, so they can be freely -/// added into a single int32_t accumulator during text layout. The -/// accumulator is snapped to the nearest whole pixel only at render time, -/// which avoids the per-character rounding errors that plagued integer-only -/// layout. +/// added before snapping to whole pixels. +/// +/// Rendering and measurement use "differential rounding": each glyph step +/// (previous advance + current kern) is combined in fixed-point and snapped +/// to a pixel as one unit. This guarantees identical character pairs always +/// produce the same pixel spacing, regardless of position on the line. /// /// The helpers below eliminate the raw bit-shifts that would otherwise be /// scattered across every layout / measurement call site. @@ -28,6 +30,37 @@ constexpr int toPixel(int32_t fp) { return static_cast((fp + HALF) >> FRAC_ constexpr float toFloat(int32_t fp) { return fp / static_cast(1 << FRAC_BITS); } } // namespace fp4 +/// Helpers for positioning Unicode combining marks (U+0300 ff.) over a +/// preceding base glyph without GPOS anchor tables. +namespace combiningMark { + +constexpr int MIN_GAP_PX = 1; + +/// Compute the cursor-X at which to render a combining mark so its bitmap +/// is visually centered over the base glyph's bitmap. +constexpr int centerOver(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) { + return baseCursorPos + baseLeft + baseWidth / 2 - markWidth / 2 - markLeft; +} + +/// Rotated-90CW variant of centerOver. In the rotated coordinate system +/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so +/// every left/width term inverts sign. +constexpr int centerOverRotated90CW(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) { + return baseCursorPos - baseLeft - baseWidth / 2 + markWidth / 2 + markLeft; +} + +/// For combining marks that sit entirely above the baseline, compute how many +/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom +/// edge and the top of the base glyph. Returns 0 for marks that extend to or +/// below the baseline (e.g. cedilla, dot-below, ogonek). +constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) { + if (markTop - markHeight <= 0) return 0; + const int gap = markTop - markHeight - baseTop; + return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0; +} + +} // namespace combiningMark + /// Fixed-point conventions used by EpdGlyph and EpdFontData: /// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel) /// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index cb0b18017..a5befe5b3 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -155,6 +155,7 @@ bool Epub::parseTocNcxFile() const { return false; } readItemContentsToStream(tocNcxItem, tempNcxFile, 1024); + // Explicitly close() file before reopening for reading tempNcxFile.close(); if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) { return false; @@ -165,14 +166,12 @@ bool Epub::parseTocNcxFile() const { if (!ncxParser.setup()) { LOG_ERR("EBP", "Could not setup toc ncx parser"); - tempNcxFile.close(); return false; } const auto ncxBuffer = static_cast(malloc(1024)); if (!ncxBuffer) { LOG_ERR("EBP", "Could not allocate memory for toc ncx parser"); - tempNcxFile.close(); return false; } @@ -184,12 +183,12 @@ bool Epub::parseTocNcxFile() const { if (processedSize != readSize) { LOG_ERR("EBP", "Could not process all toc ncx data"); free(ncxBuffer); - tempNcxFile.close(); return false; } } free(ncxBuffer); + // Explicitly close() file before calling Storage.remove() tempNcxFile.close(); Storage.remove(tmpNcxPath.c_str()); @@ -212,6 +211,7 @@ bool Epub::parseTocNavFile() const { return false; } readItemContentsToStream(tocNavItem, tempNavFile, 1024); + // Explicitly close() file before reopening for reading tempNavFile.close(); if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) { return false; @@ -241,12 +241,12 @@ bool Epub::parseTocNavFile() const { if (processedSize != readSize) { LOG_ERR("EBP", "Could not process all toc nav data"); free(navBuffer); - tempNavFile.close(); return false; } } free(navBuffer); + // Explicitly close() file before calling Storage.remove() tempNavFile.close(); Storage.remove(tmpNavPath.c_str()); @@ -304,10 +304,12 @@ void Epub::parseCssFiles() const { } if (!readItemContentsToStream(cssPath, tempCssFile, 1024)) { LOG_ERR("EBP", "Could not read CSS file: %s", cssPath.c_str()); + // Explicitly close() file before calling Storage.remove() tempCssFile.close(); Storage.remove(tmpCssPath.c_str()); continue; } + // Explicitly close() file before reopening for reading tempCssFile.close(); // Parse the CSS file @@ -317,6 +319,7 @@ void Epub::parseCssFiles() const { continue; } cssParser->loadFromStream(tempCssFile); + // Explicitly close() file before calling Storage.remove() tempCssFile.close(); Storage.remove(tmpCssPath.c_str()); } @@ -547,6 +550,7 @@ bool Epub::generateCoverBmp(bool cropped) const { return false; } readItemContentsToStream(coverImageHref, coverJpg, 1024); + // Explicitly close() file before reopening for reading coverJpg.close(); if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { @@ -555,10 +559,10 @@ bool Epub::generateCoverBmp(bool cropped) const { FsFile coverBmp; if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) { - coverJpg.close(); return false; } const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped); + // Explicitly close() files before calling Storage.remove() coverJpg.close(); coverBmp.close(); Storage.remove(coverJpgTempPath.c_str()); @@ -580,6 +584,7 @@ bool Epub::generateCoverBmp(bool cropped) const { return false; } readItemContentsToStream(coverImageHref, coverPng, 1024); + // Explicitly close() file before reopening for reading coverPng.close(); if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) { @@ -588,10 +593,10 @@ bool Epub::generateCoverBmp(bool cropped) const { FsFile coverBmp; if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) { - coverPng.close(); return false; } const bool success = PngToBmpConverter::pngFileToBmpStream(coverPng, coverBmp, cropped); + // Explicitly close() files before calling Storage.remove() coverPng.close(); coverBmp.close(); Storage.remove(coverPngTempPath.c_str()); @@ -634,6 +639,7 @@ bool Epub::generateThumbBmp(int height) const { return false; } readItemContentsToStream(coverImageHref, coverJpg, 1024); + // Explicitly close() file before reopening for reading coverJpg.close(); if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { @@ -642,7 +648,6 @@ bool Epub::generateThumbBmp(int height) const { FsFile thumbBmp; if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) { - coverJpg.close(); return false; } // Use smaller target size for Continue Reading card (half of screen: 240x400) @@ -651,6 +656,7 @@ bool Epub::generateThumbBmp(int height) const { int THUMB_TARGET_HEIGHT = height; const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT); + // Explicitly close() files before calling Storage.remove() coverJpg.close(); thumbBmp.close(); Storage.remove(coverJpgTempPath.c_str()); @@ -670,6 +676,7 @@ bool Epub::generateThumbBmp(int height) const { return false; } readItemContentsToStream(coverImageHref, coverPng, 1024); + // Explicitly close() file before reopening for reading coverPng.close(); if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) { @@ -678,13 +685,13 @@ bool Epub::generateThumbBmp(int height) const { FsFile thumbBmp; if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) { - coverPng.close(); return false; } int THUMB_TARGET_WIDTH = height * 0.6; int THUMB_TARGET_HEIGHT = height; const bool success = PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT); + // Explicitly close() files before calling Storage.remove() coverPng.close(); thumbBmp.close(); Storage.remove(coverPngTempPath.c_str()); @@ -702,7 +709,6 @@ bool Epub::generateThumbBmp(int height) const { // Write an empty bmp file to avoid generation attempts in the future FsFile thumbBmp; Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp); - thumbBmp.close(); return false; } diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index 3cdee0b0e..8985776b0 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include "FsHelpers.h" @@ -33,6 +33,7 @@ bool BookMetadataCache::beginContentOpfPass() { } bool BookMetadataCache::endContentOpfPass() { + // Explicit close() required: member variable persists beyond function scope spineFile.close(); return true; } @@ -44,13 +45,14 @@ bool BookMetadataCache::beginTocPass() { return false; } if (!Storage.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) { + // Explicit close() required: member variable persists beyond function scope spineFile.close(); return false; } if (spineCount >= LARGE_SPINE_THRESHOLD) { spineHrefIndex.clear(); - spineHrefIndex.reserve(spineCount); + spineHrefIndex.resize(spineCount); spineFile.seek(0); for (int i = 0; i < spineCount; i++) { auto entry = readSpineEntry(spineFile); @@ -58,7 +60,7 @@ bool BookMetadataCache::beginTocPass() { idx.hrefHash = fnvHash64(entry.href); idx.hrefLen = static_cast(entry.href.size()); idx.spineIndex = static_cast(i); - spineHrefIndex.push_back(idx); + spineHrefIndex[i] = idx; } std::sort(spineHrefIndex.begin(), spineHrefIndex.end(), [](const SpineHrefIndexEntry& a, const SpineHrefIndexEntry& b) { @@ -75,6 +77,7 @@ bool BookMetadataCache::beginTocPass() { } bool BookMetadataCache::endTocPass() { + // Explicit close() required: member variables persist beyond function scope tocFile.close(); spineFile.close(); @@ -103,11 +106,13 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta } if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { + // Explicit close() required: member variable persists beyond function scope bookFile.close(); return false; } if (!Storage.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) { + // Explicit close() required: member variables persist beyond function scope bookFile.close(); spineFile.close(); return false; @@ -153,7 +158,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta // Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin // Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m)) - std::vector spineToTocIndex(spineCount, -1); + std::deque spineToTocIndex(spineCount, -1); tocFile.seek(0); for (int j = 0; j < tocCount; j++) { auto tocEntry = readTocEntry(tocFile); @@ -168,6 +173,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta // Pre-open zip file to speed up size calculations if (!zip.open()) { LOG_ERR("BMC", "Could not open EPUB zip for size calculations"); + // Explicit close() required: member variables persist beyond function scope bookFile.close(); spineFile.close(); tocFile.close(); @@ -181,14 +187,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta // This is O(n*log(m)) instead of O(n*m) while avoiding memory exhaustion. // See: https://github.com/crosspoint-reader/crosspoint-reader/issues/134 - std::vector spineSizes; + std::deque spineSizes; bool useBatchSizes = false; if (spineCount >= LARGE_SPINE_THRESHOLD) { LOG_DBG("BMC", "Using batch size lookup for %d spine items", spineCount); - std::vector targets; - targets.reserve(spineCount); + std::deque targets; + targets.resize(spineCount); spineFile.seek(0); for (int i = 0; i < spineCount; i++) { @@ -199,7 +205,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta t.hash = ZipFile::fnvHash64(path.c_str(), path.size()); t.len = static_cast(path.size()); t.index = static_cast(i); - targets.push_back(t); + targets[i] = t; } std::sort(targets.begin(), targets.end(), [](const ZipFile::SizeTarget& a, const ZipFile::SizeTarget& b) { @@ -265,6 +271,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta writeTocEntry(bookFile, tocEntry); } + // Explicit close() required: member variables persist beyond function scope bookFile.close(); spineFile.close(); tocFile.close(); @@ -373,6 +380,7 @@ bool BookMetadataCache::load() { serialization::readPod(bookFile, version); if (version != BOOK_CACHE_VERSION) { LOG_DBG("BMC", "Cache version mismatch: expected %d, got %d", BOOK_CACHE_VERSION, version); + // Explicit close() required: member variable persists beyond function scope bookFile.close(); return false; } diff --git a/lib/Epub/Epub/BookMetadataCache.h b/lib/Epub/Epub/BookMetadataCache.h index 9439b37fe..7f45090a9 100644 --- a/lib/Epub/Epub/BookMetadataCache.h +++ b/lib/Epub/Epub/BookMetadataCache.h @@ -3,8 +3,8 @@ #include #include +#include #include -#include class BookMetadataCache { public: @@ -61,7 +61,7 @@ class BookMetadataCache { uint16_t hrefLen; // length for collision reduction int16_t spineIndex; }; - std::vector spineHrefIndex; + std::deque spineHrefIndex; bool useSpineHrefIndex = false; static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 7c0b3c12b..da74ce648 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -74,6 +74,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con uint8_t version; serialization::readPod(file, version); if (version != SECTION_FILE_VERSION) { + // Explicit close() required: member variable persists beyond function scope file.close(); LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version); clearCache(); @@ -103,6 +104,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight || hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle || imageRendering != fileImageRendering) { + // Explicit close() required: member variable persists beyond function scope file.close(); LOG_ERR("SCT", "Deserialization failed: Parameters do not match"); clearCache(); @@ -111,6 +113,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con } serialization::readPod(file, pageCount); + // Explicit close() required: member variable persists beyond function scope file.close(); LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount); return true; @@ -165,6 +168,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } success = epub->readItemContentsToStream(localPath, tmpHtml, 1024); fileSize = tmpHtml.size(); + // Explicitly close() file before calling Storage.remove() tmpHtml.close(); // If streaming failed, remove the incomplete file immediately @@ -214,6 +218,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c Storage.remove(tmpHtmlPath.c_str()); if (!success) { LOG_ERR("SCT", "Failed to parse XML and build pages"); + // Explicitly close() file before calling Storage.remove() file.close(); Storage.remove(filePath.c_str()); if (cssParser) { @@ -235,6 +240,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c if (hasFailedLutRecords) { LOG_ERR("SCT", "Failed to write LUT due to invalid page positions"); + // Explicitly close() file before calling Storage.remove() file.close(); Storage.remove(filePath.c_str()); return false; @@ -254,6 +260,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c serialization::writePod(file, pageCount); serialization::writePod(file, lutOffset); serialization::writePod(file, anchorMapOffset); + // Explicit close() required: member variable persists beyond function scope file.close(); if (cssParser) { cssParser->clear(); @@ -275,6 +282,7 @@ std::unique_ptr Section::loadPageFromSectionFile() { file.seek(pagePos); auto page = Page::deserialize(file); + // Explicit close() required: member variable persists beyond function scope file.close(); return page; } @@ -290,7 +298,6 @@ std::optional Section::getPageForAnchor(const std::string& anchor) con uint32_t anchorMapOffset; serialization::readPod(f, anchorMapOffset); if (anchorMapOffset == 0 || anchorMapOffset >= fileSize) { - f.close(); return std::nullopt; } @@ -303,11 +310,9 @@ std::optional Section::getPageForAnchor(const std::string& anchor) con serialization::readString(f, key); serialization::readPod(f, page); if (key == anchor) { - f.close(); return page; } } - f.close(); return std::nullopt; } diff --git a/lib/Epub/Epub/blocks/ImageBlock.cpp b/lib/Epub/Epub/blocks/ImageBlock.cpp index 9a958d11c..1b71817a8 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.cpp +++ b/lib/Epub/Epub/blocks/ImageBlock.cpp @@ -37,7 +37,6 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, uint16_t cachedWidth, cachedHeight; if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) { - cacheFile.close(); return false; } @@ -47,7 +46,6 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, if (widthDiff > 1 || heightDiff > 1) { LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth, expectedHeight); - cacheFile.close(); return false; } @@ -62,7 +60,6 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, uint8_t* rowBuffer = (uint8_t*)malloc(bytesPerRow); if (!rowBuffer) { LOG_ERR("IMG", "Failed to allocate row buffer"); - cacheFile.close(); return false; } @@ -73,7 +70,6 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) { LOG_ERR("IMG", "Cache read error at row %d", row); free(rowBuffer); - cacheFile.close(); return false; } @@ -89,7 +85,6 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, } free(rowBuffer); - cacheFile.close(); LOG_DBG("IMG", "Cache render complete"); return true; } diff --git a/lib/Epub/Epub/converters/DirectPixelWriter.h b/lib/Epub/Epub/converters/DirectPixelWriter.h index 3c742871d..bc66c2f78 100644 --- a/lib/Epub/Epub/converters/DirectPixelWriter.h +++ b/lib/Epub/Epub/converters/DirectPixelWriter.h @@ -15,6 +15,7 @@ struct DirectPixelWriter { uint8_t* fb; GfxRenderer::RenderMode mode; + uint16_t displayWidthBytes; // Runtime framebuffer stride (X4: 100, X3: 99) // Orientation is collapsed into a linear transform: // phyX = phyXBase + x * phyXStepX + y * phyXStepY @@ -29,29 +30,33 @@ struct DirectPixelWriter { void init(GfxRenderer& renderer) { fb = renderer.getFrameBuffer(); mode = renderer.getRenderMode(); + displayWidthBytes = renderer.getDisplayWidthBytes(); + + const int phyW = renderer.getDisplayWidth(); + const int phyH = renderer.getDisplayHeight(); switch (renderer.getOrientation()) { case GfxRenderer::Portrait: - // phyX = y, phyY = (DISPLAY_HEIGHT-1) - x + // phyX = y, phyY = (phyH-1) - x phyXBase = 0; - phyYBase = HalDisplay::DISPLAY_HEIGHT - 1; + phyYBase = phyH - 1; phyXStepX = 0; phyYStepX = -1; phyXStepY = 1; phyYStepY = 0; break; case GfxRenderer::LandscapeClockwise: - // phyX = (DISPLAY_WIDTH-1) - x, phyY = (DISPLAY_HEIGHT-1) - y - phyXBase = HalDisplay::DISPLAY_WIDTH - 1; - phyYBase = HalDisplay::DISPLAY_HEIGHT - 1; + // phyX = (phyW-1) - x, phyY = (phyH-1) - y + phyXBase = phyW - 1; + phyYBase = phyH - 1; phyXStepX = -1; phyYStepX = 0; phyXStepY = 0; phyYStepY = -1; break; case GfxRenderer::PortraitInverted: - // phyX = (DISPLAY_WIDTH-1) - y, phyY = x - phyXBase = HalDisplay::DISPLAY_WIDTH - 1; + // phyX = (phyW-1) - y, phyY = x + phyXBase = phyW - 1; phyYBase = 0; phyXStepX = 0; phyYStepX = 1; @@ -115,7 +120,7 @@ struct DirectPixelWriter { const int phyX = rowPhyXBase + logicalX * phyXStepX; const int phyY = rowPhyYBase + logicalX * phyYStepX; - const uint16_t byteIndex = phyY * HalDisplay::DISPLAY_WIDTH_BYTES + (phyX >> 3); + const uint16_t byteIndex = phyY * displayWidthBytes + (phyX >> 3); const uint8_t bitMask = 1 << (7 - (phyX & 7)); if (state) { diff --git a/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp b/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp index 83e6b547f..4cf55ae3b 100644 --- a/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp +++ b/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp @@ -19,38 +19,25 @@ namespace { // The draw callback receives this via pDraw->pUser (set by setUserPointer()). // The file I/O callbacks receive the FsFile* via pFile->fHandle (set by jpegOpen()). struct JpegContext { - GfxRenderer* renderer; - const RenderConfig* config; - int screenWidth; - int screenHeight; + GfxRenderer* renderer{nullptr}; + const RenderConfig* config{nullptr}; + int screenWidth{0}; + int screenHeight{0}; // Source dimensions after JPEGDEC's built-in scaling - int scaledSrcWidth; - int scaledSrcHeight; + int scaledSrcWidth{0}; + int scaledSrcHeight{0}; // Final output dimensions - int dstWidth; - int dstHeight; + int dstWidth{0}; + int dstHeight{0}; // Fine scale in 16.16 fixed-point (ESP32-C3 has no FPU) - int32_t fineScaleFP; // src -> dst mapping - int32_t invScaleFP; // dst -> src mapping + int32_t fineScaleFP{1 << 16}; // src -> dst mapping + int32_t invScaleFP{1 << 16}; // dst -> src mapping PixelCache cache; - bool caching; - - JpegContext() - : renderer(nullptr), - config(nullptr), - screenWidth(0), - screenHeight(0), - scaledSrcWidth(0), - scaledSrcHeight(0), - dstWidth(0), - dstHeight(0), - fineScaleFP(1 << 16), - invScaleFP(1 << 16), - caching(false) {} + bool caching{false}; }; // File I/O callbacks use pFile->fHandle to access the FsFile*, diff --git a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp index c80cb23a6..0cc1616ab 100644 --- a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp +++ b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp @@ -19,37 +19,23 @@ namespace { // The draw callback receives this via pDraw->pUser (set by png.decode()). // The file I/O callbacks receive the FsFile* via pFile->fHandle (set by pngOpen()). struct PngContext { - GfxRenderer* renderer; - const RenderConfig* config; - int screenWidth; - int screenHeight; + GfxRenderer* renderer{nullptr}; + const RenderConfig* config{nullptr}; + int screenWidth{0}; + int screenHeight{0}; // Scaling state - float scale; - int srcWidth; - int srcHeight; - int dstWidth; - int dstHeight; - int lastDstY; // Track last rendered destination Y to avoid duplicates + float scale{1.f}; + int srcWidth{0}; + int srcHeight{0}; + int dstWidth{0}; + int dstHeight{0}; + int lastDstY{-1}; // Track last rendered destination Y to avoid duplicates PixelCache cache; - bool caching; + bool caching{false}; - uint8_t* grayLineBuffer; - - PngContext() - : renderer(nullptr), - config(nullptr), - screenWidth(0), - screenHeight(0), - scale(1.0f), - srcWidth(0), - srcHeight(0), - dstWidth(0), - dstHeight(0), - lastDstY(-1), - caching(false), - grayLineBuffer(nullptr) {} + uint8_t* grayLineBuffer{nullptr}; }; // File I/O callbacks use pFile->fHandle to access the FsFile*, diff --git a/lib/Epub/Epub/css/CssParser.cpp b/lib/Epub/Epub/css/CssParser.cpp index d2e679c36..8f7237e5c 100644 --- a/lib/Epub/Epub/css/CssParser.cpp +++ b/lib/Epub/Epub/css/CssParser.cpp @@ -743,7 +743,6 @@ bool CssParser::saveToCache() const { } LOG_DBG("CSS", "Saved %u rules to cache", ruleCount); - file.close(); return true; } @@ -765,6 +764,7 @@ bool CssParser::loadFromCache() { if (file.read(&version, 1) != 1 || version != CssParser::CSS_CACHE_VERSION) { LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u), removing stale cache for rebuild", version, CssParser::CSS_CACHE_VERSION); + // Explicitly close() file before calling Storage.remove() file.close(); Storage.remove((cachePath + rulesCache).c_str()); return false; @@ -773,14 +773,12 @@ bool CssParser::loadFromCache() { // Read rule count uint16_t ruleCount = 0; if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount)) { - file.close(); return false; } if (ruleCount > MAX_RULES) { LOG_DBG("CSS", "Invalid cache rule count (%u > %zu)", ruleCount, MAX_RULES); rulesBySelector_.clear(); - file.close(); return false; } @@ -799,19 +797,16 @@ bool CssParser::loadFromCache() { uint16_t selectorLen = 0; if (!hasRemainingBytes(sizeof(selectorLen))) { rulesBySelector_.clear(); - file.close(); return false; } if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) { rulesBySelector_.clear(); - file.close(); return false; } if (selectorLen == 0 || selectorLen > MAX_SELECTOR_LENGTH || !hasRemainingBytes(selectorLen)) { LOG_DBG("CSS", "Invalid selector length in cache: %u", selectorLen); rulesBySelector_.clear(); - file.close(); return false; } @@ -819,14 +814,12 @@ bool CssParser::loadFromCache() { selector.resize(selectorLen); if (file.read(&selector[0], selectorLen) != selectorLen) { rulesBySelector_.clear(); - file.close(); return false; } if (!hasRemainingBytes(CSS_FIXED_STYLE_BYTES)) { LOG_DBG("CSS", "Truncated CSS cache while reading style payload"); rulesBySelector_.clear(); - file.close(); return false; } @@ -836,28 +829,24 @@ bool CssParser::loadFromCache() { if (file.read(&enumVal, 1) != 1) { rulesBySelector_.clear(); - file.close(); return false; } style.textAlign = static_cast(enumVal); if (file.read(&enumVal, 1) != 1) { rulesBySelector_.clear(); - file.close(); return false; } style.fontStyle = static_cast(enumVal); if (file.read(&enumVal, 1) != 1) { rulesBySelector_.clear(); - file.close(); return false; } style.fontWeight = static_cast(enumVal); if (file.read(&enumVal, 1) != 1) { rulesBySelector_.clear(); - file.close(); return false; } style.textDecoration = static_cast(enumVal); @@ -880,7 +869,6 @@ bool CssParser::loadFromCache() { !readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight) || !readLength(style.imageHeight) || !readLength(style.imageWidth)) { rulesBySelector_.clear(); - file.close(); return false; } @@ -888,7 +876,6 @@ bool CssParser::loadFromCache() { uint8_t displayVal; if (file.read(&displayVal, 1) != 1) { rulesBySelector_.clear(); - file.close(); return false; } style.display = static_cast(displayVal); @@ -897,7 +884,6 @@ bool CssParser::loadFromCache() { uint16_t definedBits = 0; if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) { rulesBySelector_.clear(); - file.close(); return false; } style.defined.textAlign = (definedBits & 1 << 0) != 0; @@ -921,6 +907,5 @@ bool CssParser::loadFromCache() { } LOG_DBG("CSS", "Loaded %u rules from cache", ruleCount); - file.close(); return true; } diff --git a/lib/Epub/Epub/hyphenation/Hyphenator.cpp b/lib/Epub/Epub/hyphenation/Hyphenator.cpp index 4ae5307ec..ad5de454c 100644 --- a/lib/Epub/Epub/hyphenation/Hyphenator.cpp +++ b/lib/Epub/Epub/hyphenation/Hyphenator.cpp @@ -12,11 +12,24 @@ const LanguageHyphenator* Hyphenator::cachedHyphenator_ = nullptr; namespace { -// Maps a BCP-47 language tag to a language-specific hyphenator. +// Normalize ISO 639-2 (three-letter) codes to ISO 639-1 (two-letter) codes used by the +// hyphenation registry. EPUBs may use either form in their dc:language metadata (e.g. +// "eng" instead of "en"). Both the bibliographic ("fre"/"ger") and terminological +// ("fra"/"deu") ISO 639-2 variants are mapped. +struct Iso639Mapping { + const char* iso639_2; + const char* iso639_1; +}; +static constexpr Iso639Mapping kIso639Mappings[] = { + {"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"}, {"ger", "de"}, + {"rus", "ru"}, {"spa", "es"}, {"ita", "it"}, {"ukr", "uk"}, +}; + +// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator. const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) { if (langTag.empty()) return nullptr; - // Extract primary subtag and normalize to lowercase (e.g., "en-US" -> "en"). + // Extract primary subtag and normalize to lowercase (e.g., "en-US" -> "en", "ENG" -> "en"). std::string primary; primary.reserve(langTag.size()); for (char c : langTag) { @@ -26,6 +39,14 @@ const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) { } if (primary.empty()) return nullptr; + // Normalize ISO 639-2 three-letter codes to two-letter equivalents. + for (const auto& mapping : kIso639Mappings) { + if (primary == mapping.iso639_2) { + primary = mapping.iso639_1; + break; + } + } + return getLanguageHyphenatorForPrimaryTag(primary); } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 4d71614b9..e75421eac 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -726,14 +726,29 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char // Collect footnote link display text (for the number label) // Skip whitespace and brackets to normalize noterefs like "[1]" → "1" if (self->insideFootnoteLink) { - for (int i = 0; i < len; i++) { - unsigned char c = static_cast(s[i]); - if (isWhitespace(c) || c == '[' || c == ']') continue; - if (self->currentFootnoteLinkTextLen < static_cast(sizeof(self->currentFootnoteLinkText)) - 1) { - self->currentFootnoteLinkText[self->currentFootnoteLinkTextLen++] = c; - self->currentFootnoteLinkText[self->currentFootnoteLinkTextLen] = '\0'; - } + int start = 0; + int end = len - 1; + + // Example input and output texts: + // " [ 12 ] " => "12" + // " turn to 256 " => "turn to 256" + + // Ignore leading whitespaces and left square brackets + while (start < len && (isWhitespace(s[start]) || (s[start] == '['))) { + ++start; } + + // Ignore trailing whitespaces and right square brackets + while (end >= start && (isWhitespace(s[end]) || (s[end] == ']'))) { + --end; + } + + // Extract footnote link text + for (int i = start; (self->currentFootnoteLinkTextLen < sizeof(self->currentFootnoteLinkText) - 1) && (i <= end); + ++i) { + self->currentFootnoteLinkText[self->currentFootnoteLinkTextLen++] = s[i]; + } + self->currentFootnoteLinkText[self->currentFootnoteLinkTextLen] = '\0'; } for (int i = 0; i < len; i++) { @@ -1053,7 +1068,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetCharacterDataHandler(parser, nullptr); XML_ParserFree(parser); - file.close(); return false; } @@ -1065,7 +1079,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetCharacterDataHandler(parser, nullptr); XML_ParserFree(parser); - file.close(); return false; } @@ -1078,7 +1091,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetCharacterDataHandler(parser, nullptr); XML_ParserFree(parser); - file.close(); return false; } } while (!done); @@ -1088,7 +1100,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks XML_SetCharacterDataHandler(parser, nullptr); XML_ParserFree(parser); - file.close(); // Process last page if there is still text if (currentTextBlock) { diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.h b/lib/Epub/Epub/parsers/ContentOpfParser.h index 89fb3379b..485b3a857 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.h +++ b/lib/Epub/Epub/parsers/ContentOpfParser.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "Epub.h" @@ -37,7 +38,7 @@ class ContentOpfParser final : public Print { uint16_t idLen; // length for collision reduction uint32_t fileOffset; // offset in .items.bin }; - std::vector itemIndex; + std::deque itemIndex; bool useItemIndex = false; static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400; diff --git a/lib/FsHelpers/FsHelpers.cpp b/lib/FsHelpers/FsHelpers.cpp index 08ca44606..616b094b5 100644 --- a/lib/FsHelpers/FsHelpers.cpp +++ b/lib/FsHelpers/FsHelpers.cpp @@ -78,4 +78,12 @@ bool hasTxtExtension(std::string_view fileName) { return checkFileExtension(file bool hasMarkdownExtension(std::string_view fileName) { return checkFileExtension(fileName, ".md"); } +std::string extractFolderPath(const std::string& filePath) { + const auto lastSlash = filePath.find_last_of('/'); + if (lastSlash == std::string::npos || lastSlash == 0) { + return "/"; + } + return filePath.substr(0, lastSlash); +} + } // namespace FsHelpers diff --git a/lib/FsHelpers/FsHelpers.h b/lib/FsHelpers/FsHelpers.h index a21135127..f8af636a0 100644 --- a/lib/FsHelpers/FsHelpers.h +++ b/lib/FsHelpers/FsHelpers.h @@ -55,4 +55,6 @@ inline bool hasTxtExtension(const String& fileName) { // Check for .md extension (case-insensitive) bool hasMarkdownExtension(std::string_view fileName); +std::string extractFolderPath(const std::string& filePath); + } // namespace FsHelpers diff --git a/lib/GfxRenderer/BitmapHelpers.cpp b/lib/GfxRenderer/BitmapHelpers.cpp index e9dbb64d5..dca059ec9 100644 --- a/lib/GfxRenderer/BitmapHelpers.cpp +++ b/lib/GfxRenderer/BitmapHelpers.cpp @@ -108,7 +108,7 @@ uint8_t quantize1bit(int gray, int x, int y) { return (gray >= adjustedThreshold) ? 1 : 0; } -void createBmpHeader(BmpHeader* bmpHeader, int width, int height) { +void createBmpHeader(BmpHeader* bmpHeader, int width, int height, BmpRowOrder rowOrder) { if (!bmpHeader) return; // Zero out the memory to ensure no garbage data if called on uninitialized stack memory @@ -126,15 +126,15 @@ void createBmpHeader(BmpHeader* bmpHeader, int width, int height) { bmpHeader->infoHeader.biSize = sizeof(bmpHeader->infoHeader); bmpHeader->infoHeader.biWidth = width; - bmpHeader->infoHeader.biHeight = height; + bmpHeader->infoHeader.biHeight = (rowOrder == BmpRowOrder::TopDown) ? -height : height; bmpHeader->infoHeader.biPlanes = 1; bmpHeader->infoHeader.biBitCount = 1; bmpHeader->infoHeader.biCompression = 0; bmpHeader->infoHeader.biSizeImage = imageSize; - bmpHeader->infoHeader.biXPelsPerMeter = 0; - bmpHeader->infoHeader.biYPelsPerMeter = 0; - bmpHeader->infoHeader.biClrUsed = 0; - bmpHeader->infoHeader.biClrImportant = 0; + bmpHeader->infoHeader.biXPelsPerMeter = 2835; // 72 DPI + bmpHeader->infoHeader.biYPelsPerMeter = 2835; // 72 DPI + bmpHeader->infoHeader.biClrUsed = 2; + bmpHeader->infoHeader.biClrImportant = 2; // Color 0 (black) bmpHeader->colors[0].rgbBlue = 0; diff --git a/lib/GfxRenderer/BitmapHelpers.h b/lib/GfxRenderer/BitmapHelpers.h index 8f49124c8..d9d2d8554 100644 --- a/lib/GfxRenderer/BitmapHelpers.h +++ b/lib/GfxRenderer/BitmapHelpers.h @@ -11,8 +11,10 @@ uint8_t quantizeSimple(int gray); uint8_t quantize1bit(int gray, int x, int y); int adjustPixel(int gray); +enum class BmpRowOrder { BottomUp, TopDown }; + // Populates a 1-bit BMP header in the provided memory. -void createBmpHeader(BmpHeader* bmpHeader, int width, int height); +void createBmpHeader(BmpHeader* bmpHeader, int width, int height, BmpRowOrder rowOrder); // 1-bit Atkinson dithering - better quality than noise dithering for thumbnails // Error distribution pattern (same as 2-bit but quantizes to 2 levels): diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 50aa2db14..2d685e5fe 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -131,9 +131,9 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode if (renderMode == GfxRenderer::BW && bmpVal < 3) { // Black (also paints over the grays in BW mode) renderer.drawPixel(screenX, screenY, pixelState); - } else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || (gpio.deviceIsX4() && bmpVal == 2))) { + } else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { // Light gray (also mark the MSB if it's going to be a dark gray too) - // X3 AA tuning: keep only the darker antialias level to avoid washed text + // Dedicated X3 gray LUTs now provide proper 4-level gray on both devices // We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update renderer.drawPixel(screenX, screenY, false); } else if (renderMode == GfxRenderer::GRAYSCALE_LSB && bmpVal == 1) { @@ -215,10 +215,11 @@ void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* te void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black, const EpdFontFamily::Style style) const { const int yPos = y + getFontAscenderSize(fontId); - int32_t xPosFP = fp4::fromPixel(x); // 12.4 fixed-point accumulator int lastBaseX = x; - int lastBaseAdvanceFP = 0; // 12.4 fixed-point + int lastBaseLeft = 0; + int lastBaseWidth = 0; int lastBaseTop = 0; + int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap // cannot draw a NULL / empty string if (text == nullptr || *text == '\0') { @@ -236,41 +237,38 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha return; } const auto& font = fontIt->second; - constexpr int MIN_COMBINING_GAP_PX = 1; uint32_t cp; uint32_t prevCp = 0; while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { if (utf8IsCombiningMark(cp)) { const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); - int raiseBy = 0; - if (combiningGlyph) { - const int currentGap = combiningGlyph->top - combiningGlyph->height - lastBaseTop; - if (currentGap < MIN_COMBINING_GAP_PX) { - raiseBy = MIN_COMBINING_GAP_PX - currentGap; - } - } - - const int combiningX = lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2); - const int combiningY = yPos - raiseBy; - renderCharImpl(*this, renderMode, font, cp, combiningX, combiningY, black, style); + if (!combiningGlyph) continue; + const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop); + const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left, + combiningGlyph->width); + renderCharImpl(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style); continue; } cp = font.applyLigatures(cp, text, style); - const int kernFP = (prevCp != 0) ? font.getKerning(prevCp, cp, style) : 0; // 4.4 fixed-point kern - xPosFP += kernFP; - lastBaseX = fp4::toPixel(xPosFP); // snap 12.4 fixed-point to nearest pixel + // Differential rounding: snap (previous advance + current kern) as one unit so + // identical character pairs always produce the same pixel step regardless of + // where they fall on the line. + if (prevCp != 0) { + const auto kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern + lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel + } + const EpdGlyph* glyph = font.getGlyph(cp, style); - lastBaseAdvanceFP = glyph ? glyph->advanceX : 0; + lastBaseLeft = glyph ? glyph->left : 0; + lastBaseWidth = glyph ? glyph->width : 0; lastBaseTop = glyph ? glyph->top : 0; + prevAdvanceFP = glyph ? glyph->advanceX : 0; // 12.4 fixed-point renderCharImpl(*this, renderMode, font, cp, lastBaseX, yPos, black, style); - if (glyph) { - xPosFP += glyph->advanceX; // 12.4 fixed-point advance - } prevCp = cp; } } @@ -683,7 +681,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con if (renderMode == BW && val < 3) { drawPixel(screenX, screenY); - } else if (renderMode == GRAYSCALE_MSB && (val == 1 || (gpio.deviceIsX4() && val == 2))) { + } else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) { drawPixel(screenX, screenY, false); } else if (renderMode == GRAYSCALE_LSB && val == 1) { drawPixel(screenX, screenY, false); @@ -1007,21 +1005,28 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami uint32_t cp; uint32_t prevCp = 0; - int32_t widthFP = 0; // 12.4 fixed-point accumulator + int widthPx = 0; + int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap const auto& font = fontIt->second; while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { if (utf8IsCombiningMark(cp)) { continue; } cp = font.applyLigatures(cp, text, style); + + // Differential rounding: snap (previous advance + current kern) together, + // matching drawText so measurement and rendering agree exactly. if (prevCp != 0) { - widthFP += font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern + const auto kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern + widthPx += fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel } + const EpdGlyph* glyph = font.getGlyph(cp, style); - if (glyph) widthFP += glyph->advanceX; // 12.4 fixed-point advance + prevAdvanceFP = glyph ? glyph->advanceX : 0; prevCp = cp; } - return fp4::toPixel(widthFP); // snap 12.4 fixed-point to nearest pixel + widthPx += fp4::toPixel(prevAdvanceFP); // final glyph's advance + return widthPx; } int GfxRenderer::getFontAscenderSize(const int fontId) const { @@ -1068,46 +1073,43 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y const auto& font = fontIt->second; - int32_t yPosFP = fp4::fromPixel(y); // 12.4 fixed-point accumulator int lastBaseY = y; - int lastBaseAdvanceFP = 0; // 12.4 fixed-point + int lastBaseLeft = 0; + int lastBaseWidth = 0; int lastBaseTop = 0; - constexpr int MIN_COMBINING_GAP_PX = 1; + int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap uint32_t cp; uint32_t prevCp = 0; while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { if (utf8IsCombiningMark(cp)) { const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); - int raiseBy = 0; - if (combiningGlyph) { - const int currentGap = combiningGlyph->top - combiningGlyph->height - lastBaseTop; - if (currentGap < MIN_COMBINING_GAP_PX) { - raiseBy = MIN_COMBINING_GAP_PX - currentGap; - } - } - + if (!combiningGlyph) continue; + const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop); const int combiningX = x - raiseBy; - const int combiningY = lastBaseY - fp4::toPixel(lastBaseAdvanceFP / 2); + const int combiningY = combiningMark::centerOverRotated90CW(lastBaseY, lastBaseLeft, lastBaseWidth, + combiningGlyph->left, combiningGlyph->width); renderCharImpl(*this, renderMode, font, cp, combiningX, combiningY, black, style); continue; } cp = font.applyLigatures(cp, text, style); + + // Differential rounding: snap (previous advance + current kern) as one unit, + // subtracting for the rotated coordinate direction. if (prevCp != 0) { - yPosFP -= font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern (subtract for rotated) + const auto kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern + lastBaseY -= fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel } - lastBaseY = fp4::toPixel(yPosFP); // snap 12.4 fixed-point to nearest pixel const EpdGlyph* glyph = font.getGlyph(cp, style); - lastBaseAdvanceFP = glyph ? glyph->advanceX : 0; // 12.4 fixed-point + lastBaseLeft = glyph ? glyph->left : 0; + lastBaseWidth = glyph ? glyph->width : 0; lastBaseTop = glyph ? glyph->top : 0; + prevAdvanceFP = glyph ? glyph->advanceX : 0; // 12.4 fixed-point renderCharImpl(*this, renderMode, font, cp, x, lastBaseY, black, style); - if (glyph) { - yPosFP -= glyph->advanceX; // 12.4 fixed-point advance (subtract for rotated) - } prevCp = cp; } } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 01556522d..e683e3122 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -157,4 +157,7 @@ class GfxRenderer { // Low level functions uint8_t* getFrameBuffer() const; size_t getBufferSize() const; + uint16_t getDisplayWidth() const { return panelWidth; } + uint16_t getDisplayHeight() const { return panelHeight; } + uint16_t getDisplayWidthBytes() const { return panelWidthBytes; } }; diff --git a/lib/I18n/I18n.cpp b/lib/I18n/I18n.cpp index 55cf8fc49..545e14360 100644 --- a/lib/I18n/I18n.cpp +++ b/lib/I18n/I18n.cpp @@ -71,7 +71,6 @@ void I18n::loadSettings() { serialization::readPod(file, version); if (version != SETTINGS_VERSION) { Serial.printf("[I18N] Settings version mismatch\n"); - file.close(); return; } @@ -81,8 +80,6 @@ void I18n::loadSettings() { _language = static_cast(lang); Serial.printf("[I18N] Loaded language: %d\n", static_cast(_language)); } - - file.close(); } // Generate character set for a specific language diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index e17f3bad2..cf1d2de74 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Адкрыйце гэты адрас у браўзеры" STR_OR_HTTP_PREFIX: "або http://" STR_SCAN_QR_HINT: "або адсканіруйце QR-код:" STR_CALIBRE_WIRELESS: "Calibre па Wi-Fi" -STR_CALIBRE_WEB_URL: "Вэб-адрас Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Абаронена | + = Захавана" STR_MAC_ADDRESS: "MAC-адрас:" STR_CHECKING_WIFI: "Праверка Wi-Fi..." diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index a585019f1..614821a21 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Obriu aquest URL al navegador" STR_OR_HTTP_PREFIX: "o http://" STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:" STR_CALIBRE_WIRELESS: "Calibre sense fils" -STR_CALIBRE_WEB_URL: "URL web del Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Encriptat | + = Desat" STR_MAC_ADDRESS: "Adreça MAC:" STR_CHECKING_WIFI: "S'està comprovant el WiFi..." diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index 2add9e737..06261a26c 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Otevřete tuto URL ve svém prohlížeči" STR_OR_HTTP_PREFIX: "nebo http://" STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "URL webu Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo" STR_MAC_ADDRESS: "MAC adresa:" STR_CHECKING_WIFI: "Kontrola WiFi..." diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index 3e4704c91..807d98312 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Åbn denne URL i din browser" STR_OR_HTTP_PREFIX: "eller http://" STR_SCAN_QR_HINT: "eller scan QR-kode med din telefon:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Krypteret | + = Gemt" STR_MAC_ADDRESS: "MAC-adresse:" STR_CHECKING_WIFI: "Tjekker WiFi..." diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index 73e44d61a..38b88d77c 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Open deze URL in je browser" STR_OR_HTTP_PREFIX: "of http://" STR_SCAN_QR_HINT: "of scan de QR-code met je telefoon:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Beveiligd | + = Opgeslagen" STR_MAC_ADDRESS: "MAC-adres:" STR_CHECKING_WIFI: "Wifi controleren..." diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index df91c1a1c..af66e074c 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Open this URL in your browser" STR_OR_HTTP_PREFIX: "or http://" STR_SCAN_QR_HINT: "or scan QR code with your phone:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Encrypted | + = Saved" STR_MAC_ADDRESS: "MAC address:" STR_CHECKING_WIFI: "Checking WiFi..." @@ -127,6 +127,7 @@ STR_ALWAYS: "Always" STR_IGNORE: "Ignore" STR_SLEEP: "Sleep" STR_PAGE_TURN: "Page Turn" +STR_FORCE_REFRESH: "Refresh Screen" STR_PORTRAIT: "Portrait" STR_LANDSCAPE_CW: "Landscape CW" STR_INVERTED: "Inverted" @@ -175,6 +176,8 @@ STR_UNNAMED: "Unnamed" STR_NO_SERVER_URL: "No server URL configured" STR_FETCH_FEED_FAILED: "Failed to fetch feed" STR_PARSE_FEED_FAILED: "Failed to parse feed" +STR_NEXT_PAGE: "Next Page »" +STR_PREV_PAGE: "« Previous Page" STR_NETWORK_PREFIX: "Network: " STR_IP_ADDRESS_PREFIX: "IP Address: " STR_ERROR_GENERAL_FAILURE: "Error: General failure" @@ -229,6 +232,7 @@ STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix" STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" STR_OPDS_BROWSER: "OPDS Browser" +STR_SEARCH: "Search" STR_COVER_CUSTOM: "Cover + Custom" STR_MENU_RECENT_BOOKS: "Recent Books" STR_NO_RECENT_BOOKS: "No recent books" @@ -290,3 +294,7 @@ STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Take screenshot" STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: " STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)" +STR_CRASH_TITLE: "System Crash" +STR_CRASH_DESCRIPTION: "A detailed report was saved to crash_report.txt. Please include this file in your bug report." +STR_CRASH_REASON: "Crash reason:" +STR_CRASH_NO_REASON: "(No reason was recorded)" diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index 35e581dd5..e584d7a26 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Avaa tämä osoite selaimessasi" STR_OR_HTTP_PREFIX: "tai http://" STR_SCAN_QR_HINT: "tai skannaa QR-koodi puhelimellasi:" STR_CALIBRE_WIRELESS: "Calibre langaton" -STR_CALIBRE_WEB_URL: "Calibre-verkko-osoite" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Salattu | + = Tallennettu" STR_MAC_ADDRESS: "MAC-osoite:" STR_CHECKING_WIFI: "Tarkistetaan WiFi..." diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index f91e46f42..b908fa84d 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Ouvrez cette URL dans un navigateur" STR_OR_HTTP_PREFIX: "ou http://" STR_SCAN_QR_HINT: "ou scannez le QR code :" STR_CALIBRE_WIRELESS: "Connexion Calibre sans fil" -STR_CALIBRE_WEB_URL: "URL Web Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Sécurisé | + = Sauvegardé" STR_MAC_ADDRESS: "Adresse MAC :" STR_CHECKING_WIFI: "Vérification du WiFi…" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index b53ff4944..881a2b012 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Diese URL im Browser öffnen" STR_OR_HTTP_PREFIX: "oder http://" STR_SCAN_QR_HINT: "oder QR-Code mit dem Handy scannen:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre-Web-URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Verschlüsselt | + = Gespeichert" STR_MAC_ADDRESS: "MAC-Adresse:" STR_CHECKING_WIFI: "WLAN prüfen…" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index e28524ce3..38c1bc12b 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Nyisd meg ezt az URL-t a böngésződben" STR_OR_HTTP_PREFIX: "vagy http://" STR_SCAN_QR_HINT: "vagy olvasd be a QR-kódot a telefonoddal:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Titkosított | + = Mentett" STR_MAC_ADDRESS: "MAC-cím:" STR_CHECKING_WIFI: "WiFi ellenőrzése..." diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index b18bea1f2..f7679ccf6 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Apri questo URL nel tuo browser" STR_OR_HTTP_PREFIX: "o http://" STR_SCAN_QR_HINT: "o scansiona il codice QR con il tuo telefono:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "URL Web Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Criptata | + = Salvata" STR_MAC_ADDRESS: "Indirizzo MAC:" STR_CHECKING_WIFI: "Controllo WiFi in corso..." diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index 4b30e9328..66c07712b 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -44,7 +44,7 @@ STR_OPEN_URL_HINT: "Браузерде осы URL мекенжайын ашың STR_OR_HTTP_PREFIX: "немесе http://" STR_SCAN_QR_HINT: "немесе телефонмен QR кодын сканерлеңіз:" STR_CALIBRE_WIRELESS: "Calibre сымсыз" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Шифрланған | + = Сақталған" STR_MAC_ADDRESS: "MAC мекенжайы:" STR_CHECKING_WIFI: "WiFi тексерілуде..." diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index 91eb7c6ec..67e424abc 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Atidarykite šį adresą naršyklėje" STR_OR_HTTP_PREFIX: "arba http://" STR_SCAN_QR_HINT: "arba nuskaitykite QR kodą:" STR_CALIBRE_WIRELESS: "Calibre belaidis" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Užšifruota | + = Išsaugota" STR_MAC_ADDRESS: "MAC adresas:" STR_CHECKING_WIFI: "Tikrinamas WiFi..." diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index c5d9fed1f..a4020fbb4 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Otwórz ten URL w przeglądarce" STR_OR_HTTP_PREFIX: "albo http://" STR_SCAN_QR_HINT: "albo zeskanuj kod QR telefonem:" STR_CALIBRE_WIRELESS: "Bezprzewodowe połączenie z Calibre" -STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Zaszyfrowane | + = Zapisane" STR_MAC_ADDRESS: "Adres MAC:" STR_CHECKING_WIFI: "Sprawdzanie WiFi..." diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index 962863ad9..989806493 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Abra este URL seu navegador" STR_OR_HTTP_PREFIX: "ou http://" STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:" STR_CALIBRE_WIRELESS: "Calibre sem fio" -STR_CALIBRE_WEB_URL: "URL do Calibre Web" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Criptografada | + = Salva" STR_MAC_ADDRESS: "Endereço MAC:" STR_CHECKING_WIFI: "Verificando Wi‑Fi..." diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 27ec6e4a1..b41caa60f 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Deschideţi acest URL în browserul dvs." STR_OR_HTTP_PREFIX: "sau http://" STR_SCAN_QR_HINT: "sau scanaţi codul QR cu telefonul dvs.:" STR_CALIBRE_WIRELESS: "Calibre Wireless" -STR_CALIBRE_WEB_URL: "Calibre URL" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Criptat | + = Salvat" STR_MAC_ADDRESS: "Adresă MAC:" STR_CHECKING_WIFI: "Verificare WiFi..." diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 82a1c188a..708a0b4ba 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Откройте этот адрес в браузере" STR_OR_HTTP_PREFIX: "или http://" STR_SCAN_QR_HINT: "или отсканируйте QR-код:" STR_CALIBRE_WIRELESS: "Calibre по Wi-Fi" -STR_CALIBRE_WEB_URL: "Web-адрес Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Защищена | + = Сохранена" STR_MAC_ADDRESS: "MAC-адрес:" STR_CHECKING_WIFI: "Проверка Wi-Fi..." @@ -175,8 +175,8 @@ STR_UNNAMED: "Без имени" STR_NO_SERVER_URL: "URL сервера не настроен" STR_FETCH_FEED_FAILED: "Не удалось получить ленту" STR_PARSE_FEED_FAILED: "Не удалось обработать ленту" -STR_NETWORK_PREFIX: "Сеть:" -STR_IP_ADDRESS_PREFIX: "IP-адрес:" +STR_NETWORK_PREFIX: "Сеть: " +STR_IP_ADDRESS_PREFIX: "IP-адрес: " STR_ERROR_GENERAL_FAILURE: "Ошибка: Общая ошибка" STR_ERROR_NETWORK_NOT_FOUND: "Ошибка: Сеть не найдена" STR_ERROR_CONNECTION_TIMEOUT: "Ошибка: Тайм-аут соединения" @@ -257,10 +257,10 @@ STR_GO_HOME_BUTTON: "На главную" STR_SYNC_PROGRESS: "Синхронизировать прогресс" STR_DELETE_CACHE: "Удалить кэш книги" STR_DELETE: "Удалить" -STR_CHAPTER_PREFIX: "Глава:" +STR_CHAPTER_PREFIX: "Глава: " STR_DISPLAY_QR: "Показать страницу в виде QR-кода" -STR_PAGES_SEPARATOR: "стр. |" -STR_BOOK_PREFIX: "Книга:" +STR_PAGES_SEPARATOR: " стр. | " +STR_BOOK_PREFIX: "Книга: " STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL" STR_PERCENT_STEP_HINT: "Влево/Вправо: 1% Вверх/Вниз: 10%" STR_SYNCING_TIME: "Синхронизация времени..." @@ -282,11 +282,11 @@ STR_NO_REMOTE_MSG: "Удалённый прогресс не найден" STR_UPLOAD_PROMPT: "Отправить текущую позицию?" STR_UPLOAD_SUCCESS: "Прогресс отправлен!" STR_SYNC_FAILED_MSG: "Ошибка синхронизации" -STR_SECTION_PREFIX: "Раздел" +STR_SECTION_PREFIX: "Раздел " STR_UPLOAD: "Отправить" STR_BOOK_S_STYLE: "Стиль книги" STR_EMBEDDED_STYLE: "Встроенный стиль" STR_OPDS_SERVER_URL: "URL OPDS сервера" STR_SCREENSHOT_BUTTON: "Сделать снимок экрана" -STR_AUTO_TURN_ENABLED: "Авто-поворот включён: " -STR_AUTO_TURN_PAGES_PER_MIN: "Авто-поворот (Страниц в минуту)" +STR_AUTO_TURN_ENABLED: "Автоперелистывание: " +STR_AUTO_TURN_PAGES_PER_MIN: "Автоперелистывание (стр./мин)" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml new file mode 100644 index 000000000..195a5ac01 --- /dev/null +++ b/lib/I18n/translations/slovenian.yaml @@ -0,0 +1,292 @@ +_language_name: "Slovenščina" +_language_code: "SI" +_order: "21" + +STR_CROSSPOINT: "CrossPoint" +STR_BOOTING: "ZAGON" +STR_SLEEPING: "SPANJE" +STR_ENTERING_SLEEP: "Prehajanje v spanje" +STR_BROWSE_FILES: "Prebrskaj datoteke" +STR_FILE_TRANSFER: "Prenos datotek" +STR_SETTINGS_TITLE: "Nastavitve" +STR_CONTINUE_READING: "Nadaljuj z branjem" +STR_NO_OPEN_BOOK: "Ni odprte knjige" +STR_START_READING: "Začni brati spodaj" +STR_NO_FILES_FOUND: "Ni najdenih datotek" +STR_SELECT_CHAPTER: "Izberi poglavje" +STR_NO_CHAPTERS: "Ni poglavij" +STR_END_OF_BOOK: "Konec knjige" +STR_EMPTY_CHAPTER: "Prazno poglavje" +STR_INDEXING: "Indeksiranje" +STR_MEMORY_ERROR: "Napaka pomnilnika" +STR_PAGE_LOAD_ERROR: "Napaka pri nalaganju strani" +STR_EMPTY_FILE: "Prazna datoteka" +STR_OUT_OF_BOUNDS: "Izven meja" +STR_LOADING: "Nalaganje..." +STR_LOADING_POPUP: "Nalaganje" +STR_WIFI_NETWORKS: "WiFi omrežja" +STR_NO_NETWORKS: "Ni najdenih omrežij" +STR_NETWORKS_FOUND: "Najdenih omrežij: %zu" +STR_SCANNING: "Iskanje..." +STR_CONNECTING: "Povezovanje..." +STR_CONNECTED: "Povezano!" +STR_CONNECTION_FAILED: "Povezava ni uspela" +STR_FORGET_NETWORK: "Pozabi omrežje?" +STR_SAVE_PASSWORD: "Shranim geslo za naslednjič?" +STR_PRESS_OK_SCAN: "Pritisni OK za ponovno iskanje" +STR_JOIN_NETWORK: "Poveži se v omrežje" +STR_CREATE_HOTSPOT: "Ustvari dostopno točko" +STR_JOIN_DESC: "Poveži se v obstoječe WiFi omrežje" +STR_HOTSPOT_DESC: "Ustvari WiFi omrežje, v katerega se lahko povežejo drugi" +STR_STARTING_HOTSPOT: "Zaganjanje dostopne točke..." +STR_HOTSPOT_MODE: "Način dostopne točke" +STR_CONNECT_WIFI_HINT: "Poveži svojo napravo v to WiFi omrežje" +STR_OPEN_URL_HINT: "Odpri ta URL v svojem brskalniku" +STR_OR_HTTP_PREFIX: "ali http://" +STR_SCAN_QR_HINT: "ali skeniraj QR kodo s telefonom:" +STR_CALIBRE_WIRELESS: "Brezžični Calibre" +STR_CALIBRE_WEB_URL: "Calibre Web URL" +STR_NETWORK_LEGEND: "* = Šifrirano | + = Shranjeno" +STR_MAC_ADDRESS: "MAC naslov:" +STR_CHECKING_WIFI: "Preverjanje WiFi-ja..." +STR_ENTER_WIFI_PASSWORD: "Vnesi WiFi geslo" +STR_TO_PREFIX: "v " +STR_CALIBRE_RECEIVING: "Prejemanje: " +STR_CALIBRE_RECEIVED: "Prejeto: " +STR_CALIBRE_INSTRUCTION_1: "1) Namesti vtičnik CrossPoint Reader" +STR_CALIBRE_INSTRUCTION_2: "2) Bodi v istem WiFi omrežju" +STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: \"Pošlji v napravo\"" +STR_CALIBRE_INSTRUCTION_4: "\"Med pošiljanjem pusti ta zaslon odprt\"" +STR_CAT_DISPLAY: "Zaslon" +STR_CAT_READER: "Bralnik" +STR_CAT_CONTROLS: "Kontrole" +STR_CAT_SYSTEM: "Sistem" +STR_SLEEP_SCREEN: "Zaslon za spanje" +STR_SLEEP_COVER_MODE: "Način naslovnice v spanju" +STR_HIDE_BATTERY: "Skrij % baterije" +STR_EXTRA_SPACING: "Dodaten razmik med odstavki" +STR_TEXT_AA: "Glajenje besedila (AA)" +STR_IMAGES: "Slike" +STR_IMAGES_DISPLAY: "Prikaži" +STR_IMAGES_PLACEHOLDER: "Oznaka mesta" +STR_IMAGES_SUPPRESS: "Zatdi" +STR_SHORT_PWR_BTN: "Kratek pritisk na gumb za vklop" +STR_ORIENTATION: "Orientacija branja" +STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov" +STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja" +STR_FONT_FAMILY: "Pisava bralnika" +STR_FONT_SIZE: "Velikost pisave" +STR_LINE_SPACING: "Razmik med vrsticami" +STR_SCREEN_MARGIN: "Robovi zaslona" +STR_PARA_ALIGNMENT: "Poravnava odstavkov" +STR_HYPHENATION: "Deljenje besed" +STR_TIME_TO_SLEEP: "Čas do spanja" +STR_SHOW_HIDDEN_FILES: "Prikaži skrite datoteke" +STR_REFRESH_FREQ: "Pogostost osveževanja" +STR_KOREADER_SYNC: "KOReader sinhronizacija" +STR_CHECK_UPDATES: "Preveri posodobitve" +STR_LANGUAGE: "Jezik" +STR_CLEAR_READING_CACHE: "Počisti predpomnilnik branja" +STR_USERNAME: "Uporabniško ime" +STR_PASSWORD: "Geslo" +STR_SYNC_SERVER_URL: "URL strežnika za sinhronizacijo" +STR_DOCUMENT_MATCHING: "Ujemanje dokumentov" +STR_AUTHENTICATE: "Avtentikacija" +STR_KOREADER_USERNAME: "KOReader uporabnik" +STR_KOREADER_PASSWORD: "KOReader geslo" +STR_FILENAME: "Ime datoteke" +STR_BINARY: "Binarno" +STR_SET_CREDENTIALS_FIRST: "Najprej nastavi podatke za prijavo" +STR_WIFI_CONN_FAILED: "WiFi povezava ni uspela" +STR_AUTHENTICATING: "Preverjanje..." +STR_AUTH_SUCCESS: "Uspešna prijava!" +STR_KOREADER_AUTH: "KOReader avtentikacija" +STR_SYNC_READY: "KOReader sinhronizacija je pripravljena" +STR_AUTH_FAILED: "Prijava ni uspela" +STR_DONE: "Končano" +STR_CLEAR_CACHE_WARNING_1: "To bo izbrisalo vse predpomnjene podatke o knjigah." +STR_CLEAR_CACHE_WARNING_2: "Ves napredek pri branju bo izgubljen!" +STR_CLEAR_CACHE_WARNING_3: "Knjige bo treba ob ponovnem odpiranju" +STR_CLEAR_CACHE_WARNING_4: "ponovno indeksirati." +STR_CLEARING_CACHE: "Čiščenje predpomnilnika..." +STR_CACHE_CLEARED: "Predpomnilnik očiščen" +STR_ITEMS_REMOVED: "elementov odstranjenih" +STR_FAILED_LOWER: "ni uspelo" +STR_CLEAR_CACHE_FAILED: "Čiščenje predpomnilnika ni uspelo" +STR_CHECK_SERIAL_OUTPUT: "Za podrobnosti preveri serijski izhod" +STR_DARK: "Temno" +STR_LIGHT: "Svetlo" +STR_CUSTOM: "Po meri" +STR_COVER: "Naslovnica" +STR_NONE_OPT: "Brez" +STR_FIT: "Prilagodi" +STR_CROP: "Obreži" +STR_NEVER: "Nikoli" +STR_IN_READER: "V bralniku" +STR_ALWAYS: "Vedno" +STR_IGNORE: "Prezri" +STR_SLEEP: "Spanje" +STR_PAGE_TURN: "Obračanje strani" +STR_PORTRAIT: "Pokončno" +STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)" +STR_INVERTED: "Obrnjeno" +STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)" +STR_PREV_NEXT: "Nazaj/Naprej" +STR_NEXT_PREV: "Naprej/Nazaj" +STR_BOOKERLY: "Bookerly" +STR_NOTO_SANS: "Noto Sans" +STR_OPEN_DYSLEXIC: "Open Dyslexic" +STR_SMALL: "Majhno" +STR_MEDIUM: "Srednje" +STR_LARGE: "Veliko" +STR_X_LARGE: "Zelo veliko" +STR_TIGHT: "Tesno" +STR_NORMAL: "Normalno" +STR_WIDE: "Široko" +STR_JUSTIFY: "Obojestransko" +STR_ALIGN_LEFT: "Levo" +STR_CENTER: "Sredinsko" +STR_ALIGN_RIGHT: "Desno" +STR_MIN_1: "1 min" +STR_MIN_5: "5 min" +STR_MIN_10: "10 min" +STR_MIN_15: "15 min" +STR_MIN_30: "30 min" +STR_PAGES_1: "1 stran" +STR_PAGES_5: "5 strani" +STR_PAGES_10: "10 strani" +STR_PAGES_15: "15 strani" +STR_PAGES_30: "30 strani" +STR_UPDATE: "Posodobi" +STR_CHECKING_UPDATE: "Preverjanje posodobitev..." +STR_NEW_UPDATE: "Na voljo je nova posodobitev!" +STR_CURRENT_VERSION: "Trenutna različica: " +STR_NEW_VERSION: "Nova različica: " +STR_UPDATING: "Posodabljanje..." +STR_NO_UPDATE: "Ni novih posodobitev" +STR_UPDATE_FAILED: "Posodobitev ni uspela" +STR_UPDATE_COMPLETE: "Posodobitev končana" +STR_POWER_ON_HINT: "Pridrži gumb za vklop, da napravo znova vklopiš" +STR_NO_ENTRIES: "Ni najdenih vnosov" +STR_DOWNLOADING: "Prenašanje..." +STR_DOWNLOAD_FAILED: "Prenos ni uspel" +STR_ERROR_MSG: "Napaka:" +STR_UNNAMED: "Neimenovano" +STR_NO_SERVER_URL: "URL strežnika ni nastavljen" +STR_FETCH_FEED_FAILED: "Nalaganje vira ni uspelo" +STR_PARSE_FEED_FAILED: "Razčlenjevanje vira ni uspelo" +STR_NETWORK_PREFIX: "Omrežje: " +STR_IP_ADDRESS_PREFIX: "IP naslov: " +STR_ERROR_GENERAL_FAILURE: "Napaka: Splošna napaka" +STR_ERROR_NETWORK_NOT_FOUND: "Napaka: Omrežje ni najdeno" +STR_ERROR_CONNECTION_TIMEOUT: "Napaka: Časovna omejitev povezave" +STR_SD_CARD: "SD kartica" +STR_BACK: "« Nazaj" +STR_EXIT: "« Izhod" +STR_HOME: "« Domov" +STR_SELECT: "Izberi" +STR_SELECTED: "Izbrano" +STR_TOGGLE: "Preklopi" +STR_CONFIRM: "Potrdi" +STR_CANCEL: "Prekliči" +STR_CONNECT: "Poveži" +STR_OPEN: "Odpri" +STR_DOWNLOAD: "Prenesi" +STR_RETRY: "Poskusi znova" +STR_YES: "Da" +STR_NO: "Ne" +STR_SHOW: "Prikaži" +STR_HIDE: "Skrij" +STR_STATE_ON: "VKLOP" +STR_STATE_OFF: "IZKLOP" +STR_NOT_SET: "Ni nastavljeno" +STR_DIR_LEFT: "Levo" +STR_DIR_RIGHT: "Desno" +STR_DIR_UP: "Gor" +STR_DIR_DOWN: "Dol" +STR_OK_BUTTON: "V redu" +STR_SLEEP_COVER_FILTER: "Filter naslovnice v spanju" +STR_FILTER_CONTRAST: "Kontrast" +STR_CUSTOMISE_STATUS_BAR: "Prilagodi vrstico stanja" +STR_CHAPTER_PAGE_COUNT: "Število strani v poglavju" +STR_BOOK_PROGRESS_PERCENTAGE: "Odstotek napredka v knjigi" +STR_PROGRESS_BAR: "Vrstica napredka" +STR_PROGRESS_BAR_THICKNESS: "Debelina vrstice napredka" +STR_PROGRESS_BAR_THIN: "Tanko" +STR_PROGRESS_BAR_MEDIUM: "Srednje" +STR_PROGRESS_BAR_THICK: "Debelo" +STR_BOOK: "Knjiga" +STR_CHAPTER: "Poglavje" +STR_EXAMPLE_CHAPTER: "Poglavje 21" +STR_EXAMPLE_BOOK: "Naslov knjige" +STR_PREVIEW: "Predogled" +STR_TITLE: "Naslov" +STR_BATTERY: "Baterija" +STR_UI_THEME: "Tema uporabniškega vmesnika" +STR_THEME_CLASSIC: "Klasična" +STR_THEME_LYRA: "Lyra" +STR_THEME_LYRA_EXTENDED: "Lyra razširjena" +STR_SUNLIGHT_FADING_FIX: "Popravek bledenja na soncu" +STR_REMAP_FRONT_BUTTONS: "Prenastavi sprednje gumbe" +STR_OPDS_BROWSER: "OPDS brskalnik" +STR_COVER_CUSTOM: "Naslovnica + po meri" +STR_MENU_RECENT_BOOKS: "Zadnje knjige" +STR_NO_RECENT_BOOKS: "Ni zadnjih knjig" +STR_CALIBRE_DESC: "Uporabi brezžični prenos Calibre" +STR_FORGET_AND_REMOVE: "Pozabi omrežje in odstrani shranjeno geslo?" +STR_FORGET_BUTTON: "Pozabi" +STR_CALIBRE_STARTING: "Zaganjanje Calibre..." +STR_CALIBRE_SETUP: "Nastavitev" +STR_CALIBRE_STATUS: "Stanje" +STR_CLEAR_BUTTON: "Počisti" +STR_DEFAULT_VALUE: "Privzeto" +STR_REMAP_PROMPT: "Pritisni sprednji gumb za vsako vlogo" +STR_UNASSIGNED: "Nedodeljeno" +STR_ALREADY_ASSIGNED: "Že dodeljeno" +STR_REMAP_RESET_HINT: "Stranski gumb gor: Ponastavi na privzeto" +STR_REMAP_CANCEL_HINT: "Stranski gumb dol: Prekliči nastavljanje" +STR_HW_BACK_LABEL: "Nazaj (1. gumb)" +STR_HW_CONFIRM_LABEL: "Potrdi (2. gumb)" +STR_HW_LEFT_LABEL: "Levo (3. gumb)" +STR_HW_RIGHT_LABEL: "Desno (4. gumb)" +STR_GO_TO_PERCENT: "Pojdi na %" +STR_GO_HOME_BUTTON: "Pojdi domov" +STR_SYNC_PROGRESS: "Sinhroniziraj napredek" +STR_DELETE_CACHE: "Izbriši predpomnilnik knjige" +STR_DELETE: "Izbriši" +STR_DISPLAY_QR: "Prikaži stran kot QR" +STR_CHAPTER_PREFIX: "Poglavje: " +STR_PAGES_SEPARATOR: " strani | " +STR_BOOK_PREFIX: "Knjiga: " +STR_CALIBRE_URL_HINT: "Za Calibre dodaj /opds svojemu URL-ju" +STR_PERCENT_STEP_HINT: "Levo/desno: 1% Gor/dol: 10%" +STR_SYNCING_TIME: "Sinhronizacija časa..." +STR_CALC_HASH: "Izračunavanje podpisa dokumenta..." +STR_HASH_FAILED: "Izračun podpisa dokumenta ni uspel" +STR_FETCH_PROGRESS: "Pridobivanje napredka iz oblaka..." +STR_UPLOAD_PROGRESS: "Nalaganje napredka..." +STR_NO_CREDENTIALS_MSG: "Podatki za prijavo niso nastavljeni" +STR_KOREADER_SETUP_HINT: "Nastavi KOReader račun v nastavitvah" +STR_PROGRESS_FOUND: "Najden napredek!" +STR_REMOTE_LABEL: "Oddaljeno:" +STR_LOCAL_LABEL: "Lokalno:" +STR_PAGE_OVERALL_FORMAT: "Stran %d, %.2f%% skupno" +STR_PAGE_TOTAL_OVERALL_FORMAT: "Stran %d/%d, %.2f%% skupno" +STR_DEVICE_FROM_FORMAT: " Iz: %s" +STR_APPLY_REMOTE: "Uporabi oddaljen napredek" +STR_UPLOAD_LOCAL: "Naloži lokalni napredek" +STR_NO_REMOTE_MSG: "Oddaljen napredek ni bil najden" +STR_UPLOAD_PROMPT: "Naložim trenutno pozicijo?" +STR_UPLOAD_SUCCESS: "Napredek naložen!" +STR_SYNC_FAILED_MSG: "Sinhronizacija ni uspela" +STR_SECTION_PREFIX: "Razdelek " +STR_UPLOAD: "Naloži" +STR_BOOK_S_STYLE: "Slog knjige" +STR_EMBEDDED_STYLE: "Vgrajen slog" +STR_OPDS_SERVER_URL: "URL OPDS strežnika" +STR_FOOTNOTES: "Opombe" +STR_NO_FOOTNOTES: "Na tej strani ni opomb" +STR_LINK: "[povezava]" +STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona" +STR_AUTO_TURN_ENABLED: "Samodejno obračanje: " +STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index d4b2ad149..be4ab7a7e 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Abra esta dirección en su navegador" STR_OR_HTTP_PREFIX: "o http://" STR_SCAN_QR_HINT: "o escanee el código QR con su móvil:" STR_CALIBRE_WIRELESS: "Calibre inalámbrico" -STR_CALIBRE_WEB_URL: "URL del sitio web de Calibre" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* (Cifrado) | + (Guardado)" STR_MAC_ADDRESS: "MAC Address:" STR_CHECKING_WIFI: "Verificando Wi-Fi..." diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index c5e577e3a..456d2afda 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Öppna denna adress i din browser" STR_OR_HTTP_PREFIX: "eller http://" STR_SCAN_QR_HINT: "eller skanna QR-kod med din telefon:" STR_CALIBRE_WIRELESS: "Calibre Trådlöst" -STR_CALIBRE_WEB_URL: "Calibre webbadress" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Krypterad | + = Sparad" STR_MAC_ADDRESS: "MAC-adress:" STR_CHECKING_WIFI: "Kontrollerar trådlöst nätverk…" @@ -127,6 +127,7 @@ STR_ALWAYS: "Alltid" STR_IGNORE: "Ignorera" STR_SLEEP: "Vila" STR_PAGE_TURN: "Sidvändning" +STR_FORCE_REFRESH: "Uppdatera skärmen" STR_PORTRAIT: "Porträtt" STR_LANDSCAPE_CW: "Landskap medurs" STR_INVERTED: "Inverterad" @@ -175,6 +176,8 @@ STR_UNNAMED: "Ej namngiven" STR_NO_SERVER_URL: "Ingen serveradress konfigurerad" STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde" STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde" +STR_NEXT_PAGE: "Nästa sida »" +STR_PREV_PAGE: "« Föregående sida" STR_NETWORK_PREFIX: "Nätverk:" STR_IP_ADDRESS_PREFIX: "IP-adress;" STR_ERROR_GENERAL_FAILURE: "Fel: Generellt fel" @@ -229,6 +232,7 @@ STR_THEME_LYRA_EXTENDED: "Lyra utökad" STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning" STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar" STR_OPDS_BROWSER: "OPDS-webbläsare" +STR_SEARCH: "Sök" STR_COVER_CUSTOM: "Omslag + Valfri" STR_MENU_RECENT_BOOKS: "Senaste böckerna" STR_NO_RECENT_BOOKS: "Inga senaste böcker" @@ -290,3 +294,7 @@ STR_LINK: "[länk]" STR_SCREENSHOT_BUTTON: "Ta en skärmdump" STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: " STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)" +STR_CRASH_TITLE: "Systemkrasch" +STR_CRASH_DESCRIPTION: "En detaljerad rapport sparades till crash_report.txt. Vänligen inkludera den här filen i din felrapport." +STR_CRASH_REASON: "Orsak till kraschen:" +STR_CRASH_NO_REASON: "(Ingen orsak registrerades)" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index 0c6aa1a88..667c90494 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -44,7 +44,7 @@ STR_OPEN_URL_HINT: "Tarayıcınızda bu adresi açın" STR_OR_HTTP_PREFIX: "veya http://" STR_SCAN_QR_HINT: "veya telefonunuzla QR kodu tarayın:" STR_CALIBRE_WIRELESS: "Calibre Kablosuz" -STR_CALIBRE_WEB_URL: "Calibre Web Adresi" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Şifreli | + = Kayıtlı" STR_MAC_ADDRESS: "MAC adresi:" STR_CHECKING_WIFI: "WiFi kontrol ediliyor..." diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index 9eefa0222..d76cad9db 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Відкрийте цю URL-адресу у вашому б STR_OR_HTTP_PREFIX: "або http://" STR_SCAN_QR_HINT: "або відскануйте QR-код телефоном:" STR_CALIBRE_WIRELESS: "Calibre бездротовий" -STR_CALIBRE_WEB_URL: "URL Calibre Web" +STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Зашифровано | + = Збережено" STR_MAC_ADDRESS: "MAC адреса:" STR_CHECKING_WIFI: "Перевірка WiFi..." @@ -284,8 +284,8 @@ STR_UPLOAD: "Завантажити" STR_BOOK_S_STYLE: "Стиль книги" STR_EMBEDDED_STYLE: "Вбудований стиль" STR_OPDS_SERVER_URL: "URL сервера OPDS" -STR_FOOTNOTES: "Зноски" -STR_NO_FOOTNOTES: "На цій сторінці немає зносок" +STR_FOOTNOTES: "Примітки" +STR_NO_FOOTNOTES: "На цій сторінці немає приміток" STR_LINK: "[посилання]" STR_SCREENSHOT_BUTTON: "Знімок екрана" STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: " diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 4b87d632c..0dd787275 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -2,22 +2,15 @@ #include #include +#include #include -#include #include #include +#include #include "BitmapHelpers.h" -// Context structure for picojpeg callback -struct JpegReadContext { - FsFile& file; - uint8_t buffer[512]; - size_t bufferPos; - size_t bufferFilled; -}; - // ============================================================================ // IMAGE PROCESSING OPTIONS - Toggle these to test different configurations // ============================================================================ @@ -165,103 +158,292 @@ static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) } } -// Callback function for picojpeg to read JPEG data -unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const unsigned char buf_size, - unsigned char* pBytes_actually_read, void* pCallback_data) { - auto* context = static_cast(pCallback_data); +namespace { - if (!context || !context->file) { - return PJPG_STREAM_READ_ERROR; +// Max MCU height supported by any JPEG (4:2:0 chroma = 16 rows, 4:4:4 = 8 rows) +constexpr int MAX_MCU_HEIGHT = 16; +constexpr size_t JPEG_DECODER_SIZE = 20 * 1024; +constexpr size_t MIN_FREE_HEAP = JPEG_DECODER_SIZE + 32 * 1024; + +// Static file pointer for JPEGDEC open callback. +// Safe in single-threaded embedded context; never accessed concurrently. +static FsFile* s_jpegFile = nullptr; + +void* bmpJpegOpen(const char* /*filename*/, int32_t* size) { + if (!s_jpegFile || !*s_jpegFile) return nullptr; + s_jpegFile->seek(0); + *size = static_cast(s_jpegFile->size()); + return s_jpegFile; +} + +void bmpJpegClose(void* /*handle*/) { + // Caller owns the file — do not close it here +} + +int32_t bmpJpegRead(JPEGFILE* pFile, uint8_t* pBuf, int32_t len) { + auto* f = reinterpret_cast(pFile->fHandle); + if (!f) return 0; + int32_t n = f->read(pBuf, len); + if (n < 0) n = 0; + pFile->iPos += n; + return n; +} + +int32_t bmpJpegSeek(JPEGFILE* pFile, int32_t pos) { + auto* f = reinterpret_cast(pFile->fHandle); + if (!f || !f->seek(pos)) return -1; + pFile->iPos = pos; + return pos; +} + +// Context passed to the JPEGDEC draw callback via setUserPointer() +struct BmpConvertCtx { + Print* bmpOut; + int srcWidth; + int srcHeight; + int outWidth; + int outHeight; + bool oneBit; + int bytesPerRow; + bool needsScaling; + uint32_t scaleX_fp; // source pixels per output pixel, 16.16 fixed-point + uint32_t scaleY_fp; + + // Accumulates one MCU row (up to MAX_MCU_HEIGHT source rows × srcWidth pixels) + // Filled column-by-column as JPEGDEC callbacks arrive for the same MCU row + uint8_t* mcuBuf; + + // Y-axis area averaging accumulators (needsScaling only) + int currentOutY; + uint32_t nextOutY_srcStart; // 16.16 fixed-point boundary for the next output row + uint32_t* rowAccum; + uint32_t* rowCount; + + uint8_t* bmpRow; + + AtkinsonDitherer* atkinsonDitherer; + FloydSteinbergDitherer* fsDitherer; + Atkinson1BitDitherer* atkinson1BitDitherer; + + bool error; +}; + +// Write a fully-assembled output row (grayscale bytes, length outWidth) to BMP +static void writeOutputRow(BmpConvertCtx* ctx, const uint8_t* srcRow, int outY) { + memset(ctx->bmpRow, 0, ctx->bytesPerRow); + + if (USE_8BIT_OUTPUT && !ctx->oneBit) { + for (int x = 0; x < ctx->outWidth; x++) { + ctx->bmpRow[x] = adjustPixel(srcRow[x]); + } + } else if (ctx->oneBit) { + for (int x = 0; x < ctx->outWidth; x++) { + const uint8_t bit = ctx->atkinson1BitDitherer ? ctx->atkinson1BitDitherer->processPixel(srcRow[x], x) + : quantize1bit(srcRow[x], x, outY); + ctx->bmpRow[x / 8] |= (bit << (7 - (x % 8))); + } + if (ctx->atkinson1BitDitherer) ctx->atkinson1BitDitherer->nextRow(); + } else { + for (int x = 0; x < ctx->outWidth; x++) { + const uint8_t gray = adjustPixel(srcRow[x]); + uint8_t twoBit; + if (ctx->atkinsonDitherer) { + twoBit = ctx->atkinsonDitherer->processPixel(gray, x); + } else if (ctx->fsDitherer) { + twoBit = ctx->fsDitherer->processPixel(gray, x); + } else { + twoBit = quantize(gray, x, outY); + } + ctx->bmpRow[(x * 2) / 8] |= (twoBit << (6 - ((x * 2) % 8))); + } + if (ctx->atkinsonDitherer) + ctx->atkinsonDitherer->nextRow(); + else if (ctx->fsDitherer) + ctx->fsDitherer->nextRow(); } - // Check if we need to refill our context buffer - if (context->bufferPos >= context->bufferFilled) { - context->bufferFilled = context->file.read(context->buffer, sizeof(context->buffer)); - context->bufferPos = 0; + ctx->bmpOut->write(ctx->bmpRow, ctx->bytesPerRow); +} - if (context->bufferFilled == 0) { - // EOF or error - *pBytes_actually_read = 0; - return 0; // Success (EOF is normal) +// Flush one scaled output row from Y-axis accumulators and advance currentOutY +static void flushScaledRow(BmpConvertCtx* ctx) { + memset(ctx->bmpRow, 0, ctx->bytesPerRow); + + if (USE_8BIT_OUTPUT && !ctx->oneBit) { + for (int x = 0; x < ctx->outWidth; x++) { + const uint8_t gray = (ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0; + ctx->bmpRow[x] = adjustPixel(gray); + } + } else if (ctx->oneBit) { + for (int x = 0; x < ctx->outWidth; x++) { + const uint8_t gray = (ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0; + const uint8_t bit = ctx->atkinson1BitDitherer ? ctx->atkinson1BitDitherer->processPixel(gray, x) + : quantize1bit(gray, x, ctx->currentOutY); + ctx->bmpRow[x / 8] |= (bit << (7 - (x % 8))); + } + if (ctx->atkinson1BitDitherer) ctx->atkinson1BitDitherer->nextRow(); + } else { + for (int x = 0; x < ctx->outWidth; x++) { + const uint8_t gray = adjustPixel((ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0); + uint8_t twoBit; + if (ctx->atkinsonDitherer) { + twoBit = ctx->atkinsonDitherer->processPixel(gray, x); + } else if (ctx->fsDitherer) { + twoBit = ctx->fsDitherer->processPixel(gray, x); + } else { + twoBit = quantize(gray, x, ctx->currentOutY); + } + ctx->bmpRow[(x * 2) / 8] |= (twoBit << (6 - ((x * 2) % 8))); + } + if (ctx->atkinsonDitherer) + ctx->atkinsonDitherer->nextRow(); + else if (ctx->fsDitherer) + ctx->fsDitherer->nextRow(); + } + + ctx->bmpOut->write(ctx->bmpRow, ctx->bytesPerRow); + ctx->currentOutY++; +} + +// JPEGDEC draw callback — receives one MCU-width × MCU-height block at a time, +// in left-to-right, top-to-bottom order (baseline JPEG). +// Accumulates columns into mcuBuf; once the last column arrives (completing the MCU +// row), applies scaling + dithering and writes packed BMP rows to bmpOut. +int bmpDrawCallback(JPEGDRAW* pDraw) { + auto* ctx = reinterpret_cast(pDraw->pUser); + if (!ctx || ctx->error) return 0; + + const uint8_t* pixels = reinterpret_cast(pDraw->pPixels); + const int stride = pDraw->iWidth; + const int validW = pDraw->iWidthUsed; + const int blockH = pDraw->iHeight; + const int blockX = pDraw->x; + const int blockY = pDraw->y; + + // Copy block pixels into MCU row buffer + for (int r = 0; r < blockH && r < MAX_MCU_HEIGHT; r++) { + const int copyW = (blockX + validW <= ctx->srcWidth) ? validW : (ctx->srcWidth - blockX); + if (copyW <= 0) continue; + memcpy(ctx->mcuBuf + r * ctx->srcWidth + blockX, pixels + r * stride, copyW); + } + + // Wait for the last MCU column before processing any rows + if (blockX + validW < ctx->srcWidth) return 1; + + // Process each complete source row in this MCU row + const int endRow = blockY + blockH; + + for (int y = blockY; y < endRow && y < ctx->srcHeight; y++) { + const uint8_t* srcRow = ctx->mcuBuf + (y - blockY) * ctx->srcWidth; + + if (!ctx->needsScaling) { + // 1:1 — outWidth == srcWidth, write directly + writeOutputRow(ctx, srcRow, y); + } else { + // Fixed-point area averaging on X axis + for (int outX = 0; outX < ctx->outWidth; outX++) { + const int srcXStart = (static_cast(outX) * ctx->scaleX_fp) >> 16; + const int srcXEnd = (static_cast(outX + 1) * ctx->scaleX_fp) >> 16; + int sum = 0; + int count = 0; + for (int srcX = srcXStart; srcX < srcXEnd && srcX < ctx->srcWidth; srcX++) { + sum += srcRow[srcX]; + count++; + } + if (count == 0 && srcXStart < ctx->srcWidth) { + sum = srcRow[srcXStart]; + count = 1; + } + ctx->rowAccum[outX] += sum; + ctx->rowCount[outX] += count; + } + + // Flush output row(s) whose Y boundary we've crossed + const uint32_t srcY_fp = static_cast(y + 1) << 16; + while (srcY_fp >= ctx->nextOutY_srcStart && ctx->currentOutY < ctx->outHeight) { + flushScaledRow(ctx); + ctx->nextOutY_srcStart = static_cast(ctx->currentOutY + 1) * ctx->scaleY_fp; + if (srcY_fp >= ctx->nextOutY_srcStart) continue; + memset(ctx->rowAccum, 0, ctx->outWidth * sizeof(uint32_t)); + memset(ctx->rowCount, 0, ctx->outWidth * sizeof(uint32_t)); + } } } - // Copy available bytes to picojpeg's buffer - const size_t available = context->bufferFilled - context->bufferPos; - const size_t toRead = available < buf_size ? available : buf_size; - - memcpy(pBuf, context->buffer + context->bufferPos, toRead); - context->bufferPos += toRead; - *pBytes_actually_read = static_cast(toRead); - - return 0; // Success + return ctx->error ? 0 : 1; } +} // namespace + // Internal implementation with configurable target size and bit depth bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight, bool oneBit, bool crop) { LOG_DBG("JPG", "Converting JPEG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight); - // Setup context for picojpeg callback - JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0}; - - // Initialize picojpeg decoder - pjpeg_image_info_t imageInfo; - const unsigned char status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0); - if (status != 0) { - LOG_ERR("JPG", "JPEG decode init failed with error code: %d", status); + if (ESP.getFreeHeap() < MIN_FREE_HEAP) { + LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", ESP.getFreeHeap(), MIN_FREE_HEAP); return false; } - LOG_DBG("JPG", "JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d", imageInfo.m_width, imageInfo.m_height, - imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol); + s_jpegFile = &jpegFile; + + JPEGDEC* jpeg = new (std::nothrow) JPEGDEC(); + if (!jpeg) { + LOG_ERR("JPG", "Failed to allocate JPEG decoder"); + return false; + } + + int rc = jpeg->open("", bmpJpegOpen, bmpJpegClose, bmpJpegRead, bmpJpegSeek, bmpDrawCallback); + if (rc != 1) { + LOG_ERR("JPG", "JPEG open failed (err=%d)", jpeg->getLastError()); + delete jpeg; + return false; + } + + const int srcWidth = jpeg->getWidth(); + const int srcHeight = jpeg->getHeight(); + + LOG_DBG("JPG", "JPEG dimensions: %dx%d", srcWidth, srcHeight); - // Safety limits to prevent memory issues on ESP32 constexpr int MAX_IMAGE_WIDTH = 2048; constexpr int MAX_IMAGE_HEIGHT = 3072; - constexpr int MAX_MCU_ROW_BYTES = 65536; - if (imageInfo.m_width > MAX_IMAGE_WIDTH || imageInfo.m_height > MAX_IMAGE_HEIGHT) { - LOG_DBG("JPG", "Image too large (%dx%d), max supported: %dx%d", imageInfo.m_width, imageInfo.m_height, - MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT); + if (srcWidth <= 0 || srcHeight <= 0 || srcWidth > MAX_IMAGE_WIDTH || srcHeight > MAX_IMAGE_HEIGHT) { + LOG_DBG("JPG", "Image too large or invalid (%dx%d), max supported: %dx%d", srcWidth, srcHeight, MAX_IMAGE_WIDTH, + MAX_IMAGE_HEIGHT); + jpeg->close(); + delete jpeg; return false; } // Calculate output dimensions (pre-scale to fit display exactly) - int outWidth = imageInfo.m_width; - int outHeight = imageInfo.m_height; - // Use fixed-point scaling (16.16) for sub-pixel accuracy + int outWidth = srcWidth; + int outHeight = srcHeight; uint32_t scaleX_fp = 65536; // 1.0 in 16.16 fixed point uint32_t scaleY_fp = 65536; bool needsScaling = false; - if (targetWidth > 0 && targetHeight > 0 && (imageInfo.m_width != targetWidth || imageInfo.m_height != targetHeight)) { - // Calculate scale to fit/fill target dimensions while maintaining aspect ratio - const float scaleToFitWidth = static_cast(targetWidth) / imageInfo.m_width; - const float scaleToFitHeight = static_cast(targetHeight) / imageInfo.m_height; - // We scale to the smaller dimension, so we can potentially crop later. - float scale = 1.0; - if (crop) { // if we will crop, scale to the smaller dimension + if (targetWidth > 0 && targetHeight > 0 && (srcWidth != targetWidth || srcHeight != targetHeight)) { + const float scaleToFitWidth = static_cast(targetWidth) / srcWidth; + const float scaleToFitHeight = static_cast(targetHeight) / srcHeight; + float scale = 1.0f; + if (crop) { scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; - } else { // else, scale to the larger dimension to fit + } else { scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; } - outWidth = static_cast(imageInfo.m_width * scale); - outHeight = static_cast(imageInfo.m_height * scale); - - // Ensure at least 1 pixel + outWidth = static_cast(srcWidth * scale); + outHeight = static_cast(srcHeight * scale); if (outWidth < 1) outWidth = 1; if (outHeight < 1) outHeight = 1; - // Calculate fixed-point scale factors (source pixels per output pixel) - // scaleX_fp = (srcWidth << 16) / outWidth - scaleX_fp = (static_cast(imageInfo.m_width) << 16) / outWidth; - scaleY_fp = (static_cast(imageInfo.m_height) << 16) / outHeight; + scaleX_fp = (static_cast(srcWidth) << 16) / outWidth; + scaleY_fp = (static_cast(srcHeight) << 16) / outHeight; needsScaling = true; - LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", imageInfo.m_width, imageInfo.m_height, outWidth, outHeight, - targetWidth, targetHeight); + LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", srcWidth, srcHeight, outWidth, outHeight, targetWidth, + targetHeight); } // Write BMP header with output dimensions @@ -271,285 +453,84 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm bytesPerRow = (outWidth + 3) / 4 * 4; } else if (oneBit) { writeBmpHeader1bit(bmpOut, outWidth, outHeight); - bytesPerRow = (outWidth + 31) / 32 * 4; // 1 bit per pixel + bytesPerRow = (outWidth + 31) / 32 * 4; } else { writeBmpHeader2bit(bmpOut, outWidth, outHeight); bytesPerRow = (outWidth * 2 + 31) / 32 * 4; } - uint8_t* rowBuffer = nullptr; - uint8_t* mcuRowBuffer = nullptr; - AtkinsonDitherer* atkinsonDitherer = nullptr; - FloydSteinbergDitherer* fsDitherer = nullptr; - Atkinson1BitDitherer* atkinson1BitDitherer = nullptr; - uint32_t* rowAccum = nullptr; // Accumulator for each output X (32-bit for larger sums) - uint32_t* rowCount = nullptr; // Count of source pixels accumulated per output X + BmpConvertCtx ctx = {}; + ctx.bmpOut = &bmpOut; + ctx.srcWidth = srcWidth; + ctx.srcHeight = srcHeight; + ctx.outWidth = outWidth; + ctx.outHeight = outHeight; + ctx.oneBit = oneBit; + ctx.bytesPerRow = bytesPerRow; + ctx.needsScaling = needsScaling; + ctx.scaleX_fp = scaleX_fp; + ctx.scaleY_fp = scaleY_fp; + ctx.error = false; - // RAII guard: frees all heap resources on any return path, including early exits. - // Holds references so it always sees the latest pointer values assigned below. + // RAII guard: frees all heap resources on any return path struct Cleanup { - uint8_t*& rowBuffer; - uint8_t*& mcuRowBuffer; - AtkinsonDitherer*& atkinsonDitherer; - FloydSteinbergDitherer*& fsDitherer; - Atkinson1BitDitherer*& atkinson1BitDitherer; - uint32_t*& rowAccum; - uint32_t*& rowCount; + BmpConvertCtx& ctx; + JPEGDEC* jpeg; ~Cleanup() { - delete[] rowAccum; - delete[] rowCount; - delete atkinsonDitherer; - delete fsDitherer; - delete atkinson1BitDitherer; - free(mcuRowBuffer); - free(rowBuffer); + delete[] ctx.rowAccum; + delete[] ctx.rowCount; + delete ctx.atkinsonDitherer; + delete ctx.fsDitherer; + delete ctx.atkinson1BitDitherer; + free(ctx.mcuBuf); + free(ctx.bmpRow); + jpeg->close(); + delete jpeg; } - } cleanup{rowBuffer, mcuRowBuffer, atkinsonDitherer, fsDitherer, atkinson1BitDitherer, rowAccum, rowCount}; + } cleanup{ctx, jpeg}; - // Allocate row buffer - rowBuffer = static_cast(malloc(bytesPerRow)); - if (!rowBuffer) { - LOG_ERR("JPG", "Failed to allocate row buffer"); + // MCU row buffer: MAX_MCU_HEIGHT rows × srcWidth columns of grayscale + ctx.mcuBuf = static_cast(malloc(MAX_MCU_HEIGHT * srcWidth)); + if (!ctx.mcuBuf) { + LOG_ERR("JPG", "Failed to allocate MCU buffer (%d bytes)", MAX_MCU_HEIGHT * srcWidth); return false; } + memset(ctx.mcuBuf, 0, MAX_MCU_HEIGHT * srcWidth); - // Allocate a buffer for one MCU row worth of grayscale pixels - // This is the minimal memory needed for streaming conversion - const int mcuPixelHeight = imageInfo.m_MCUHeight; - const int mcuRowPixels = imageInfo.m_width * mcuPixelHeight; - - // Validate MCU row buffer size before allocation - if (mcuRowPixels > MAX_MCU_ROW_BYTES) { - LOG_DBG("JPG", "MCU row buffer too large (%d bytes), max: %d", mcuRowPixels, MAX_MCU_ROW_BYTES); + ctx.bmpRow = static_cast(malloc(bytesPerRow)); + if (!ctx.bmpRow) { + LOG_ERR("JPG", "Failed to allocate BMP row buffer"); return false; } - mcuRowBuffer = static_cast(malloc(mcuRowPixels)); - if (!mcuRowBuffer) { - LOG_ERR("JPG", "Failed to allocate MCU row buffer (%d bytes)", mcuRowPixels); - return false; - } - - // Create ditherer if enabled - // Use OUTPUT dimensions for dithering (after prescaling) - if (oneBit) { - // For 1-bit output, use Atkinson dithering for better quality - atkinson1BitDitherer = new Atkinson1BitDitherer(outWidth); - } else if (!USE_8BIT_OUTPUT) { - if (USE_ATKINSON) { - atkinsonDitherer = new AtkinsonDitherer(outWidth); - } else if (USE_FLOYD_STEINBERG) { - fsDitherer = new FloydSteinbergDitherer(outWidth); - } - } - - // For scaling: accumulate source rows into scaled output rows - // We need to track which source Y maps to which output Y - // Using fixed-point: srcY_fp = outY * scaleY_fp (gives source Y in 16.16 format) - int currentOutY = 0; // Current output row being accumulated - uint32_t nextOutY_srcStart = 0; // Source Y where next output row starts (16.16 fixed point) - if (needsScaling) { - rowAccum = new uint32_t[outWidth](); - rowCount = new uint32_t[outWidth](); - nextOutY_srcStart = scaleY_fp; // First boundary is at scaleY_fp (source Y for outY=1) + ctx.rowAccum = new (std::nothrow) uint32_t[outWidth](); + ctx.rowCount = new (std::nothrow) uint32_t[outWidth](); + if (!ctx.rowAccum || !ctx.rowCount) { + LOG_ERR("JPG", "Failed to allocate scaling buffers"); + return false; + } + ctx.nextOutY_srcStart = scaleY_fp; } - // Process MCUs row-by-row and write to BMP as we go (top-down) - const int mcuPixelWidth = imageInfo.m_MCUWidth; - - for (int mcuY = 0; mcuY < imageInfo.m_MCUSPerCol; mcuY++) { - // Clear the MCU row buffer - memset(mcuRowBuffer, 0, mcuRowPixels); - - // Decode one row of MCUs - for (int mcuX = 0; mcuX < imageInfo.m_MCUSPerRow; mcuX++) { - const unsigned char mcuStatus = pjpeg_decode_mcu(); - if (mcuStatus != 0) { - if (mcuStatus == PJPG_NO_MORE_BLOCKS) { - LOG_ERR("JPG", "Unexpected end of blocks at MCU (%d, %d)", mcuX, mcuY); - } else { - LOG_ERR("JPG", "JPEG decode MCU failed at (%d, %d) with error code: %d", mcuX, mcuY, mcuStatus); - } - return false; - } - - // picojpeg stores MCU data in 8x8 blocks - // Block layout: H2V2(16x16)=0,64,128,192 H2V1(16x8)=0,64 H1V2(8x16)=0,128 - for (int blockY = 0; blockY < mcuPixelHeight; blockY++) { - for (int blockX = 0; blockX < mcuPixelWidth; blockX++) { - const int pixelX = mcuX * mcuPixelWidth + blockX; - if (pixelX >= imageInfo.m_width) continue; - - // Calculate proper block offset for picojpeg buffer - const int blockCol = blockX / 8; - const int blockRow = blockY / 8; - const int localX = blockX % 8; - const int localY = blockY % 8; - const int blocksPerRow = mcuPixelWidth / 8; - const int blockIndex = blockRow * blocksPerRow + blockCol; - const int pixelOffset = blockIndex * 64 + localY * 8 + localX; - - uint8_t gray; - if (imageInfo.m_comps == 1) { - gray = imageInfo.m_pMCUBufR[pixelOffset]; - } else { - const uint8_t r = imageInfo.m_pMCUBufR[pixelOffset]; - const uint8_t g = imageInfo.m_pMCUBufG[pixelOffset]; - const uint8_t b = imageInfo.m_pMCUBufB[pixelOffset]; - gray = (r * 25 + g * 50 + b * 25) / 100; - } - - mcuRowBuffer[blockY * imageInfo.m_width + pixelX] = gray; - } - } + if (oneBit) { + ctx.atkinson1BitDitherer = new (std::nothrow) Atkinson1BitDitherer(outWidth); + } else if (!USE_8BIT_OUTPUT) { + if (USE_ATKINSON) { + ctx.atkinsonDitherer = new (std::nothrow) AtkinsonDitherer(outWidth); + } else if (USE_FLOYD_STEINBERG) { + ctx.fsDitherer = new (std::nothrow) FloydSteinbergDitherer(outWidth); } + } - // Process source rows from this MCU row - const int startRow = mcuY * mcuPixelHeight; - const int endRow = (mcuY + 1) * mcuPixelHeight; + jpeg->setPixelType(EIGHT_BIT_GRAYSCALE); + jpeg->setUserPointer(&ctx); - for (int y = startRow; y < endRow && y < imageInfo.m_height; y++) { - const int bufferY = y - startRow; + rc = jpeg->decode(0, 0, 0); - if (!needsScaling) { - // No scaling - direct output (1:1 mapping) - memset(rowBuffer, 0, bytesPerRow); - - if (USE_8BIT_OUTPUT && !oneBit) { - for (int x = 0; x < outWidth; x++) { - const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x]; - rowBuffer[x] = adjustPixel(gray); - } - } else if (oneBit) { - // 1-bit output with Atkinson dithering for better quality - for (int x = 0; x < outWidth; x++) { - const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x]; - const uint8_t bit = - atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x) : quantize1bit(gray, x, y); - // Pack 1-bit value: MSB first, 8 pixels per byte - const int byteIndex = x / 8; - const int bitOffset = 7 - (x % 8); - rowBuffer[byteIndex] |= (bit << bitOffset); - } - if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow(); - } else { - // 2-bit output - for (int x = 0; x < outWidth; x++) { - const uint8_t gray = adjustPixel(mcuRowBuffer[bufferY * imageInfo.m_width + x]); - uint8_t twoBit; - if (atkinsonDitherer) { - twoBit = atkinsonDitherer->processPixel(gray, x); - } else if (fsDitherer) { - twoBit = fsDitherer->processPixel(gray, x); - } else { - twoBit = quantize(gray, x, y); - } - const int byteIndex = (x * 2) / 8; - const int bitOffset = 6 - ((x * 2) % 8); - rowBuffer[byteIndex] |= (twoBit << bitOffset); - } - if (atkinsonDitherer) - atkinsonDitherer->nextRow(); - else if (fsDitherer) - fsDitherer->nextRow(); - } - bmpOut.write(rowBuffer, bytesPerRow); - } else { - // Fixed-point area averaging for exact fit scaling - // For each output pixel X, accumulate source pixels that map to it - // srcX range for outX: [outX * scaleX_fp >> 16, (outX+1) * scaleX_fp >> 16) - const uint8_t* srcRow = mcuRowBuffer + bufferY * imageInfo.m_width; - - for (int outX = 0; outX < outWidth; outX++) { - // Calculate source X range for this output pixel - const int srcXStart = (static_cast(outX) * scaleX_fp) >> 16; - const int srcXEnd = (static_cast(outX + 1) * scaleX_fp) >> 16; - - // Accumulate all source pixels in this range - int sum = 0; - int count = 0; - for (int srcX = srcXStart; srcX < srcXEnd && srcX < imageInfo.m_width; srcX++) { - sum += srcRow[srcX]; - count++; - } - - // Handle edge case: if no pixels in range, use nearest - if (count == 0 && srcXStart < imageInfo.m_width) { - sum = srcRow[srcXStart]; - count = 1; - } - - rowAccum[outX] += sum; - rowCount[outX] += count; - } - - // Check if we've crossed into the next output row(s) - // Current source Y in fixed point: y << 16 - const uint32_t srcY_fp = static_cast(y + 1) << 16; - - // Output all rows whose boundaries we've crossed (handles both up and downscaling) - // For upscaling, one source row may produce multiple output rows - while (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) { - memset(rowBuffer, 0, bytesPerRow); - - if (USE_8BIT_OUTPUT && !oneBit) { - for (int x = 0; x < outWidth; x++) { - const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0; - rowBuffer[x] = adjustPixel(gray); - } - } else if (oneBit) { - // 1-bit output with Atkinson dithering for better quality - for (int x = 0; x < outWidth; x++) { - const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0; - const uint8_t bit = atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x) - : quantize1bit(gray, x, currentOutY); - // Pack 1-bit value: MSB first, 8 pixels per byte - const int byteIndex = x / 8; - const int bitOffset = 7 - (x % 8); - rowBuffer[byteIndex] |= (bit << bitOffset); - } - if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow(); - } else { - // 2-bit output - for (int x = 0; x < outWidth; x++) { - const uint8_t gray = adjustPixel((rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0); - uint8_t twoBit; - if (atkinsonDitherer) { - twoBit = atkinsonDitherer->processPixel(gray, x); - } else if (fsDitherer) { - twoBit = fsDitherer->processPixel(gray, x); - } else { - twoBit = quantize(gray, x, currentOutY); - } - const int byteIndex = (x * 2) / 8; - const int bitOffset = 6 - ((x * 2) % 8); - rowBuffer[byteIndex] |= (twoBit << bitOffset); - } - if (atkinsonDitherer) - atkinsonDitherer->nextRow(); - else if (fsDitherer) - fsDitherer->nextRow(); - } - - bmpOut.write(rowBuffer, bytesPerRow); - currentOutY++; - - // Update boundary for next output row - nextOutY_srcStart = static_cast(currentOutY + 1) * scaleY_fp; - - // For upscaling: don't reset accumulators if next output row uses same source data - // Only reset when we'll move to a new source row - if (srcY_fp >= nextOutY_srcStart) { - // More output rows to emit from same source - keep accumulator data - continue; - } - // Moving to next source row - reset accumulators - memset(rowAccum, 0, outWidth * sizeof(uint32_t)); - memset(rowCount, 0, outWidth * sizeof(uint32_t)); - } - } - } + if (rc != 1 || ctx.error) { + LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError()); + return false; } LOG_DBG("JPG", "Successfully converted JPEG to BMP"); diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.h b/lib/JpegToBmpConverter/JpegToBmpConverter.h index 125692e46..66f77f673 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.h +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.h @@ -6,8 +6,6 @@ class Print; class ZipFile; class JpegToBmpConverter { - static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size, - unsigned char* pBytes_actually_read, void* pCallback_data); static bool jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight, bool oneBit, bool crop = true); diff --git a/lib/KOReaderSync/KOReaderCredentialStore.cpp b/lib/KOReaderSync/KOReaderCredentialStore.cpp index 574f2b8e9..ce7132df2 100644 --- a/lib/KOReaderSync/KOReaderCredentialStore.cpp +++ b/lib/KOReaderSync/KOReaderCredentialStore.cpp @@ -82,7 +82,6 @@ bool KOReaderCredentialStore::loadFromBinaryFile() { serialization::readPod(file, version); if (version != KOREADER_FILE_VERSION) { LOG_DBG("KRS", "Unknown file version: %u", version); - file.close(); return false; } @@ -113,7 +112,6 @@ bool KOReaderCredentialStore::loadFromBinaryFile() { matchMethod = DocumentMatchMethod::FILENAME; } - file.close(); LOG_DBG("KRS", "Loaded KOReader credentials from binary for user: %s", username.c_str()); return true; } diff --git a/lib/KOReaderSync/KOReaderDocumentId.cpp b/lib/KOReaderSync/KOReaderDocumentId.cpp index efb18d1b7..887214191 100644 --- a/lib/KOReaderSync/KOReaderDocumentId.cpp +++ b/lib/KOReaderSync/KOReaderDocumentId.cpp @@ -84,8 +84,6 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) { } } - file.close(); - // Calculate final hash md5.calculate(); std::string result = md5.toString().c_str(); diff --git a/lib/Logging/Logging.cpp b/lib/Logging/Logging.cpp index d670f5fbd..3306f2542 100644 --- a/lib/Logging/Logging.cpp +++ b/lib/Logging/Logging.cpp @@ -38,37 +38,26 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) { va_start(args, format); char buf[MAX_ENTRY_LEN]; char* c = buf; - // add the timestamp + // add timestamp, level and origin { unsigned long ms = millis(); - int len = snprintf(c, sizeof(buf), "[%lu] ", ms); + int len = snprintf(c, sizeof(buf), "[%lu] [%s] [%s] ", ms, level, origin); + // erro while writing => return if (len < 0) { - return; // encoding error, skip logging + va_end(args); + return; } - c += len; - } - // add the level - { - const char* p = level; - size_t remaining = sizeof(buf) - (c - buf); - while (*p && remaining > 1) { - *c++ = *p++; - remaining--; - } - if (remaining > 1) { - *c++ = ' '; - } - } - // add the origin - { - int len = snprintf(c, sizeof(buf) - (c - buf), "[%s] ", origin); - if (len < 0) { - return; // encoding error, skip logging - } - c += len; + // clamp c to be in buffer range + c += std::min(len, MAX_ENTRY_LEN); } // add the user message - vsnprintf(c, sizeof(buf) - (c - buf), format, args); + { + int len = vsnprintf(c, sizeof(buf) - (c - buf), format, args); + if (len < 0) { + va_end(args); + return; + } + } va_end(args); if (logSerial) { logSerial.print(buf); diff --git a/lib/Logging/Logging.h b/lib/Logging/Logging.h index 47e8eb7dc..8784a3418 100644 --- a/lib/Logging/Logging.h +++ b/lib/Logging/Logging.h @@ -33,19 +33,19 @@ void logPrintf(const char* level, const char* origin, const char* format, ...); #ifdef ENABLE_SERIAL_LOG #if LOG_LEVEL >= 0 -#define LOG_ERR(origin, format, ...) logPrintf("[ERR]", origin, format "\n", ##__VA_ARGS__) +#define LOG_ERR(origin, format, ...) logPrintf("ERR", origin, format "\n", ##__VA_ARGS__) #else #define LOG_ERR(origin, format, ...) #endif #if LOG_LEVEL >= 1 -#define LOG_INF(origin, format, ...) logPrintf("[INF]", origin, format "\n", ##__VA_ARGS__) +#define LOG_INF(origin, format, ...) logPrintf("INF", origin, format "\n", ##__VA_ARGS__) #else #define LOG_INF(origin, format, ...) #endif #if LOG_LEVEL >= 2 -#define LOG_DBG(origin, format, ...) logPrintf("[DBG]", origin, format "\n", ##__VA_ARGS__) +#define LOG_DBG(origin, format, ...) logPrintf("DBG", origin, format "\n", ##__VA_ARGS__) #else #define LOG_DBG(origin, format, ...) #endif diff --git a/lib/OpdsParser/OpdsParser.cpp b/lib/OpdsParser/OpdsParser.cpp index f4ce69602..84feef74f 100644 --- a/lib/OpdsParser/OpdsParser.cpp +++ b/lib/OpdsParser/OpdsParser.cpp @@ -25,15 +25,12 @@ OpdsParser::~OpdsParser() { size_t OpdsParser::write(uint8_t c) { return write(&c, 1); } size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) { - if (errorOccured) { - return length; - } + if (errorOccured) return length; XML_SetUserData(parser, this); XML_SetElementHandler(parser, startElement, endElement); XML_SetCharacterDataHandler(parser, characterData); - // Parse in chunks to avoid large buffer allocations const char* currentPos = reinterpret_cast(xmlData); size_t remaining = length; constexpr size_t chunkSize = 1024; @@ -42,9 +39,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) { void* const buf = XML_GetBuffer(parser, chunkSize); if (!buf) { errorOccured = true; - LOG_DBG("OPDS", "Couldn't allocate memory for buffer"); XML_ParserFree(parser); - parser = nullptr; return length; } @@ -53,13 +48,9 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) { if (XML_ParseBuffer(parser, static_cast(toRead), 0) == XML_STATUS_ERROR) { errorOccured = true; - LOG_DBG("OPDS", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser), - XML_ErrorString(XML_GetErrorCode(parser))); XML_ParserFree(parser); - parser = nullptr; return length; } - currentPos += toRead; remaining -= toRead; } @@ -78,30 +69,25 @@ bool OpdsParser::error() const { return errorOccured; } void OpdsParser::clear() { entries.clear(); + searchTemplate.clear(); + nextPageUrl.clear(); + prevPageUrl.clear(); currentEntry = OpdsEntry{}; currentText.clear(); - inEntry = false; - inTitle = false; - inAuthor = false; - inAuthorName = false; - inId = false; + inEntry = inTitle = inAuthor = inAuthorName = inId = false; } std::vector OpdsParser::getBooks() const { std::vector books; for (const auto& entry : entries) { - if (entry.type == OpdsEntryType::BOOK) { - books.push_back(entry); - } + if (entry.type == OpdsEntryType::BOOK) books.push_back(entry); } return books; } const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) { for (int i = 0; atts[i]; i += 2) { - if (strcmp(atts[i], name) == 0) { - return atts[i + 1]; - } + if (strcmp(atts[i], name) == 0) return atts[i + 1]; } return nullptr; } @@ -109,7 +95,38 @@ const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) { void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { auto* self = static_cast(userData); - // Check for entry element (with or without namespace prefix) + if (strcmp(name, "link") == 0 || strstr(name, ":link") != nullptr) { + const char* href = findAttribute(atts, "href"); + if (href) { + const char* rel = findAttribute(atts, "rel"); + const char* type = findAttribute(atts, "type"); + + if (rel && strcmp(rel, "search") == 0) { + std::string sHref(href); + if (sHref.find("{searchTerms}") != std::string::npos) { + self->searchTemplate = sHref; + } + } else if (rel && strcmp(rel, "next") == 0 && !self->inEntry) { + self->nextPageUrl = href; + } else if (rel && strcmp(rel, "previous") == 0 && !self->inEntry) { + self->prevPageUrl = href; + } + + if (self->inEntry) { + if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr && + strcmp(type, "application/epub+zip") == 0) { + self->currentEntry.type = OpdsEntryType::BOOK; + self->currentEntry.href = href; + } else if (type && strstr(type, "application/atom+xml") != nullptr) { + if (self->currentEntry.type != OpdsEntryType::BOOK) { + self->currentEntry.type = OpdsEntryType::NAVIGATION; + self->currentEntry.href = href; + } + } + } + } + } + if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) { self->inEntry = true; self->currentEntry = OpdsEntry{}; @@ -118,112 +135,46 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons if (!self->inEntry) return; - // Check for title element if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { self->inTitle = true; self->currentText.clear(); - return; - } - - // Check for author element - if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { + } else if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { self->inAuthor = true; - return; - } - - // Check for author name element - if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { + } else if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { self->inAuthorName = true; self->currentText.clear(); - return; - } - - // Check for id element - if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { + } else if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { self->inId = true; self->currentText.clear(); - return; - } - - // Check for link element - if (strcmp(name, "link") == 0 || strstr(name, ":link") != nullptr) { - const char* rel = findAttribute(atts, "rel"); - const char* type = findAttribute(atts, "type"); - const char* href = findAttribute(atts, "href"); - - if (href) { - // Check for acquisition link with epub type (this is a downloadable book) - if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr && - strcmp(type, "application/epub+zip") == 0) { - self->currentEntry.type = OpdsEntryType::BOOK; - self->currentEntry.href = href; - } - // Check for navigation link (subsection or no rel specified with atom+xml type) - else if (type && strstr(type, "application/atom+xml") != nullptr) { - // Only set navigation link if we don't already have an epub link - if (self->currentEntry.type != OpdsEntryType::BOOK) { - self->currentEntry.type = OpdsEntryType::NAVIGATION; - self->currentEntry.href = href; - } - } - } } } void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) { auto* self = static_cast(userData); - // Check for entry end if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) { - // Only add entry if it has required fields (title and href) if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) { self->entries.push_back(self->currentEntry); } self->inEntry = false; - self->currentEntry = OpdsEntry{}; - return; - } - - if (!self->inEntry) return; - - // Check for title end - if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { - if (self->inTitle) { - self->currentEntry.title = self->currentText; - } - self->inTitle = false; - return; - } - - // Check for author end - if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { - self->inAuthor = false; - return; - } - - // Check for author name end - if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { - if (self->inAuthorName) { + } else if (self->inEntry) { + if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) { + if (self->inTitle) self->currentEntry.title = self->currentText; + self->inTitle = false; + } else if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) { + self->inAuthor = false; + } else if (self->inAuthorName && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) { self->currentEntry.author = self->currentText; + self->inAuthorName = false; + } else if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { + if (self->inId) self->currentEntry.id = self->currentText; + self->inId = false; } - self->inAuthorName = false; - return; - } - - // Check for id end - if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) { - if (self->inId) { - self->currentEntry.id = self->currentText; - } - self->inId = false; - return; } } void XMLCALL OpdsParser::characterData(void* userData, const XML_Char* s, const int len) { auto* self = static_cast(userData); - - // Only accumulate text when in a text element if (self->inTitle || self->inAuthorName || self->inId) { self->currentText.append(s, len); } diff --git a/lib/OpdsParser/OpdsParser.h b/lib/OpdsParser/OpdsParser.h index 570ac4cce..9c93b89ac 100644 --- a/lib/OpdsParser/OpdsParser.h +++ b/lib/OpdsParser/OpdsParser.h @@ -49,6 +49,9 @@ class OpdsParser final : public Print { ~OpdsParser(); // Disable copy + const std::string& getSearchTemplate() const { return searchTemplate; } + const std::string& getNextPageUrl() const { return nextPageUrl; } + const std::string& getPrevPageUrl() const { return prevPageUrl; } OpdsParser(const OpdsParser&) = delete; OpdsParser& operator=(const OpdsParser&) = delete; @@ -85,6 +88,9 @@ class OpdsParser final : public Print { static void XMLCALL endElement(void* userData, const XML_Char* name); static void XMLCALL characterData(void* userData, const XML_Char* s, int len); + std::string searchTemplate; + std::string nextPageUrl; + std::string prevPageUrl; // Helper to find attribute value static const char* findAttribute(const XML_Char** atts, const char* name); diff --git a/lib/Txt/Txt.cpp b/lib/Txt/Txt.cpp index 83ef123cc..0209923a8 100644 --- a/lib/Txt/Txt.cpp +++ b/lib/Txt/Txt.cpp @@ -120,7 +120,6 @@ bool Txt::generateCoverBmp() const { return false; } if (!Storage.openFileForWrite("TXT", getCoverBmpPath(), dst)) { - src.close(); return false; } uint8_t buffer[1024]; @@ -128,8 +127,6 @@ bool Txt::generateCoverBmp() const { size_t bytesRead = src.read(buffer, sizeof(buffer)); dst.write(buffer, bytesRead); } - src.close(); - dst.close(); LOG_DBG("TXT", "Copied BMP cover to cache"); return true; } else if (FsHelpers::hasJpgExtension(coverImagePath)) { @@ -140,12 +137,9 @@ bool Txt::generateCoverBmp() const { return false; } if (!Storage.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) { - coverJpg.close(); return false; } const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp); - coverJpg.close(); - coverBmp.close(); if (!success) { LOG_ERR("TXT", "Failed to generate BMP from JPG cover image"); @@ -172,12 +166,9 @@ bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const { } if (!file.seek(offset)) { - file.close(); return false; } size_t bytesRead = file.read(buffer, length); - file.close(); - return bytesRead > 0; } diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index 53d32cac3..893e8169c 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -7,6 +7,7 @@ #include "Xtc.h" +#include #include #include @@ -172,52 +173,12 @@ bool Xtc::generateCoverBmp() const { return false; } - // Write BMP header - // BMP file header (14 bytes) - const uint32_t rowSize = ((pageInfo.width + 31) / 32) * 4; // Row size aligned to 4 bytes - const uint32_t imageSize = rowSize * pageInfo.height; - const uint32_t fileSize = 14 + 40 + 8 + imageSize; // Header + DIB + palette + data + // Write 1-bit BMP header (top-down row order) + BmpHeader bmpHeader; + createBmpHeader(&bmpHeader, pageInfo.width, pageInfo.height, BmpRowOrder::TopDown); + coverBmp.write(reinterpret_cast(&bmpHeader), sizeof(bmpHeader)); - // File header - coverBmp.write('B'); - coverBmp.write('M'); - coverBmp.write(reinterpret_cast(&fileSize), 4); - uint32_t reserved = 0; - coverBmp.write(reinterpret_cast(&reserved), 4); - uint32_t dataOffset = 14 + 40 + 8; // 1-bit palette has 2 colors (8 bytes) - coverBmp.write(reinterpret_cast(&dataOffset), 4); - - // DIB header (BITMAPINFOHEADER - 40 bytes) - uint32_t dibHeaderSize = 40; - coverBmp.write(reinterpret_cast(&dibHeaderSize), 4); - int32_t width = pageInfo.width; - coverBmp.write(reinterpret_cast(&width), 4); - int32_t height = -static_cast(pageInfo.height); // Negative for top-down - coverBmp.write(reinterpret_cast(&height), 4); - uint16_t planes = 1; - coverBmp.write(reinterpret_cast(&planes), 2); - uint16_t bitsPerPixel = 1; // 1-bit monochrome - coverBmp.write(reinterpret_cast(&bitsPerPixel), 2); - uint32_t compression = 0; // BI_RGB (no compression) - coverBmp.write(reinterpret_cast(&compression), 4); - coverBmp.write(reinterpret_cast(&imageSize), 4); - int32_t ppmX = 2835; // 72 DPI - coverBmp.write(reinterpret_cast(&ppmX), 4); - int32_t ppmY = 2835; - coverBmp.write(reinterpret_cast(&ppmY), 4); - uint32_t colorsUsed = 2; - coverBmp.write(reinterpret_cast(&colorsUsed), 4); - uint32_t colorsImportant = 2; - coverBmp.write(reinterpret_cast(&colorsImportant), 4); - - // Color palette (2 colors for 1-bit) - // XTC 1-bit polarity: 0 = black, 1 = white (standard BMP palette order) - // Color 0: Black (text/foreground in XTC) - uint8_t black[4] = {0x00, 0x00, 0x00, 0x00}; - coverBmp.write(black, 4); - // Color 1: White (background in XTC) - uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00}; - coverBmp.write(white, 4); + const uint32_t rowSize = ((pageInfo.width + 31) / 32) * 4; // Write bitmap data // BMP requires 4-byte row alignment @@ -238,7 +199,6 @@ bool Xtc::generateCoverBmp() const { uint8_t* rowBuffer = static_cast(malloc(dstRowSize)); if (!rowBuffer) { free(pageBuffer); - coverBmp.close(); return false; } @@ -294,7 +254,6 @@ bool Xtc::generateCoverBmp() const { } } - coverBmp.close(); free(pageBuffer); LOG_DBG("XTC", "Generated cover BMP: %s", getCoverBmpPath().c_str()); @@ -355,9 +314,7 @@ bool Xtc::generateThumbBmp(int height) const { size_t bytesRead = src.read(buffer, sizeof(buffer)); dst.write(buffer, bytesRead); } - dst.close(); } - src.close(); } LOG_DBG("XTC", "Copied cover to thumb (no scaling needed)"); return Storage.exists(getThumbBmpPath(height).c_str()); @@ -400,55 +357,17 @@ bool Xtc::generateThumbBmp(int height) const { return false; } - // Write 1-bit BMP header for fast home screen rendering - const uint32_t rowSize = (thumbWidth + 31) / 32 * 4; // 1 bit per pixel, aligned to 4 bytes - const uint32_t imageSize = rowSize * thumbHeight; - const uint32_t fileSize = 14 + 40 + 8 + imageSize; // 8 bytes for 2-color palette + // Write 1-bit BMP header (top-down row order) + BmpHeader bmpHeader; + createBmpHeader(&bmpHeader, thumbWidth, thumbHeight, BmpRowOrder::TopDown); + thumbBmp.write(reinterpret_cast(&bmpHeader), sizeof(bmpHeader)); - // File header - thumbBmp.write('B'); - thumbBmp.write('M'); - thumbBmp.write(reinterpret_cast(&fileSize), 4); - uint32_t reserved = 0; - thumbBmp.write(reinterpret_cast(&reserved), 4); - uint32_t dataOffset = 14 + 40 + 8; // 1-bit palette has 2 colors (8 bytes) - thumbBmp.write(reinterpret_cast(&dataOffset), 4); - - // DIB header - uint32_t dibHeaderSize = 40; - thumbBmp.write(reinterpret_cast(&dibHeaderSize), 4); - int32_t widthVal = thumbWidth; - thumbBmp.write(reinterpret_cast(&widthVal), 4); - int32_t heightVal = -static_cast(thumbHeight); // Negative for top-down - thumbBmp.write(reinterpret_cast(&heightVal), 4); - uint16_t planes = 1; - thumbBmp.write(reinterpret_cast(&planes), 2); - uint16_t bitsPerPixel = 1; // 1-bit for black and white - thumbBmp.write(reinterpret_cast(&bitsPerPixel), 2); - uint32_t compression = 0; - thumbBmp.write(reinterpret_cast(&compression), 4); - thumbBmp.write(reinterpret_cast(&imageSize), 4); - int32_t ppmX = 2835; - thumbBmp.write(reinterpret_cast(&ppmX), 4); - int32_t ppmY = 2835; - thumbBmp.write(reinterpret_cast(&ppmY), 4); - uint32_t colorsUsed = 2; - thumbBmp.write(reinterpret_cast(&colorsUsed), 4); - uint32_t colorsImportant = 2; - thumbBmp.write(reinterpret_cast(&colorsImportant), 4); - - // Color palette (2 colors for 1-bit: black and white) - uint8_t palette[8] = { - 0x00, 0x00, 0x00, 0x00, // Color 0: Black - 0xFF, 0xFF, 0xFF, 0x00 // Color 1: White - }; - thumbBmp.write(palette, 8); + const uint32_t rowSize = (thumbWidth + 31) / 32 * 4; // Allocate row buffer for 1-bit output uint8_t* rowBuffer = static_cast(malloc(rowSize)); if (!rowBuffer) { free(pageBuffer); - thumbBmp.close(); return false; } @@ -555,7 +474,6 @@ bool Xtc::generateThumbBmp(int height) const { } free(rowBuffer); - thumbBmp.close(); free(pageBuffer); LOG_DBG("XTC", "Generated thumb BMP (%dx%d): %s", thumbWidth, thumbHeight, getThumbBmpPath(height).c_str()); diff --git a/lib/Xtc/Xtc/XtcParser.cpp b/lib/Xtc/Xtc/XtcParser.cpp index 12e8a61db..183afea92 100644 --- a/lib/Xtc/Xtc/XtcParser.cpp +++ b/lib/Xtc/Xtc/XtcParser.cpp @@ -43,6 +43,7 @@ XtcError XtcParser::open(const char* filepath) { m_lastError = readHeader(); if (m_lastError != XtcError::OK) { LOG_DBG("XTC", "Failed to read header: %s", errorToString(m_lastError)); + // Explicit close() required: member variable persists beyond function scope m_file.close(); return m_lastError; } @@ -52,12 +53,14 @@ XtcError XtcParser::open(const char* filepath) { m_lastError = readTitle(); if (m_lastError != XtcError::OK) { LOG_DBG("XTC", "Failed to read title: %s", errorToString(m_lastError)); + // Explicit close() required: member variable persists beyond function scope m_file.close(); return m_lastError; } m_lastError = readAuthor(); if (m_lastError != XtcError::OK) { LOG_DBG("XTC", "Failed to read author: %s", errorToString(m_lastError)); + // Explicit close() required: member variable persists beyond function scope m_file.close(); return m_lastError; } @@ -67,6 +70,7 @@ XtcError XtcParser::open(const char* filepath) { m_lastError = readPageTable(); if (m_lastError != XtcError::OK) { LOG_DBG("XTC", "Failed to read page table: %s", errorToString(m_lastError)); + // Explicit close() required: member variable persists beyond function scope m_file.close(); return m_lastError; } @@ -75,6 +79,7 @@ XtcError XtcParser::open(const char* filepath) { m_lastError = readChapters(); if (m_lastError != XtcError::OK) { LOG_DBG("XTC", "Failed to read chapters: %s", errorToString(m_lastError)); + // Explicit close() required: member variable persists beyond function scope m_file.close(); return m_lastError; } @@ -86,6 +91,7 @@ XtcError XtcParser::open(const char* filepath) { void XtcParser::close() { if (m_isOpen) { + // Explicit close() required: member variable persists beyond function scope m_file.close(); m_isOpen = false; } diff --git a/lib/ZipFile/ZipFile.cpp b/lib/ZipFile/ZipFile.cpp index 58fa64d5a..fe59dfaa0 100644 --- a/lib/ZipFile/ZipFile.cpp +++ b/lib/ZipFile/ZipFile.cpp @@ -18,6 +18,28 @@ namespace { constexpr uint16_t ZIP_METHOD_STORED = 0; constexpr uint16_t ZIP_METHOD_DEFLATED = 8; +// RAII zip: opens the zip if not already open, closes on destruction only if +// it performed the open. Removes the wasOpen/close boilerplate from every method. +class ScopedOpenClose final { + public: + [[nodiscard]] explicit ScopedOpenClose(ZipFile& zf) : zf(zf), needsClose(!zf.isOpen()) { + if (needsClose) ok = zf.open(); + } + ~ScopedOpenClose() { + if (needsClose && ok) zf.close(); + } + ScopedOpenClose(const ScopedOpenClose&) = delete; + ScopedOpenClose& operator=(const ScopedOpenClose&) = delete; + ScopedOpenClose(ScopedOpenClose&&) = delete; + ScopedOpenClose& operator=(ScopedOpenClose&&) = delete; + explicit operator bool() const { return ok || !needsClose; } + + private: + ZipFile& zf; + bool needsClose = false; + bool ok = true; // true when zip was already open (no open() call needed) +}; + int zipReadCallback(uzlib_uncomp* uncomp) { auto* ctx = reinterpret_cast(uncomp); if (ctx->fileRemaining == 0) return -1; @@ -35,17 +57,10 @@ int zipReadCallback(uzlib_uncomp* uncomp) { } // namespace bool ZipFile::loadAllFileStatSlims() { - const bool wasOpen = isOpen(); - if (!wasOpen && !open()) { - return false; - } + const ScopedOpenClose zip{*this}; + if (!zip) return false; - if (!loadZipDetails()) { - if (!wasOpen) { - close(); - } - return false; - } + if (!loadZipDetails()) return false; file.seek(zipDetails.centralDirOffset); @@ -89,9 +104,6 @@ bool ZipFile::loadAllFileStatSlims() { lastCentralDirPos = zipDetails.centralDirOffset; lastCentralDirPosValid = true; - if (!wasOpen) { - close(); - } return true; } @@ -105,17 +117,10 @@ bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) { return false; } - const bool wasOpen = isOpen(); - if (!wasOpen && !open()) { - return false; - } + const ScopedOpenClose zip{*this}; + if (!zip) return false; - if (!loadZipDetails()) { - if (!wasOpen) { - close(); - } - return false; - } + if (!loadZipDetails()) return false; // Phase 1: Try scanning from cursor position first uint32_t startPos = lastCentralDirPosValid ? lastCentralDirPos : zipDetails.centralDirOffset; @@ -179,17 +184,12 @@ bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) { file.seekCur(m + k); } - if (!wasOpen) { - close(); - } return found; } long ZipFile::getDataOffset(const FileStatSlim& fileStat) { - const bool wasOpen = isOpen(); - if (!wasOpen && !open()) { - return -1; - } + const ScopedOpenClose zip{*this}; + if (!zip) return -1; constexpr auto localHeaderSize = 30; @@ -198,9 +198,6 @@ long ZipFile::getDataOffset(const FileStatSlim& fileStat) { file.seek(fileOffset); const size_t read = file.read(pLocalHeader, localHeaderSize); - if (!wasOpen) { - close(); - } if (read != localHeaderSize) { LOG_ERR("ZIP", "Something went wrong reading the local header"); @@ -223,17 +220,12 @@ bool ZipFile::loadZipDetails() { return true; } - const bool wasOpen = isOpen(); - if (!wasOpen && !open()) { - return false; - } + const ScopedOpenClose zip{*this}; + if (!zip) return false; const size_t fileSize = file.size(); if (fileSize < 22) { LOG_ERR("ZIP", "File too small to be a valid zip"); - if (!wasOpen) { - close(); - } return false; // Minimum EOCD size is 22 bytes } @@ -243,9 +235,6 @@ bool ZipFile::loadZipDetails() { const auto buffer = static_cast(malloc(scanRange)); if (!buffer) { LOG_ERR("ZIP", "Failed to allocate memory for EOCD scan buffer"); - if (!wasOpen) { - close(); - } return false; } @@ -265,9 +254,6 @@ bool ZipFile::loadZipDetails() { if (foundOffset == -1) { LOG_ERR("ZIP", "EOCD signature not found in zip file"); free(buffer); - if (!wasOpen) { - close(); - } return false; } @@ -280,9 +266,6 @@ bool ZipFile::loadZipDetails() { zipDetails.isSet = true; free(buffer); - if (!wasOpen) { - close(); - } return true; } @@ -295,6 +278,7 @@ bool ZipFile::open() { bool ZipFile::close() { if (file) { + // Explicit close() required: member variable persists beyond function scope file.close(); } lastCentralDirPos = 0; @@ -312,22 +296,15 @@ bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) { return true; } -int ZipFile::fillUncompressedSizes(std::vector& targets, std::vector& sizes) { +int ZipFile::fillUncompressedSizes(std::deque& targets, std::deque& sizes) { if (targets.empty()) { return 0; } - const bool wasOpen = isOpen(); - if (!wasOpen && !open()) { - return 0; - } + const ScopedOpenClose zip{*this}; + if (!zip) return 0; - if (!loadZipDetails()) { - if (!wasOpen) { - close(); - } - return 0; - } + if (!loadZipDetails()) return 0; file.seek(zipDetails.centralDirOffset); @@ -384,34 +361,18 @@ int ZipFile::fillUncompressedSizes(std::vector& targets, std::vector file.seekCur(m + k); } - if (!wasOpen) { - close(); - } - return matched; } uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) { - const bool wasOpen = isOpen(); - if (!wasOpen && !open()) { - return nullptr; - } + const ScopedOpenClose zip{*this}; + if (!zip) return nullptr; FileStatSlim fileStat = {}; - if (!loadFileStatSlim(filename, &fileStat)) { - if (!wasOpen) { - close(); - } - return nullptr; - } + if (!loadFileStatSlim(filename, &fileStat)) return nullptr; const long fileOffset = getDataOffset(fileStat); - if (fileOffset < 0) { - if (!wasOpen) { - close(); - } - return nullptr; - } + if (fileOffset < 0) return nullptr; file.seek(fileOffset); @@ -421,18 +382,12 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo const auto data = static_cast(malloc(dataSize)); if (data == nullptr) { LOG_ERR("ZIP", "Failed to allocate memory for output buffer (%zu bytes)", dataSize); - if (!wasOpen) { - close(); - } return nullptr; } if (fileStat.method == ZIP_METHOD_STORED) { // no deflation, just read content const size_t dataRead = file.read(data, inflatedDataSize); - if (!wasOpen) { - close(); - } if (dataRead != inflatedDataSize) { LOG_ERR("ZIP", "Failed to read data"); @@ -446,16 +401,11 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo const auto deflatedData = static_cast(malloc(deflatedDataSize)); if (deflatedData == nullptr) { LOG_ERR("ZIP", "Failed to allocate memory for decompression buffer"); - if (!wasOpen) { - close(); - } + free(data); return nullptr; } const size_t dataRead = file.read(deflatedData, deflatedDataSize); - if (!wasOpen) { - close(); - } if (dataRead != deflatedDataSize) { LOG_ERR("ZIP", "Failed to read data, expected %d got %d", deflatedDataSize, dataRead); @@ -482,9 +432,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo // Continue out of block with data set } else { LOG_ERR("ZIP", "Unsupported compression method"); - if (!wasOpen) { - close(); - } + free(data); return nullptr; } @@ -494,20 +442,14 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo } bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize) { - const bool wasOpen = isOpen(); - if (!wasOpen && !open()) { - return false; - } + const ScopedOpenClose zip{*this}; + if (!zip) return false; FileStatSlim fileStat = {}; - if (!loadFileStatSlim(filename, &fileStat)) { - return false; - } + if (!loadFileStatSlim(filename, &fileStat)) return false; const long fileOffset = getDataOffset(fileStat); - if (fileOffset < 0) { - return false; - } + if (fileOffset < 0) return false; file.seek(fileOffset); const auto deflatedDataSize = fileStat.compressedSize; @@ -518,9 +460,6 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch const auto buffer = static_cast(malloc(chunkSize)); if (!buffer) { LOG_ERR("ZIP", "Failed to allocate memory for buffer"); - if (!wasOpen) { - close(); - } return false; } @@ -530,19 +469,17 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch if (dataRead == 0) { LOG_ERR("ZIP", "Could not read more bytes"); free(buffer); - if (!wasOpen) { - close(); - } return false; } - out.write(buffer, dataRead); + if (out.write(buffer, dataRead) != dataRead) { + LOG_ERR("ZIP", "Failed to write all output bytes to stream"); + free(buffer); + return false; + } remaining -= dataRead; } - if (!wasOpen) { - close(); - } free(buffer); return true; } @@ -551,9 +488,6 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch auto* fileReadBuffer = static_cast(malloc(chunkSize)); if (!fileReadBuffer) { LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer"); - if (!wasOpen) { - close(); - } return false; } @@ -561,9 +495,6 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch if (!outputBuffer) { LOG_ERR("ZIP", "Failed to allocate memory for output buffer"); free(fileReadBuffer); - if (!wasOpen) { - close(); - } return false; } @@ -577,9 +508,6 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch LOG_ERR("ZIP", "Failed to init inflate reader"); free(outputBuffer); free(fileReadBuffer); - if (!wasOpen) { - close(); - } return false; } ctx.reader.setReadCallback(zipReadCallback); @@ -623,18 +551,11 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch // InflateStatus::Ok: output buffer full, continue } - if (!wasOpen) { - close(); - } free(outputBuffer); free(fileReadBuffer); return success; // ctx.reader destructor frees the ring buffer } - if (!wasOpen) { - close(); - } - LOG_ERR("ZIP", "Unsupported compression method"); return false; } diff --git a/lib/ZipFile/ZipFile.h b/lib/ZipFile/ZipFile.h index bc97559dd..60c97a4cf 100644 --- a/lib/ZipFile/ZipFile.h +++ b/lib/ZipFile/ZipFile.h @@ -1,9 +1,9 @@ #pragma once #include +#include #include #include -#include class ZipFile { public: @@ -64,7 +64,7 @@ class ZipFile { // Batch lookup: scan ZIP central dir once and fill sizes for matching targets. // targets must be sorted by (hash, len). sizes[target.index] receives uncompressedSize. // Returns number of targets matched. - int fillUncompressedSizes(std::vector& targets, std::vector& sizes); + int fillUncompressedSizes(std::deque& targets, std::deque& sizes); // Due to the memory required to run each of these, it is recommended to not preopen the zip file for multiple // These functions will open and close the zip as needed uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false); diff --git a/lib/picojpeg/picojpeg.c b/lib/picojpeg/picojpeg.c deleted file mode 100644 index f612b73c0..000000000 --- a/lib/picojpeg/picojpeg.c +++ /dev/null @@ -1,2087 +0,0 @@ -//------------------------------------------------------------------------------ -// picojpeg.c v1.1 - Public domain, Rich Geldreich -// Nov. 27, 2010 - Initial release -// Feb. 9, 2013 - Added H1V2/H2V1 support, cleaned up macros, signed shift fixes -// Also integrated and tested changes from Chris Phoenix . -//------------------------------------------------------------------------------ -#include "picojpeg.h" -//------------------------------------------------------------------------------ -// Set to 1 if right shifts on signed ints are always unsigned (logical) shifts -// When 1, arithmetic right shifts will be emulated by using a logical shift -// with special case code to ensure the sign bit is replicated. -#define PJPG_RIGHT_SHIFT_IS_ALWAYS_UNSIGNED 0 - -// Define PJPG_INLINE to "inline" if your C compiler supports explicit inlining -#define PJPG_INLINE -//------------------------------------------------------------------------------ -typedef unsigned char uint8; -typedef unsigned short uint16; -typedef signed char int8; -typedef signed short int16; -//------------------------------------------------------------------------------ -#if PJPG_RIGHT_SHIFT_IS_ALWAYS_UNSIGNED -static int16 replicateSignBit16(int8 n) { - switch (n) { - case 0: - return 0x0000; - case 1: - return 0x8000; - case 2: - return 0xC000; - case 3: - return 0xE000; - case 4: - return 0xF000; - case 5: - return 0xF800; - case 6: - return 0xFC00; - case 7: - return 0xFE00; - case 8: - return 0xFF00; - case 9: - return 0xFF80; - case 10: - return 0xFFC0; - case 11: - return 0xFFE0; - case 12: - return 0xFFF0; - case 13: - return 0xFFF8; - case 14: - return 0xFFFC; - case 15: - return 0xFFFE; - default: - return 0xFFFF; - } -} -static PJPG_INLINE int16 arithmeticRightShiftN16(int16 x, int8 n) { - int16 r = (uint16)x >> (uint8)n; - if (x < 0) r |= replicateSignBit16(n); - return r; -} -static PJPG_INLINE long arithmeticRightShift8L(long x) { - long r = (unsigned long)x >> 8U; - if (x < 0) r |= ~(~(unsigned long)0U >> 8U); - return r; -} -#define PJPG_ARITH_SHIFT_RIGHT_N_16(x, n) arithmeticRightShiftN16(x, n) -#define PJPG_ARITH_SHIFT_RIGHT_8_L(x) arithmeticRightShift8L(x) -#else -#define PJPG_ARITH_SHIFT_RIGHT_N_16(x, n) ((x) >> (n)) -#define PJPG_ARITH_SHIFT_RIGHT_8_L(x) ((x) >> 8) -#endif -//------------------------------------------------------------------------------ -// Change as needed - the PJPG_MAX_WIDTH/PJPG_MAX_HEIGHT checks are only present -// to quickly detect bogus files. -#define PJPG_MAX_WIDTH 16384 -#define PJPG_MAX_HEIGHT 16384 -#define PJPG_MAXCOMPSINSCAN 3 -//------------------------------------------------------------------------------ -typedef enum { - M_SOF0 = 0xC0, - M_SOF1 = 0xC1, - M_SOF2 = 0xC2, - M_SOF3 = 0xC3, - - M_SOF5 = 0xC5, - M_SOF6 = 0xC6, - M_SOF7 = 0xC7, - - M_JPG = 0xC8, - M_SOF9 = 0xC9, - M_SOF10 = 0xCA, - M_SOF11 = 0xCB, - - M_SOF13 = 0xCD, - M_SOF14 = 0xCE, - M_SOF15 = 0xCF, - - M_DHT = 0xC4, - - M_DAC = 0xCC, - - M_RST0 = 0xD0, - M_RST1 = 0xD1, - M_RST2 = 0xD2, - M_RST3 = 0xD3, - M_RST4 = 0xD4, - M_RST5 = 0xD5, - M_RST6 = 0xD6, - M_RST7 = 0xD7, - - M_SOI = 0xD8, - M_EOI = 0xD9, - M_SOS = 0xDA, - M_DQT = 0xDB, - M_DNL = 0xDC, - M_DRI = 0xDD, - M_DHP = 0xDE, - M_EXP = 0xDF, - - M_APP0 = 0xE0, - M_APP15 = 0xEF, - - M_JPG0 = 0xF0, - M_JPG13 = 0xFD, - M_COM = 0xFE, - - M_TEM = 0x01, - - M_ERROR = 0x100, - - RST0 = 0xD0 -} JPEG_MARKER; -//------------------------------------------------------------------------------ -static const int8 ZAG[] = { - 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, - 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, - 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, -}; -//------------------------------------------------------------------------------ -// 128 bytes -static int16 gCoeffBuf[8 * 8]; - -// 8*8*4 bytes * 3 = 768 -static uint8 gMCUBufR[256]; -static uint8 gMCUBufG[256]; -static uint8 gMCUBufB[256]; - -// 256 bytes -static int16 gQuant0[8 * 8]; -static int16 gQuant1[8 * 8]; - -// 6 bytes -static int16 gLastDC[3]; - -typedef struct HuffTableT { - uint16 mMinCode[16]; - uint16 mMaxCode[16]; - uint8 mValPtr[16]; -} HuffTable; - -// DC - 192 -static HuffTable gHuffTab0; - -static uint8 gHuffVal0[16]; - -static HuffTable gHuffTab1; -static uint8 gHuffVal1[16]; - -// AC - 672 -static HuffTable gHuffTab2; -static uint8 gHuffVal2[256]; - -static HuffTable gHuffTab3; -static uint8 gHuffVal3[256]; - -static uint8 gValidHuffTables; -static uint8 gValidQuantTables; - -static uint8 gTemFlag; -#define PJPG_MAX_IN_BUF_SIZE 256 -static uint8 gInBuf[PJPG_MAX_IN_BUF_SIZE]; -static uint8 gInBufOfs; -static uint8 gInBufLeft; - -static uint16 gBitBuf; -static uint8 gBitsLeft; -//------------------------------------------------------------------------------ -static uint16 gImageXSize; -static uint16 gImageYSize; -static uint8 gCompsInFrame; -static uint8 gCompIdent[3]; -static uint8 gCompHSamp[3]; -static uint8 gCompVSamp[3]; -static uint8 gCompQuant[3]; - -static uint16 gRestartInterval; -static uint16 gNextRestartNum; -static uint16 gRestartsLeft; - -static uint8 gCompsInScan; -static uint8 gCompList[3]; -static uint8 gCompDCTab[3]; // 0,1 -static uint8 gCompACTab[3]; // 0,1 - -static pjpeg_scan_type_t gScanType; - -static uint8 gMaxBlocksPerMCU; -static uint8 gMaxMCUXSize; -static uint8 gMaxMCUYSize; -static uint16 gMaxMCUSPerRow; -static uint16 gMaxMCUSPerCol; - -static uint16 gNumMCUSRemainingX, gNumMCUSRemainingY; - -static uint8 gMCUOrg[6]; - -static pjpeg_need_bytes_callback_t g_pNeedBytesCallback; -static void* g_pCallback_data; -static uint8 gCallbackStatus; -static uint8 gReduce; -//------------------------------------------------------------------------------ -static void fillInBuf(void) { - unsigned char status; - - // Reserve a few bytes at the beginning of the buffer for putting back ("stuffing") chars. - gInBufOfs = 4; - gInBufLeft = 0; - - status = (*g_pNeedBytesCallback)(gInBuf + gInBufOfs, PJPG_MAX_IN_BUF_SIZE - gInBufOfs, &gInBufLeft, g_pCallback_data); - if (status) { - // The user provided need bytes callback has indicated an error, so record the error and continue trying to decode. - // The highest level pjpeg entrypoints will catch the error and return the non-zero status. - gCallbackStatus = status; - } -} -//------------------------------------------------------------------------------ -static PJPG_INLINE uint8 getChar(void) { - if (!gInBufLeft) { - fillInBuf(); - if (!gInBufLeft) { - gTemFlag = ~gTemFlag; - return gTemFlag ? 0xFF : 0xD9; - } - } - - gInBufLeft--; - return gInBuf[gInBufOfs++]; -} -//------------------------------------------------------------------------------ -static PJPG_INLINE void stuffChar(uint8 i) { - gInBufOfs--; - gInBuf[gInBufOfs] = i; - gInBufLeft++; -} -//------------------------------------------------------------------------------ -static PJPG_INLINE uint8 getOctet(uint8 FFCheck) { - uint8 c = getChar(); - - if ((FFCheck) && (c == 0xFF)) { - uint8 n = getChar(); - - if (n) { - stuffChar(n); - stuffChar(0xFF); - } - } - - return c; -} -//------------------------------------------------------------------------------ -static uint16 getBits(uint8 numBits, uint8 FFCheck) { - uint8 origBits = numBits; - uint16 ret = gBitBuf; - - if (numBits > 8) { - numBits -= 8; - - gBitBuf <<= gBitsLeft; - - gBitBuf |= getOctet(FFCheck); - - gBitBuf <<= (8 - gBitsLeft); - - ret = (ret & 0xFF00) | (gBitBuf >> 8); - } - - if (gBitsLeft < numBits) { - gBitBuf <<= gBitsLeft; - - gBitBuf |= getOctet(FFCheck); - - gBitBuf <<= (numBits - gBitsLeft); - - gBitsLeft = 8 - (numBits - gBitsLeft); - } else { - gBitsLeft = (uint8)(gBitsLeft - numBits); - gBitBuf <<= numBits; - } - - return ret >> (16 - origBits); -} -//------------------------------------------------------------------------------ -static PJPG_INLINE uint16 getBits1(uint8 numBits) { return getBits(numBits, 0); } -//------------------------------------------------------------------------------ -static PJPG_INLINE uint16 getBits2(uint8 numBits) { return getBits(numBits, 1); } -//------------------------------------------------------------------------------ -static PJPG_INLINE uint8 getBit(void) { - uint8 ret = 0; - if (gBitBuf & 0x8000) ret = 1; - - if (!gBitsLeft) { - gBitBuf |= getOctet(1); - - gBitsLeft += 8; - } - - gBitsLeft--; - gBitBuf <<= 1; - - return ret; -} -//------------------------------------------------------------------------------ -static uint16 getExtendTest(uint8 i) { - switch (i) { - case 0: - return 0; - case 1: - return 0x0001; - case 2: - return 0x0002; - case 3: - return 0x0004; - case 4: - return 0x0008; - case 5: - return 0x0010; - case 6: - return 0x0020; - case 7: - return 0x0040; - case 8: - return 0x0080; - case 9: - return 0x0100; - case 10: - return 0x0200; - case 11: - return 0x0400; - case 12: - return 0x0800; - case 13: - return 0x1000; - case 14: - return 0x2000; - case 15: - return 0x4000; - default: - return 0; - } -} -//------------------------------------------------------------------------------ -static int16 getExtendOffset(uint8 i) { - switch (i) { - case 0: - return 0; - case 1: - return ((-1) << 1) + 1; - case 2: - return ((-1) << 2) + 1; - case 3: - return ((-1) << 3) + 1; - case 4: - return ((-1) << 4) + 1; - case 5: - return ((-1) << 5) + 1; - case 6: - return ((-1) << 6) + 1; - case 7: - return ((-1) << 7) + 1; - case 8: - return ((-1) << 8) + 1; - case 9: - return ((-1) << 9) + 1; - case 10: - return ((-1) << 10) + 1; - case 11: - return ((-1) << 11) + 1; - case 12: - return ((-1) << 12) + 1; - case 13: - return ((-1) << 13) + 1; - case 14: - return ((-1) << 14) + 1; - case 15: - return ((-1) << 15) + 1; - default: - return 0; - } -}; -//------------------------------------------------------------------------------ -static PJPG_INLINE int16 huffExtend(uint16 x, uint8 s) { - return ((x < getExtendTest(s)) ? ((int16)x + getExtendOffset(s)) : (int16)x); -} -//------------------------------------------------------------------------------ -static PJPG_INLINE uint8 huffDecode(const HuffTable* pHuffTable, const uint8* pHuffVal) { - uint8 i = 0; - uint8 j; - uint16 code = getBit(); - - // This func only reads a bit at a time, which on modern CPU's is not terribly efficient. - // But on microcontrollers without strong integer shifting support this seems like a - // more reasonable approach. - for (;;) { - uint16 maxCode; - - if (i == 16) return 0; - - maxCode = pHuffTable->mMaxCode[i]; - if ((code <= maxCode) && (maxCode != 0xFFFF)) break; - - i++; - code <<= 1; - code |= getBit(); - } - - j = pHuffTable->mValPtr[i]; - j = (uint8)(j + (code - pHuffTable->mMinCode[i])); - - return pHuffVal[j]; -} -//------------------------------------------------------------------------------ -static void huffCreate(const uint8* pBits, HuffTable* pHuffTable) { - uint8 i = 0; - uint8 j = 0; - - uint16 code = 0; - - for (;;) { - uint8 num = pBits[i]; - - if (!num) { - pHuffTable->mMinCode[i] = 0x0000; - pHuffTable->mMaxCode[i] = 0xFFFF; - pHuffTable->mValPtr[i] = 0; - } else { - pHuffTable->mMinCode[i] = code; - pHuffTable->mMaxCode[i] = code + num - 1; - pHuffTable->mValPtr[i] = j; - - j = (uint8)(j + num); - - code = (uint16)(code + num); - } - - code <<= 1; - - i++; - if (i > 15) break; - } -} -//------------------------------------------------------------------------------ -static HuffTable* getHuffTable(uint8 index) { - // 0-1 = DC - // 2-3 = AC - switch (index) { - case 0: - return &gHuffTab0; - case 1: - return &gHuffTab1; - case 2: - return &gHuffTab2; - case 3: - return &gHuffTab3; - default: - return 0; - } -} -//------------------------------------------------------------------------------ -static uint8* getHuffVal(uint8 index) { - // 0-1 = DC - // 2-3 = AC - switch (index) { - case 0: - return gHuffVal0; - case 1: - return gHuffVal1; - case 2: - return gHuffVal2; - case 3: - return gHuffVal3; - default: - return 0; - } -} -//------------------------------------------------------------------------------ -static uint16 getMaxHuffCodes(uint8 index) { return (index < 2) ? 12 : 255; } -//------------------------------------------------------------------------------ -static uint8 readDHTMarker(void) { - uint8 bits[16]; - uint16 left = getBits1(16); - - if (left < 2) return PJPG_BAD_DHT_MARKER; - - left -= 2; - - while (left) { - uint8 i, tableIndex, index; - uint8* pHuffVal; - HuffTable* pHuffTable; - uint16 count, totalRead; - - index = (uint8)getBits1(8); - - if (((index & 0xF) > 1) || ((index & 0xF0) > 0x10)) return PJPG_BAD_DHT_INDEX; - - tableIndex = ((index >> 3) & 2) + (index & 1); - - pHuffTable = getHuffTable(tableIndex); - pHuffVal = getHuffVal(tableIndex); - - gValidHuffTables |= (1 << tableIndex); - - count = 0; - for (i = 0; i <= 15; i++) { - uint8 n = (uint8)getBits1(8); - bits[i] = n; - count = (uint16)(count + n); - } - - if (count > getMaxHuffCodes(tableIndex)) return PJPG_BAD_DHT_COUNTS; - - for (i = 0; i < count; i++) pHuffVal[i] = (uint8)getBits1(8); - - totalRead = 1 + 16 + count; - - if (left < totalRead) return PJPG_BAD_DHT_MARKER; - - left = (uint16)(left - totalRead); - - huffCreate(bits, pHuffTable); - } - - return 0; -} -//------------------------------------------------------------------------------ -static void createWinogradQuant(int16* pQuant); - -static uint8 readDQTMarker(void) { - uint16 left = getBits1(16); - - if (left < 2) return PJPG_BAD_DQT_MARKER; - - left -= 2; - - while (left) { - uint8 i; - uint8 n = (uint8)getBits1(8); - uint8 prec = n >> 4; - uint16 totalRead; - - n &= 0x0F; - - if (n > 1) return PJPG_BAD_DQT_TABLE; - - gValidQuantTables |= (n ? 2 : 1); - - // read quantization entries, in zag order - for (i = 0; i < 64; i++) { - uint16 temp = getBits1(8); - - if (prec) temp = (temp << 8) + getBits1(8); - - if (n) - gQuant1[i] = (int16)temp; - else - gQuant0[i] = (int16)temp; - } - - createWinogradQuant(n ? gQuant1 : gQuant0); - - totalRead = 64 + 1; - - if (prec) totalRead += 64; - - if (left < totalRead) return PJPG_BAD_DQT_LENGTH; - - left = (uint16)(left - totalRead); - } - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 readSOFMarker(void) { - uint8 i; - uint16 left = getBits1(16); - - if (getBits1(8) != 8) return PJPG_BAD_PRECISION; - - gImageYSize = getBits1(16); - - if ((!gImageYSize) || (gImageYSize > PJPG_MAX_HEIGHT)) return PJPG_BAD_HEIGHT; - - gImageXSize = getBits1(16); - - if ((!gImageXSize) || (gImageXSize > PJPG_MAX_WIDTH)) return PJPG_BAD_WIDTH; - - gCompsInFrame = (uint8)getBits1(8); - - if (gCompsInFrame > 3) return PJPG_TOO_MANY_COMPONENTS; - - if (left != (gCompsInFrame + gCompsInFrame + gCompsInFrame + 8)) return PJPG_BAD_SOF_LENGTH; - - for (i = 0; i < gCompsInFrame; i++) { - gCompIdent[i] = (uint8)getBits1(8); - gCompHSamp[i] = (uint8)getBits1(4); - gCompVSamp[i] = (uint8)getBits1(4); - gCompQuant[i] = (uint8)getBits1(8); - - if (gCompQuant[i] > 1) return PJPG_UNSUPPORTED_QUANT_TABLE; - } - - return 0; -} -//------------------------------------------------------------------------------ -// Used to skip unrecognized markers. -static uint8 skipVariableMarker(void) { - uint16 left = getBits1(16); - - if (left < 2) return PJPG_BAD_VARIABLE_MARKER; - - left -= 2; - - while (left) { - getBits1(8); - left--; - } - - return 0; -} -//------------------------------------------------------------------------------ -// Read a define restart interval (DRI) marker. -static uint8 readDRIMarker(void) { - if (getBits1(16) != 4) return PJPG_BAD_DRI_LENGTH; - - gRestartInterval = getBits1(16); - - return 0; -} -//------------------------------------------------------------------------------ -// Read a start of scan (SOS) marker. -static uint8 readSOSMarker(void) { - uint8 i; - uint16 left = getBits1(16); - uint8 spectral_start, spectral_end, successive_high, successive_low; - - gCompsInScan = (uint8)getBits1(8); - - left -= 3; - - if ((left != (gCompsInScan + gCompsInScan + 3)) || (gCompsInScan < 1) || (gCompsInScan > PJPG_MAXCOMPSINSCAN)) - return PJPG_BAD_SOS_LENGTH; - - for (i = 0; i < gCompsInScan; i++) { - uint8 cc = (uint8)getBits1(8); - uint8 c = (uint8)getBits1(8); - uint8 ci; - - left -= 2; - - for (ci = 0; ci < gCompsInFrame; ci++) - if (cc == gCompIdent[ci]) break; - - if (ci >= gCompsInFrame) return PJPG_BAD_SOS_COMP_ID; - - gCompList[i] = ci; - gCompDCTab[ci] = (c >> 4) & 15; - gCompACTab[ci] = (c & 15); - } - - spectral_start = (uint8)getBits1(8); - spectral_end = (uint8)getBits1(8); - successive_high = (uint8)getBits1(4); - successive_low = (uint8)getBits1(4); - - left -= 3; - - while (left) { - getBits1(8); - left--; - } - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 nextMarker(void) { - uint8 c; - uint8 bytes = 0; - - do { - do { - bytes++; - - c = (uint8)getBits1(8); - - } while (c != 0xFF); - - do { - c = (uint8)getBits1(8); - - } while (c == 0xFF); - - } while (c == 0); - - // If bytes > 0 here, there where extra bytes before the marker (not good). - - return c; -} -//------------------------------------------------------------------------------ -// Process markers. Returns when an SOFx, SOI, EOI, or SOS marker is -// encountered. -static uint8 processMarkers(uint8* pMarker) { - for (;;) { - uint8 c = nextMarker(); - - switch (c) { - case M_SOF0: - case M_SOF1: - case M_SOF2: - case M_SOF3: - case M_SOF5: - case M_SOF6: - case M_SOF7: - // case M_JPG: - case M_SOF9: - case M_SOF10: - case M_SOF11: - case M_SOF13: - case M_SOF14: - case M_SOF15: - case M_SOI: - case M_EOI: - case M_SOS: { - *pMarker = c; - return 0; - } - case M_DHT: { - readDHTMarker(); - break; - } - // Sorry, no arithmetic support at this time. Dumb patents! - case M_DAC: { - return PJPG_NO_ARITHMITIC_SUPPORT; - } - case M_DQT: { - readDQTMarker(); - break; - } - case M_DRI: { - readDRIMarker(); - break; - } - // case M_APP0: /* no need to read the JFIF marker */ - - case M_JPG: - case M_RST0: /* no parameters */ - case M_RST1: - case M_RST2: - case M_RST3: - case M_RST4: - case M_RST5: - case M_RST6: - case M_RST7: - case M_TEM: { - return PJPG_UNEXPECTED_MARKER; - } - default: /* must be DNL, DHP, EXP, APPn, JPGn, COM, or RESn or APP0 */ - { - skipVariableMarker(); - break; - } - } - } - // return 0; -} -//------------------------------------------------------------------------------ -// Finds the start of image (SOI) marker. -static uint8 locateSOIMarker(void) { - uint16 bytesleft; - - uint8 lastchar = (uint8)getBits1(8); - - uint8 thischar = (uint8)getBits1(8); - - /* ok if it's a normal JPEG file without a special header */ - - if ((lastchar == 0xFF) && (thischar == M_SOI)) return 0; - - bytesleft = 4096; // 512; - - for (;;) { - if (--bytesleft == 0) return PJPG_NOT_JPEG; - - lastchar = thischar; - - thischar = (uint8)getBits1(8); - - if (lastchar == 0xFF) { - if (thischar == M_SOI) - break; - else if (thischar == M_EOI) // getBits1 will keep returning M_EOI if we read past the end - return PJPG_NOT_JPEG; - } - } - - /* Check the next character after marker: if it's not 0xFF, it can't - be the start of the next marker, so the file is bad */ - - thischar = (uint8)((gBitBuf >> 8) & 0xFF); - - if (thischar != 0xFF) return PJPG_NOT_JPEG; - - return 0; -} -//------------------------------------------------------------------------------ -// Find a start of frame (SOF) marker. -static uint8 locateSOFMarker(void) { - uint8 c; - - uint8 status = locateSOIMarker(); - if (status) return status; - - status = processMarkers(&c); - if (status) return status; - - switch (c) { - case M_SOF2: { - // Progressive JPEG - not supported by picojpeg (would require too - // much memory, or too many IDCT's for embedded systems). - return PJPG_UNSUPPORTED_MODE; - } - case M_SOF0: /* baseline DCT */ - { - status = readSOFMarker(); - if (status) return status; - - break; - } - case M_SOF9: { - return PJPG_NO_ARITHMITIC_SUPPORT; - } - case M_SOF1: /* extended sequential DCT */ - default: { - return PJPG_UNSUPPORTED_MARKER; - } - } - - return 0; -} -//------------------------------------------------------------------------------ -// Find a start of scan (SOS) marker. -static uint8 locateSOSMarker(uint8* pFoundEOI) { - uint8 c; - uint8 status; - - *pFoundEOI = 0; - - status = processMarkers(&c); - if (status) return status; - - if (c == M_EOI) { - *pFoundEOI = 1; - return 0; - } else if (c != M_SOS) - return PJPG_UNEXPECTED_MARKER; - - return readSOSMarker(); -} -//------------------------------------------------------------------------------ -static uint8 init(void) { - gImageXSize = 0; - gImageYSize = 0; - gCompsInFrame = 0; - gRestartInterval = 0; - gCompsInScan = 0; - gValidHuffTables = 0; - gValidQuantTables = 0; - gTemFlag = 0; - gInBufOfs = 0; - gInBufLeft = 0; - gBitBuf = 0; - gBitsLeft = 8; - - getBits1(8); - getBits1(8); - - return 0; -} -//------------------------------------------------------------------------------ -// This method throws back into the stream any bytes that where read -// into the bit buffer during initial marker scanning. -static void fixInBuffer(void) { - /* In case any 0xFF's where pulled into the buffer during marker scanning */ - - if (gBitsLeft > 0) stuffChar((uint8)gBitBuf); - - stuffChar((uint8)(gBitBuf >> 8)); - - gBitsLeft = 8; - getBits2(8); - getBits2(8); -} -//------------------------------------------------------------------------------ -// Restart interval processing. -static uint8 processRestart(void) { - // Let's scan a little bit to find the marker, but not _too_ far. - // 1536 is a "fudge factor" that determines how much to scan. - uint16 i; - uint8 c = 0; - - for (i = 1536; i > 0; i--) - if (getChar() == 0xFF) break; - - if (i == 0) return PJPG_BAD_RESTART_MARKER; - - for (; i > 0; i--) - if ((c = getChar()) != 0xFF) break; - - if (i == 0) return PJPG_BAD_RESTART_MARKER; - - // Is it the expected marker? If not, something bad happened. - if (c != (gNextRestartNum + M_RST0)) return PJPG_BAD_RESTART_MARKER; - - // Reset each component's DC prediction values. - gLastDC[0] = 0; - gLastDC[1] = 0; - gLastDC[2] = 0; - - gRestartsLeft = gRestartInterval; - - gNextRestartNum = (gNextRestartNum + 1) & 7; - - // Get the bit buffer going again - - gBitsLeft = 8; - getBits2(8); - getBits2(8); - - return 0; -} -//------------------------------------------------------------------------------ -// FIXME: findEOI() is not actually called at the end of the image -// (it's optional, and probably not needed on embedded devices) -static uint8 findEOI(void) { - uint8 c; - uint8 status; - - // Prime the bit buffer - gBitsLeft = 8; - getBits1(8); - getBits1(8); - - // The next marker _should_ be EOI - status = processMarkers(&c); - if (status) - return status; - else if (gCallbackStatus) - return gCallbackStatus; - - // gTotalBytesRead -= in_buf_left; - if (c != M_EOI) return PJPG_UNEXPECTED_MARKER; - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 checkHuffTables(void) { - uint8 i; - - for (i = 0; i < gCompsInScan; i++) { - uint8 compDCTab = gCompDCTab[gCompList[i]]; - uint8 compACTab = gCompACTab[gCompList[i]] + 2; - - if (((gValidHuffTables & (1 << compDCTab)) == 0) || ((gValidHuffTables & (1 << compACTab)) == 0)) - return PJPG_UNDEFINED_HUFF_TABLE; - } - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 checkQuantTables(void) { - uint8 i; - - for (i = 0; i < gCompsInScan; i++) { - uint8 compQuantMask = gCompQuant[gCompList[i]] ? 2 : 1; - - if ((gValidQuantTables & compQuantMask) == 0) return PJPG_UNDEFINED_QUANT_TABLE; - } - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 initScan(void) { - uint8 foundEOI; - uint8 status = locateSOSMarker(&foundEOI); - if (status) return status; - if (foundEOI) return PJPG_UNEXPECTED_MARKER; - - status = checkHuffTables(); - if (status) return status; - - status = checkQuantTables(); - if (status) return status; - - gLastDC[0] = 0; - gLastDC[1] = 0; - gLastDC[2] = 0; - - if (gRestartInterval) { - gRestartsLeft = gRestartInterval; - gNextRestartNum = 0; - } - - fixInBuffer(); - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 initFrame(void) { - if (gCompsInFrame == 1) { - if ((gCompHSamp[0] != 1) || (gCompVSamp[0] != 1)) return PJPG_UNSUPPORTED_SAMP_FACTORS; - - gScanType = PJPG_GRAYSCALE; - - gMaxBlocksPerMCU = 1; - gMCUOrg[0] = 0; - - gMaxMCUXSize = 8; - gMaxMCUYSize = 8; - } else if (gCompsInFrame == 3) { - if (((gCompHSamp[1] != 1) || (gCompVSamp[1] != 1)) || ((gCompHSamp[2] != 1) || (gCompVSamp[2] != 1))) - return PJPG_UNSUPPORTED_SAMP_FACTORS; - - if ((gCompHSamp[0] == 1) && (gCompVSamp[0] == 1)) { - gScanType = PJPG_YH1V1; - - gMaxBlocksPerMCU = 3; - gMCUOrg[0] = 0; - gMCUOrg[1] = 1; - gMCUOrg[2] = 2; - - gMaxMCUXSize = 8; - gMaxMCUYSize = 8; - } else if ((gCompHSamp[0] == 1) && (gCompVSamp[0] == 2)) { - gScanType = PJPG_YH1V2; - - gMaxBlocksPerMCU = 4; - gMCUOrg[0] = 0; - gMCUOrg[1] = 0; - gMCUOrg[2] = 1; - gMCUOrg[3] = 2; - - gMaxMCUXSize = 8; - gMaxMCUYSize = 16; - } else if ((gCompHSamp[0] == 2) && (gCompVSamp[0] == 1)) { - gScanType = PJPG_YH2V1; - - gMaxBlocksPerMCU = 4; - gMCUOrg[0] = 0; - gMCUOrg[1] = 0; - gMCUOrg[2] = 1; - gMCUOrg[3] = 2; - - gMaxMCUXSize = 16; - gMaxMCUYSize = 8; - } else if ((gCompHSamp[0] == 2) && (gCompVSamp[0] == 2)) { - gScanType = PJPG_YH2V2; - - gMaxBlocksPerMCU = 6; - gMCUOrg[0] = 0; - gMCUOrg[1] = 0; - gMCUOrg[2] = 0; - gMCUOrg[3] = 0; - gMCUOrg[4] = 1; - gMCUOrg[5] = 2; - - gMaxMCUXSize = 16; - gMaxMCUYSize = 16; - } else - return PJPG_UNSUPPORTED_SAMP_FACTORS; - } else - return PJPG_UNSUPPORTED_COLORSPACE; - - gMaxMCUSPerRow = (gImageXSize + (gMaxMCUXSize - 1)) >> ((gMaxMCUXSize == 8) ? 3 : 4); - gMaxMCUSPerCol = (gImageYSize + (gMaxMCUYSize - 1)) >> ((gMaxMCUYSize == 8) ? 3 : 4); - - // This can overflow on large JPEG's. - // gNumMCUSRemaining = gMaxMCUSPerRow * gMaxMCUSPerCol; - gNumMCUSRemainingX = gMaxMCUSPerRow; - gNumMCUSRemainingY = gMaxMCUSPerCol; - - return 0; -} -//---------------------------------------------------------------------------- -// Winograd IDCT: 5 multiplies per row/col, up to 80 muls for the 2D IDCT - -#define PJPG_DCT_SCALE_BITS 7 - -#define PJPG_DCT_SCALE (1U << PJPG_DCT_SCALE_BITS) - -#define PJPG_DESCALE(x) PJPG_ARITH_SHIFT_RIGHT_N_16(((x) + (1 << (PJPG_DCT_SCALE_BITS - 1))), PJPG_DCT_SCALE_BITS) - -#define PJPG_WFIX(x) ((x) * PJPG_DCT_SCALE + 0.5f) - -#define PJPG_WINOGRAD_QUANT_SCALE_BITS 10 - -const uint8 gWinogradQuant[] = { - 128, 178, 178, 167, 246, 167, 151, 232, 232, 151, 128, 209, 219, 209, 128, 101, 178, 197, 197, 178, 101, 69, - 139, 167, 177, 167, 139, 69, 35, 96, 131, 151, 151, 131, 96, 35, 49, 91, 118, 128, 118, 91, 49, 46, - 81, 101, 101, 81, 46, 42, 69, 79, 69, 42, 35, 54, 54, 35, 28, 37, 28, 19, 19, 10, -}; - -// Multiply quantization matrix by the Winograd IDCT scale factors -static void createWinogradQuant(int16* pQuant) { - uint8 i; - - for (i = 0; i < 64; i++) { - long x = pQuant[i]; - x *= gWinogradQuant[i]; - pQuant[i] = (int16)((x + (1 << (PJPG_WINOGRAD_QUANT_SCALE_BITS - PJPG_DCT_SCALE_BITS - 1))) >> - (PJPG_WINOGRAD_QUANT_SCALE_BITS - PJPG_DCT_SCALE_BITS)); - } -} - -// These multiply helper functions are the 4 types of signed multiplies needed by the Winograd IDCT. -// A smart C compiler will optimize them to use 16x8 = 24 bit muls, if not you may need to tweak -// these functions or drop to CPU specific inline assembly. - -// 1/cos(4*pi/16) -// 362, 256+106 -static PJPG_INLINE int16 imul_b1_b3(int16 w) { - long x = (w * 362L); - x += 128L; - return (int16)(PJPG_ARITH_SHIFT_RIGHT_8_L(x)); -} - -// 1/cos(6*pi/16) -// 669, 256+256+157 -static PJPG_INLINE int16 imul_b2(int16 w) { - long x = (w * 669L); - x += 128L; - return (int16)(PJPG_ARITH_SHIFT_RIGHT_8_L(x)); -} - -// 1/cos(2*pi/16) -// 277, 256+21 -static PJPG_INLINE int16 imul_b4(int16 w) { - long x = (w * 277L); - x += 128L; - return (int16)(PJPG_ARITH_SHIFT_RIGHT_8_L(x)); -} - -// 1/(cos(2*pi/16) + cos(6*pi/16)) -// 196, 196 -static PJPG_INLINE int16 imul_b5(int16 w) { - long x = (w * 196L); - x += 128L; - return (int16)(PJPG_ARITH_SHIFT_RIGHT_8_L(x)); -} - -static PJPG_INLINE uint8 clamp(int16 s) { - if ((uint16)s > 255U) { - if (s < 0) - return 0; - else if (s > 255) - return 255; - } - - return (uint8)s; -} - -static void idctRows(void) { - uint8 i; - int16* pSrc = gCoeffBuf; - - for (i = 0; i < 8; i++) { - if ((pSrc[1] | pSrc[2] | pSrc[3] | pSrc[4] | pSrc[5] | pSrc[6] | pSrc[7]) == 0) { - // Short circuit the 1D IDCT if only the DC component is non-zero - int16 src0 = *pSrc; - - *(pSrc + 1) = src0; - *(pSrc + 2) = src0; - *(pSrc + 3) = src0; - *(pSrc + 4) = src0; - *(pSrc + 5) = src0; - *(pSrc + 6) = src0; - *(pSrc + 7) = src0; - } else { - int16 src4 = *(pSrc + 5); - int16 src7 = *(pSrc + 3); - int16 x4 = src4 - src7; - int16 x7 = src4 + src7; - - int16 src5 = *(pSrc + 1); - int16 src6 = *(pSrc + 7); - int16 x5 = src5 + src6; - int16 x6 = src5 - src6; - - int16 tmp1 = imul_b5(x4 - x6); - int16 stg26 = imul_b4(x6) - tmp1; - - int16 x24 = tmp1 - imul_b2(x4); - - int16 x15 = x5 - x7; - int16 x17 = x5 + x7; - - int16 tmp2 = stg26 - x17; - int16 tmp3 = imul_b1_b3(x15) - tmp2; - int16 x44 = tmp3 + x24; - - int16 src0 = *(pSrc + 0); - int16 src1 = *(pSrc + 4); - int16 x30 = src0 + src1; - int16 x31 = src0 - src1; - - int16 src2 = *(pSrc + 2); - int16 src3 = *(pSrc + 6); - int16 x12 = src2 - src3; - int16 x13 = src2 + src3; - - int16 x32 = imul_b1_b3(x12) - x13; - - int16 x40 = x30 + x13; - int16 x43 = x30 - x13; - int16 x41 = x31 + x32; - int16 x42 = x31 - x32; - - *(pSrc + 0) = x40 + x17; - *(pSrc + 1) = x41 + tmp2; - *(pSrc + 2) = x42 + tmp3; - *(pSrc + 3) = x43 - x44; - *(pSrc + 4) = x43 + x44; - *(pSrc + 5) = x42 - tmp3; - *(pSrc + 6) = x41 - tmp2; - *(pSrc + 7) = x40 - x17; - } - - pSrc += 8; - } -} - -static void idctCols(void) { - uint8 i; - - int16* pSrc = gCoeffBuf; - - for (i = 0; i < 8; i++) { - if ((pSrc[1 * 8] | pSrc[2 * 8] | pSrc[3 * 8] | pSrc[4 * 8] | pSrc[5 * 8] | pSrc[6 * 8] | pSrc[7 * 8]) == 0) { - // Short circuit the 1D IDCT if only the DC component is non-zero - uint8 c = clamp(PJPG_DESCALE(*pSrc) + 128); - *(pSrc + 0 * 8) = c; - *(pSrc + 1 * 8) = c; - *(pSrc + 2 * 8) = c; - *(pSrc + 3 * 8) = c; - *(pSrc + 4 * 8) = c; - *(pSrc + 5 * 8) = c; - *(pSrc + 6 * 8) = c; - *(pSrc + 7 * 8) = c; - } else { - int16 src4 = *(pSrc + 5 * 8); - int16 src7 = *(pSrc + 3 * 8); - int16 x4 = src4 - src7; - int16 x7 = src4 + src7; - - int16 src5 = *(pSrc + 1 * 8); - int16 src6 = *(pSrc + 7 * 8); - int16 x5 = src5 + src6; - int16 x6 = src5 - src6; - - int16 tmp1 = imul_b5(x4 - x6); - int16 stg26 = imul_b4(x6) - tmp1; - - int16 x24 = tmp1 - imul_b2(x4); - - int16 x15 = x5 - x7; - int16 x17 = x5 + x7; - - int16 tmp2 = stg26 - x17; - int16 tmp3 = imul_b1_b3(x15) - tmp2; - int16 x44 = tmp3 + x24; - - int16 src0 = *(pSrc + 0 * 8); - int16 src1 = *(pSrc + 4 * 8); - int16 x30 = src0 + src1; - int16 x31 = src0 - src1; - - int16 src2 = *(pSrc + 2 * 8); - int16 src3 = *(pSrc + 6 * 8); - int16 x12 = src2 - src3; - int16 x13 = src2 + src3; - - int16 x32 = imul_b1_b3(x12) - x13; - - int16 x40 = x30 + x13; - int16 x43 = x30 - x13; - int16 x41 = x31 + x32; - int16 x42 = x31 - x32; - - // descale, convert to unsigned and clamp to 8-bit - *(pSrc + 0 * 8) = clamp(PJPG_DESCALE(x40 + x17) + 128); - *(pSrc + 1 * 8) = clamp(PJPG_DESCALE(x41 + tmp2) + 128); - *(pSrc + 2 * 8) = clamp(PJPG_DESCALE(x42 + tmp3) + 128); - *(pSrc + 3 * 8) = clamp(PJPG_DESCALE(x43 - x44) + 128); - *(pSrc + 4 * 8) = clamp(PJPG_DESCALE(x43 + x44) + 128); - *(pSrc + 5 * 8) = clamp(PJPG_DESCALE(x42 - tmp3) + 128); - *(pSrc + 6 * 8) = clamp(PJPG_DESCALE(x41 - tmp2) + 128); - *(pSrc + 7 * 8) = clamp(PJPG_DESCALE(x40 - x17) + 128); - } - - pSrc++; - } -} - -/*----------------------------------------------------------------------------*/ -static PJPG_INLINE uint8 addAndClamp(uint8 a, int16 b) { - b = a + b; - - if ((uint16)b > 255U) { - if (b < 0) - return 0; - else if (b > 255) - return 255; - } - - return (uint8)b; -} -/*----------------------------------------------------------------------------*/ -static PJPG_INLINE uint8 subAndClamp(uint8 a, int16 b) { - b = a - b; - - if ((uint16)b > 255U) { - if (b < 0) - return 0; - else if (b > 255) - return 255; - } - - return (uint8)b; -} -/*----------------------------------------------------------------------------*/ -// 103/256 -// R = Y + 1.402 (Cr-128) - -// 88/256, 183/256 -// G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128) - -// 198/256 -// B = Y + 1.772 (Cb-128) -/*----------------------------------------------------------------------------*/ -// Cb upsample and accumulate, 4x4 to 8x8 -static void upsampleCb(uint8 srcOfs, uint8 dstOfs) { - // Cb - affects G and B - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstG = gMCUBufG + dstOfs; - uint8* pDstB = gMCUBufB + dstOfs; - for (y = 0; y < 4; y++) { - for (x = 0; x < 4; x++) { - uint8 cb = (uint8)*pSrc++; - int16 cbG, cbB; - - cbG = ((cb * 88U) >> 8U) - 44U; - pDstG[0] = subAndClamp(pDstG[0], cbG); - pDstG[1] = subAndClamp(pDstG[1], cbG); - pDstG[8] = subAndClamp(pDstG[8], cbG); - pDstG[9] = subAndClamp(pDstG[9], cbG); - - cbB = (cb + ((cb * 198U) >> 8U)) - 227U; - pDstB[0] = addAndClamp(pDstB[0], cbB); - pDstB[1] = addAndClamp(pDstB[1], cbB); - pDstB[8] = addAndClamp(pDstB[8], cbB); - pDstB[9] = addAndClamp(pDstB[9], cbB); - - pDstG += 2; - pDstB += 2; - } - - pSrc = pSrc - 4 + 8; - pDstG = pDstG - 8 + 16; - pDstB = pDstB - 8 + 16; - } -} -/*----------------------------------------------------------------------------*/ -// Cb upsample and accumulate, 4x8 to 8x8 -static void upsampleCbH(uint8 srcOfs, uint8 dstOfs) { - // Cb - affects G and B - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstG = gMCUBufG + dstOfs; - uint8* pDstB = gMCUBufB + dstOfs; - for (y = 0; y < 8; y++) { - for (x = 0; x < 4; x++) { - uint8 cb = (uint8)*pSrc++; - int16 cbG, cbB; - - cbG = ((cb * 88U) >> 8U) - 44U; - pDstG[0] = subAndClamp(pDstG[0], cbG); - pDstG[1] = subAndClamp(pDstG[1], cbG); - - cbB = (cb + ((cb * 198U) >> 8U)) - 227U; - pDstB[0] = addAndClamp(pDstB[0], cbB); - pDstB[1] = addAndClamp(pDstB[1], cbB); - - pDstG += 2; - pDstB += 2; - } - - pSrc = pSrc - 4 + 8; - } -} -/*----------------------------------------------------------------------------*/ -// Cb upsample and accumulate, 8x4 to 8x8 -static void upsampleCbV(uint8 srcOfs, uint8 dstOfs) { - // Cb - affects G and B - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstG = gMCUBufG + dstOfs; - uint8* pDstB = gMCUBufB + dstOfs; - for (y = 0; y < 4; y++) { - for (x = 0; x < 8; x++) { - uint8 cb = (uint8)*pSrc++; - int16 cbG, cbB; - - cbG = ((cb * 88U) >> 8U) - 44U; - pDstG[0] = subAndClamp(pDstG[0], cbG); - pDstG[8] = subAndClamp(pDstG[8], cbG); - - cbB = (cb + ((cb * 198U) >> 8U)) - 227U; - pDstB[0] = addAndClamp(pDstB[0], cbB); - pDstB[8] = addAndClamp(pDstB[8], cbB); - - ++pDstG; - ++pDstB; - } - - pDstG = pDstG - 8 + 16; - pDstB = pDstB - 8 + 16; - } -} -/*----------------------------------------------------------------------------*/ -// 103/256 -// R = Y + 1.402 (Cr-128) - -// 88/256, 183/256 -// G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128) - -// 198/256 -// B = Y + 1.772 (Cb-128) -/*----------------------------------------------------------------------------*/ -// Cr upsample and accumulate, 4x4 to 8x8 -static void upsampleCr(uint8 srcOfs, uint8 dstOfs) { - // Cr - affects R and G - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstR = gMCUBufR + dstOfs; - uint8* pDstG = gMCUBufG + dstOfs; - for (y = 0; y < 4; y++) { - for (x = 0; x < 4; x++) { - uint8 cr = (uint8)*pSrc++; - int16 crR, crG; - - crR = (cr + ((cr * 103U) >> 8U)) - 179; - pDstR[0] = addAndClamp(pDstR[0], crR); - pDstR[1] = addAndClamp(pDstR[1], crR); - pDstR[8] = addAndClamp(pDstR[8], crR); - pDstR[9] = addAndClamp(pDstR[9], crR); - - crG = ((cr * 183U) >> 8U) - 91; - pDstG[0] = subAndClamp(pDstG[0], crG); - pDstG[1] = subAndClamp(pDstG[1], crG); - pDstG[8] = subAndClamp(pDstG[8], crG); - pDstG[9] = subAndClamp(pDstG[9], crG); - - pDstR += 2; - pDstG += 2; - } - - pSrc = pSrc - 4 + 8; - pDstR = pDstR - 8 + 16; - pDstG = pDstG - 8 + 16; - } -} -/*----------------------------------------------------------------------------*/ -// Cr upsample and accumulate, 4x8 to 8x8 -static void upsampleCrH(uint8 srcOfs, uint8 dstOfs) { - // Cr - affects R and G - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstR = gMCUBufR + dstOfs; - uint8* pDstG = gMCUBufG + dstOfs; - for (y = 0; y < 8; y++) { - for (x = 0; x < 4; x++) { - uint8 cr = (uint8)*pSrc++; - int16 crR, crG; - - crR = (cr + ((cr * 103U) >> 8U)) - 179; - pDstR[0] = addAndClamp(pDstR[0], crR); - pDstR[1] = addAndClamp(pDstR[1], crR); - - crG = ((cr * 183U) >> 8U) - 91; - pDstG[0] = subAndClamp(pDstG[0], crG); - pDstG[1] = subAndClamp(pDstG[1], crG); - - pDstR += 2; - pDstG += 2; - } - - pSrc = pSrc - 4 + 8; - } -} -/*----------------------------------------------------------------------------*/ -// Cr upsample and accumulate, 8x4 to 8x8 -static void upsampleCrV(uint8 srcOfs, uint8 dstOfs) { - // Cr - affects R and G - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstR = gMCUBufR + dstOfs; - uint8* pDstG = gMCUBufG + dstOfs; - for (y = 0; y < 4; y++) { - for (x = 0; x < 8; x++) { - uint8 cr = (uint8)*pSrc++; - int16 crR, crG; - - crR = (cr + ((cr * 103U) >> 8U)) - 179; - pDstR[0] = addAndClamp(pDstR[0], crR); - pDstR[8] = addAndClamp(pDstR[8], crR); - - crG = ((cr * 183U) >> 8U) - 91; - pDstG[0] = subAndClamp(pDstG[0], crG); - pDstG[8] = subAndClamp(pDstG[8], crG); - - ++pDstR; - ++pDstG; - } - - pDstR = pDstR - 8 + 16; - pDstG = pDstG - 8 + 16; - } -} -/*----------------------------------------------------------------------------*/ -// Convert Y to RGB -static void copyY(uint8 dstOfs) { - uint8 i; - uint8* pRDst = gMCUBufR + dstOfs; - uint8* pGDst = gMCUBufG + dstOfs; - uint8* pBDst = gMCUBufB + dstOfs; - int16* pSrc = gCoeffBuf; - - for (i = 64; i > 0; i--) { - uint8 c = (uint8)*pSrc++; - - *pRDst++ = c; - *pGDst++ = c; - *pBDst++ = c; - } -} -/*----------------------------------------------------------------------------*/ -// Cb convert to RGB and accumulate -static void convertCb(uint8 dstOfs) { - uint8 i; - uint8* pDstG = gMCUBufG + dstOfs; - uint8* pDstB = gMCUBufB + dstOfs; - int16* pSrc = gCoeffBuf; - - for (i = 64; i > 0; i--) { - uint8 cb = (uint8)*pSrc++; - int16 cbG, cbB; - - cbG = ((cb * 88U) >> 8U) - 44U; - *pDstG++ = subAndClamp(pDstG[0], cbG); - - cbB = (cb + ((cb * 198U) >> 8U)) - 227U; - *pDstB++ = addAndClamp(pDstB[0], cbB); - } -} -/*----------------------------------------------------------------------------*/ -// Cr convert to RGB and accumulate -static void convertCr(uint8 dstOfs) { - uint8 i; - uint8* pDstR = gMCUBufR + dstOfs; - uint8* pDstG = gMCUBufG + dstOfs; - int16* pSrc = gCoeffBuf; - - for (i = 64; i > 0; i--) { - uint8 cr = (uint8)*pSrc++; - int16 crR, crG; - - crR = (cr + ((cr * 103U) >> 8U)) - 179; - *pDstR++ = addAndClamp(pDstR[0], crR); - - crG = ((cr * 183U) >> 8U) - 91; - *pDstG++ = subAndClamp(pDstG[0], crG); - } -} -/*----------------------------------------------------------------------------*/ -static void transformBlock(uint8 mcuBlock) { - idctRows(); - idctCols(); - - switch (gScanType) { - case PJPG_GRAYSCALE: { - // MCU size: 1, 1 block per MCU - copyY(0); - break; - } - case PJPG_YH1V1: { - // MCU size: 8x8, 3 blocks per MCU - switch (mcuBlock) { - case 0: { - copyY(0); - break; - } - case 1: { - convertCb(0); - break; - } - case 2: { - convertCr(0); - break; - } - } - - break; - } - case PJPG_YH1V2: { - // MCU size: 8x16, 4 blocks per MCU - switch (mcuBlock) { - case 0: { - copyY(0); - break; - } - case 1: { - copyY(128); - break; - } - case 2: { - upsampleCbV(0, 0); - upsampleCbV(4 * 8, 128); - break; - } - case 3: { - upsampleCrV(0, 0); - upsampleCrV(4 * 8, 128); - break; - } - } - - break; - } - case PJPG_YH2V1: { - // MCU size: 16x8, 4 blocks per MCU - switch (mcuBlock) { - case 0: { - copyY(0); - break; - } - case 1: { - copyY(64); - break; - } - case 2: { - upsampleCbH(0, 0); - upsampleCbH(4, 64); - break; - } - case 3: { - upsampleCrH(0, 0); - upsampleCrH(4, 64); - break; - } - } - - break; - } - case PJPG_YH2V2: { - // MCU size: 16x16, 6 blocks per MCU - switch (mcuBlock) { - case 0: { - copyY(0); - break; - } - case 1: { - copyY(64); - break; - } - case 2: { - copyY(128); - break; - } - case 3: { - copyY(192); - break; - } - case 4: { - upsampleCb(0, 0); - upsampleCb(4, 64); - upsampleCb(4 * 8, 128); - upsampleCb(4 + 4 * 8, 192); - break; - } - case 5: { - upsampleCr(0, 0); - upsampleCr(4, 64); - upsampleCr(4 * 8, 128); - upsampleCr(4 + 4 * 8, 192); - break; - } - } - - break; - } - } -} -//------------------------------------------------------------------------------ -static void transformBlockReduce(uint8 mcuBlock) { - uint8 c = clamp(PJPG_DESCALE(gCoeffBuf[0]) + 128); - int16 cbG, cbB, crR, crG; - - switch (gScanType) { - case PJPG_GRAYSCALE: { - // MCU size: 1, 1 block per MCU - gMCUBufR[0] = c; - break; - } - case PJPG_YH1V1: { - // MCU size: 8x8, 3 blocks per MCU - switch (mcuBlock) { - case 0: { - gMCUBufR[0] = c; - gMCUBufG[0] = c; - gMCUBufB[0] = c; - break; - } - case 1: { - cbG = ((c * 88U) >> 8U) - 44U; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], cbG); - - cbB = (c + ((c * 198U) >> 8U)) - 227U; - gMCUBufB[0] = addAndClamp(gMCUBufB[0], cbB); - break; - } - case 2: { - crR = (c + ((c * 103U) >> 8U)) - 179; - gMCUBufR[0] = addAndClamp(gMCUBufR[0], crR); - - crG = ((c * 183U) >> 8U) - 91; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], crG); - break; - } - } - - break; - } - case PJPG_YH1V2: { - // MCU size: 8x16, 4 blocks per MCU - switch (mcuBlock) { - case 0: { - gMCUBufR[0] = c; - gMCUBufG[0] = c; - gMCUBufB[0] = c; - break; - } - case 1: { - gMCUBufR[128] = c; - gMCUBufG[128] = c; - gMCUBufB[128] = c; - break; - } - case 2: { - cbG = ((c * 88U) >> 8U) - 44U; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], cbG); - gMCUBufG[128] = subAndClamp(gMCUBufG[128], cbG); - - cbB = (c + ((c * 198U) >> 8U)) - 227U; - gMCUBufB[0] = addAndClamp(gMCUBufB[0], cbB); - gMCUBufB[128] = addAndClamp(gMCUBufB[128], cbB); - - break; - } - case 3: { - crR = (c + ((c * 103U) >> 8U)) - 179; - gMCUBufR[0] = addAndClamp(gMCUBufR[0], crR); - gMCUBufR[128] = addAndClamp(gMCUBufR[128], crR); - - crG = ((c * 183U) >> 8U) - 91; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], crG); - gMCUBufG[128] = subAndClamp(gMCUBufG[128], crG); - - break; - } - } - break; - } - case PJPG_YH2V1: { - // MCU size: 16x8, 4 blocks per MCU - switch (mcuBlock) { - case 0: { - gMCUBufR[0] = c; - gMCUBufG[0] = c; - gMCUBufB[0] = c; - break; - } - case 1: { - gMCUBufR[64] = c; - gMCUBufG[64] = c; - gMCUBufB[64] = c; - break; - } - case 2: { - cbG = ((c * 88U) >> 8U) - 44U; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], cbG); - gMCUBufG[64] = subAndClamp(gMCUBufG[64], cbG); - - cbB = (c + ((c * 198U) >> 8U)) - 227U; - gMCUBufB[0] = addAndClamp(gMCUBufB[0], cbB); - gMCUBufB[64] = addAndClamp(gMCUBufB[64], cbB); - - break; - } - case 3: { - crR = (c + ((c * 103U) >> 8U)) - 179; - gMCUBufR[0] = addAndClamp(gMCUBufR[0], crR); - gMCUBufR[64] = addAndClamp(gMCUBufR[64], crR); - - crG = ((c * 183U) >> 8U) - 91; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], crG); - gMCUBufG[64] = subAndClamp(gMCUBufG[64], crG); - - break; - } - } - break; - } - case PJPG_YH2V2: { - // MCU size: 16x16, 6 blocks per MCU - switch (mcuBlock) { - case 0: { - gMCUBufR[0] = c; - gMCUBufG[0] = c; - gMCUBufB[0] = c; - break; - } - case 1: { - gMCUBufR[64] = c; - gMCUBufG[64] = c; - gMCUBufB[64] = c; - break; - } - case 2: { - gMCUBufR[128] = c; - gMCUBufG[128] = c; - gMCUBufB[128] = c; - break; - } - case 3: { - gMCUBufR[192] = c; - gMCUBufG[192] = c; - gMCUBufB[192] = c; - break; - } - case 4: { - cbG = ((c * 88U) >> 8U) - 44U; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], cbG); - gMCUBufG[64] = subAndClamp(gMCUBufG[64], cbG); - gMCUBufG[128] = subAndClamp(gMCUBufG[128], cbG); - gMCUBufG[192] = subAndClamp(gMCUBufG[192], cbG); - - cbB = (c + ((c * 198U) >> 8U)) - 227U; - gMCUBufB[0] = addAndClamp(gMCUBufB[0], cbB); - gMCUBufB[64] = addAndClamp(gMCUBufB[64], cbB); - gMCUBufB[128] = addAndClamp(gMCUBufB[128], cbB); - gMCUBufB[192] = addAndClamp(gMCUBufB[192], cbB); - - break; - } - case 5: { - crR = (c + ((c * 103U) >> 8U)) - 179; - gMCUBufR[0] = addAndClamp(gMCUBufR[0], crR); - gMCUBufR[64] = addAndClamp(gMCUBufR[64], crR); - gMCUBufR[128] = addAndClamp(gMCUBufR[128], crR); - gMCUBufR[192] = addAndClamp(gMCUBufR[192], crR); - - crG = ((c * 183U) >> 8U) - 91; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], crG); - gMCUBufG[64] = subAndClamp(gMCUBufG[64], crG); - gMCUBufG[128] = subAndClamp(gMCUBufG[128], crG); - gMCUBufG[192] = subAndClamp(gMCUBufG[192], crG); - - break; - } - } - break; - } - } -} -//------------------------------------------------------------------------------ -static uint8 decodeNextMCU(void) { - uint8 status; - uint8 mcuBlock; - - if (gRestartInterval) { - if (gRestartsLeft == 0) { - status = processRestart(); - if (status) return status; - } - gRestartsLeft--; - } - - for (mcuBlock = 0; mcuBlock < gMaxBlocksPerMCU; mcuBlock++) { - uint8 componentID = gMCUOrg[mcuBlock]; - uint8 compQuant = gCompQuant[componentID]; - uint8 compDCTab = gCompDCTab[componentID]; - uint8 numExtraBits, compACTab, k; - const int16* pQ = compQuant ? gQuant1 : gQuant0; - uint16 r, dc; - - uint8 s = huffDecode(compDCTab ? &gHuffTab1 : &gHuffTab0, compDCTab ? gHuffVal1 : gHuffVal0); - - r = 0; - numExtraBits = s & 0xF; - if (numExtraBits) r = getBits2(numExtraBits); - dc = huffExtend(r, s); - - dc = dc + gLastDC[componentID]; - gLastDC[componentID] = dc; - - gCoeffBuf[0] = dc * pQ[0]; - - compACTab = gCompACTab[componentID]; - - if (gReduce) { - // Decode, but throw out the AC coefficients in reduce mode. - for (k = 1; k < 64; k++) { - s = huffDecode(compACTab ? &gHuffTab3 : &gHuffTab2, compACTab ? gHuffVal3 : gHuffVal2); - - numExtraBits = s & 0xF; - if (numExtraBits) getBits2(numExtraBits); - - r = s >> 4; - s &= 15; - - if (s) { - if (r) { - if ((k + r) > 63) return PJPG_DECODE_ERROR; - - k = (uint8)(k + r); - } - } else { - if (r == 15) { - if ((k + 16) > 64) return PJPG_DECODE_ERROR; - - k += (16 - 1); // - 1 because the loop counter is k - } else - break; - } - } - - transformBlockReduce(mcuBlock); - } else { - // Decode and dequantize AC coefficients - for (k = 1; k < 64; k++) { - uint16 extraBits; - - s = huffDecode(compACTab ? &gHuffTab3 : &gHuffTab2, compACTab ? gHuffVal3 : gHuffVal2); - - extraBits = 0; - numExtraBits = s & 0xF; - if (numExtraBits) extraBits = getBits2(numExtraBits); - - r = s >> 4; - s &= 15; - - if (s) { - int16 ac; - - if (r) { - if ((k + r) > 63) return PJPG_DECODE_ERROR; - - while (r) { - gCoeffBuf[ZAG[k++]] = 0; - r--; - } - } - - ac = huffExtend(extraBits, s); - - gCoeffBuf[ZAG[k]] = ac * pQ[k]; - } else { - if (r == 15) { - if ((k + 16) > 64) return PJPG_DECODE_ERROR; - - for (r = 16; r > 0; r--) gCoeffBuf[ZAG[k++]] = 0; - - k--; // - 1 because the loop counter is k - } else - break; - } - } - - while (k < 64) gCoeffBuf[ZAG[k++]] = 0; - - transformBlock(mcuBlock); - } - } - - return 0; -} -//------------------------------------------------------------------------------ -unsigned char pjpeg_decode_mcu(void) { - uint8 status; - - if (gCallbackStatus) return gCallbackStatus; - - if ((!gNumMCUSRemainingX) && (!gNumMCUSRemainingY)) return PJPG_NO_MORE_BLOCKS; - - status = decodeNextMCU(); - if ((status) || (gCallbackStatus)) return gCallbackStatus ? gCallbackStatus : status; - - gNumMCUSRemainingX--; - if (!gNumMCUSRemainingX) { - gNumMCUSRemainingY--; - if (gNumMCUSRemainingY > 0) gNumMCUSRemainingX = gMaxMCUSPerRow; - } - - return 0; -} -//------------------------------------------------------------------------------ -unsigned char pjpeg_decode_init(pjpeg_image_info_t* pInfo, pjpeg_need_bytes_callback_t pNeed_bytes_callback, - void* pCallback_data, unsigned char reduce) { - uint8 status; - - pInfo->m_width = 0; - pInfo->m_height = 0; - pInfo->m_comps = 0; - pInfo->m_MCUSPerRow = 0; - pInfo->m_MCUSPerCol = 0; - pInfo->m_scanType = PJPG_GRAYSCALE; - pInfo->m_MCUWidth = 0; - pInfo->m_MCUHeight = 0; - pInfo->m_pMCUBufR = (unsigned char*)0; - pInfo->m_pMCUBufG = (unsigned char*)0; - pInfo->m_pMCUBufB = (unsigned char*)0; - - g_pNeedBytesCallback = pNeed_bytes_callback; - g_pCallback_data = pCallback_data; - gCallbackStatus = 0; - gReduce = reduce; - - status = init(); - if ((status) || (gCallbackStatus)) return gCallbackStatus ? gCallbackStatus : status; - - status = locateSOFMarker(); - if ((status) || (gCallbackStatus)) return gCallbackStatus ? gCallbackStatus : status; - - status = initFrame(); - if ((status) || (gCallbackStatus)) return gCallbackStatus ? gCallbackStatus : status; - - status = initScan(); - if ((status) || (gCallbackStatus)) return gCallbackStatus ? gCallbackStatus : status; - - pInfo->m_width = gImageXSize; - pInfo->m_height = gImageYSize; - pInfo->m_comps = gCompsInFrame; - pInfo->m_scanType = gScanType; - pInfo->m_MCUSPerRow = gMaxMCUSPerRow; - pInfo->m_MCUSPerCol = gMaxMCUSPerCol; - pInfo->m_MCUWidth = gMaxMCUXSize; - pInfo->m_MCUHeight = gMaxMCUYSize; - pInfo->m_pMCUBufR = gMCUBufR; - pInfo->m_pMCUBufG = gMCUBufG; - pInfo->m_pMCUBufB = gMCUBufB; - - return 0; -} diff --git a/lib/picojpeg/picojpeg.h b/lib/picojpeg/picojpeg.h deleted file mode 100644 index 11345fb71..000000000 --- a/lib/picojpeg/picojpeg.h +++ /dev/null @@ -1,124 +0,0 @@ -//------------------------------------------------------------------------------ -// picojpeg - Public domain, Rich Geldreich -//------------------------------------------------------------------------------ -#ifndef PICOJPEG_H -#define PICOJPEG_H - -#ifdef __cplusplus -extern "C" { -#endif - -// Error codes -enum { - PJPG_NO_MORE_BLOCKS = 1, - PJPG_BAD_DHT_COUNTS, - PJPG_BAD_DHT_INDEX, - PJPG_BAD_DHT_MARKER, - PJPG_BAD_DQT_MARKER, - PJPG_BAD_DQT_TABLE, - PJPG_BAD_PRECISION, - PJPG_BAD_HEIGHT, - PJPG_BAD_WIDTH, - PJPG_TOO_MANY_COMPONENTS, - PJPG_BAD_SOF_LENGTH, - PJPG_BAD_VARIABLE_MARKER, - PJPG_BAD_DRI_LENGTH, - PJPG_BAD_SOS_LENGTH, - PJPG_BAD_SOS_COMP_ID, - PJPG_W_EXTRA_BYTES_BEFORE_MARKER, - PJPG_NO_ARITHMITIC_SUPPORT, - PJPG_UNEXPECTED_MARKER, - PJPG_NOT_JPEG, - PJPG_UNSUPPORTED_MARKER, - PJPG_BAD_DQT_LENGTH, - PJPG_TOO_MANY_BLOCKS, - PJPG_UNDEFINED_QUANT_TABLE, - PJPG_UNDEFINED_HUFF_TABLE, - PJPG_NOT_SINGLE_SCAN, - PJPG_UNSUPPORTED_COLORSPACE, - PJPG_UNSUPPORTED_SAMP_FACTORS, - PJPG_DECODE_ERROR, - PJPG_BAD_RESTART_MARKER, - PJPG_ASSERTION_ERROR, - PJPG_BAD_SOS_SPECTRAL, - PJPG_BAD_SOS_SUCCESSIVE, - PJPG_STREAM_READ_ERROR, - PJPG_NOTENOUGHMEM, - PJPG_UNSUPPORTED_COMP_IDENT, - PJPG_UNSUPPORTED_QUANT_TABLE, - PJPG_UNSUPPORTED_MODE, // picojpeg doesn't support progressive JPEG's -}; - -// Scan types -typedef enum { PJPG_GRAYSCALE, PJPG_YH1V1, PJPG_YH2V1, PJPG_YH1V2, PJPG_YH2V2 } pjpeg_scan_type_t; - -typedef struct { - // Image resolution - int m_width; - int m_height; - - // Number of components (1 or 3) - int m_comps; - - // Total number of minimum coded units (MCU's) per row/col. - int m_MCUSPerRow; - int m_MCUSPerCol; - - // Scan type - pjpeg_scan_type_t m_scanType; - - // MCU width/height in pixels (each is either 8 or 16 depending on the scan type) - int m_MCUWidth; - int m_MCUHeight; - - // m_pMCUBufR, m_pMCUBufG, and m_pMCUBufB are pointers to internal MCU Y or RGB pixel component buffers. - // Each time pjpegDecodeMCU() is called successfully these buffers will be filled with 8x8 pixel blocks of Y or RGB - // pixels. Each MCU consists of (m_MCUWidth/8)*(m_MCUHeight/8) Y/RGB blocks: 1 for greyscale/no subsampling, 2 for - // H1V2/H2V1, or 4 blocks for H2V2 sampling factors. Each block is a contiguous array of 64 (8x8) bytes of a single - // component: either Y for grayscale images, or R, G or B components for color images. - // - // The 8x8 pixel blocks are organized in these byte arrays like this: - // - // PJPG_GRAYSCALE: Each MCU is decoded to a single block of 8x8 grayscale pixels. - // Only the values in m_pMCUBufR are valid. Each 8 bytes is a row of pixels (raster order: left to right, top to - // bottom) from the 8x8 block. - // - // PJPG_H1V1: Each MCU contains is decoded to a single block of 8x8 RGB pixels. - // - // PJPG_YH2V1: Each MCU is decoded to 2 blocks, or 16x8 pixels. - // The 2 RGB blocks are at byte offsets: 0, 64 - // - // PJPG_YH1V2: Each MCU is decoded to 2 blocks, or 8x16 pixels. - // The 2 RGB blocks are at byte offsets: 0, - // 128 - // - // PJPG_YH2V2: Each MCU is decoded to 4 blocks, or 16x16 pixels. - // The 2x2 block array is organized at byte offsets: 0, 64, - // 128, 192 - // - // It is up to the caller to copy or blit these pixels from these buffers into the destination bitmap. - unsigned char* m_pMCUBufR; - unsigned char* m_pMCUBufG; - unsigned char* m_pMCUBufB; -} pjpeg_image_info_t; - -typedef unsigned char (*pjpeg_need_bytes_callback_t)(unsigned char* pBuf, unsigned char buf_size, - unsigned char* pBytes_actually_read, void* pCallback_data); - -// Initializes the decompressor. Returns 0 on success, or one of the above error codes on failure. -// pNeed_bytes_callback will be called to fill the decompressor's internal input buffer. -// If reduce is 1, only the first pixel of each block will be decoded. This mode is much faster because it skips the AC -// dequantization, IDCT and chroma upsampling of every image pixel. Not thread safe. -unsigned char pjpeg_decode_init(pjpeg_image_info_t* pInfo, pjpeg_need_bytes_callback_t pNeed_bytes_callback, - void* pCallback_data, unsigned char reduce); - -// Decompresses the file's next MCU. Returns 0 on success, PJPG_NO_MORE_BLOCKS if no more blocks are available, or an -// error code. Must be called a total of m_MCUSPerRow*m_MCUSPerCol times to completely decompress the image. Not thread -// safe. -unsigned char pjpeg_decode_mcu(void); - -#ifdef __cplusplus -} -#endif - -#endif // PICOJPEG_H diff --git a/open-x4-sdk b/open-x4-sdk index 157d724d7..a64a3c29b 160000 --- a/open-x4-sdk +++ b/open-x4-sdk @@ -1 +1 @@ -Subproject commit 157d724d7a7389d49fe108e6dd5da2455a5340ba +Subproject commit a64a3c29bebc59b2ccdfe15492cfc4b5e4c26360 diff --git a/platformio.ini b/platformio.ini index cb8fd125b..75ee95672 100644 --- a/platformio.ini +++ b/platformio.ini @@ -50,8 +50,8 @@ board_build.partitions = partitions.csv extra_scripts = pre:scripts/build_html.py pre:scripts/gen_i18n.py - pre:scripts/patch_jpegdec.py pre:scripts/git_branch.py + pre:scripts/patch_jpegdec.py ; Libraries lib_deps = @@ -62,7 +62,7 @@ lib_deps = bblanchon/ArduinoJson @ 7.4.2 ricmoo/QRCode @ 0.0.1 bitbank2/PNGdec @ ^1.0.0 - bitbank2/JPEGDEC @ ^1.8.0 + https://github.com/bitbank2/JPEGDEC.git#86282979224c8a32fd51e091ed5a35b0c699a52b links2004/WebSockets @ 2.7.3 [env:default] diff --git a/scripts/patch_jpegdec.py b/scripts/patch_jpegdec.py index 4dd5a554e..015b17616 100644 --- a/scripts/patch_jpegdec.py +++ b/scripts/patch_jpegdec.py @@ -1,117 +1,68 @@ """ -PlatformIO pre-build script: patch JPEGDEC library for progressive JPEG support. +PlatformIO pre-build script: patch JPEGDEC for MCU_SKIP wild pointer crash. -Two patches are applied: +Problem: + JPEGDecodeMCU_P computes pMCU = &sMCUs[iMCU & 0xffffff]. When iMCU is + MCU_SKIP (-8), the bitmask produces index 0xFFFFF8 (16 777 208), creating a + pointer ~33 MB past the 392-entry sMCUs array. If the progressive JPEG's + first scan includes AC coefficients (iScanEnd > 0), the AC decode loop writes + through this wild pointer and crashes with a store-access fault. -1. JPEGMakeHuffTables: Skip AC Huffman table construction for progressive JPEGs. - JPEGDEC 1.8.x fails to open progressive JPEGs because JPEGMakeHuffTables() - cannot build AC tables with 11+-bit codes (the "slow tables" path is disabled). - Since progressive decode only uses DC coefficients, AC tables are not needed. + Upstream commit 8628297 guarded the DC coefficient write (pMCU[0]) but not the + AC coefficient writes at indices 1-63. -2. JPEGDecodeMCU_P: Guard pMCU writes against MCU_SKIP (-8). - The non-progressive JPEGDecodeMCU checks `iMCU >= 0` before writing to pMCU, - but JPEGDecodeMCU_P does not. When EIGHT_BIT_GRAYSCALE mode skips chroma - channels by passing MCU_SKIP, the unguarded write goes to a wild pointer - (sMCUs[0xFFFFF8]) and crashes. +Fix: + Redirect pMCU to sMCUs[0] when MCU_SKIP is active. Writes to sMCUs[1..63] + are harmless: for JPEG_SCALE_EIGHTH only sMCUs[0] is read for output, and + the DC write at sMCUs[0] is already guarded by the existing `if (iMCU >= 0)` + check. -Both patches are applied idempotently so it is safe to run on every build. +Applied idempotently — safe to run on every build. """ Import("env") import os + def patch_jpegdec(env): - # Find the JPEGDEC library in libdeps libdeps_dir = os.path.join(env["PROJECT_DIR"], ".pio", "libdeps") if not os.path.isdir(libdeps_dir): return for env_dir in os.listdir(libdeps_dir): jpeg_inl = os.path.join(libdeps_dir, env_dir, "JPEGDEC", "src", "jpeg.inl") if os.path.isfile(jpeg_inl): - _apply_ac_table_patch(jpeg_inl) - _apply_mcu_skip_patch(jpeg_inl) + _apply_mcu_skip_pointer_fix(jpeg_inl) -def _apply_ac_table_patch(filepath): - MARKER = "// CrossPoint patch: skip AC tables for progressive JPEG" + +def _apply_mcu_skip_pointer_fix(filepath): + MARKER = "// CrossPoint patch: safe pMCU for MCU_SKIP" with open(filepath, "r") as f: content = f.read() if MARKER in content: return # already patched - OLD = """\ - } - // now do AC components (up to 4 tables of 16-bit codes)""" + # The wild-pointer line in JPEGDecodeMCU_P: + OLD = " signed short *pMCU = &pJPEG->sMCUs[iMCU & 0xffffff];" - NEW = """\ - } - """ + MARKER + """ - // Progressive JPEG: only DC coefficients are decoded (first scan), so AC - // Huffman tables are not needed. Skip building them to avoid failing on - // 11+-bit AC codes that the optimized table builder cannot handle. - if (pJPEG->ucMode == 0xc2) - return 1; - // now do AC components (up to 4 tables of 16-bit codes)""" + NEW = ( + " " + MARKER + "\n" + " signed short *pMCU = (iMCU < 0) ? pJPEG->sMCUs\n" + " : &pJPEG->sMCUs[iMCU & 0xffffff];" + ) if OLD not in content: - print("WARNING: JPEGDEC AC table patch target not found in %s — library may have been updated" % filepath) + print( + "WARNING: JPEGDEC MCU_SKIP pointer patch target not found in %s " + "— library may have been updated" % filepath + ) return content = content.replace(OLD, NEW, 1) with open(filepath, "w") as f: f.write(content) - print("Patched JPEGDEC: skip AC tables for progressive JPEG: %s" % filepath) + print("Patched JPEGDEC: safe pMCU for MCU_SKIP in JPEGDecodeMCU_P: %s" % filepath) -def _apply_mcu_skip_patch(filepath): - MARKER = "// CrossPoint patch: guard pMCU write for MCU_SKIP" - with open(filepath, "r") as f: - content = f.read() - if MARKER in content: - return # already patched - - # Patch 1: Guard the unconditional pMCU[0] write in JPEGDecodeMCU_P. - # This is the DC coefficient store that crashes when iMCU = MCU_SKIP (-8). - OLD_DC = """\ - pMCU[0] = (short)*iDCPredictor; // store in MCU[0] - } - // Now get the other 63 AC coefficients""" - - NEW_DC = """\ - """ + MARKER + """ - if (iMCU >= 0) - pMCU[0] = (short)*iDCPredictor; // store in MCU[0] - } - // Now get the other 63 AC coefficients""" - - if OLD_DC not in content: - print("WARNING: JPEGDEC MCU_SKIP patch target not found in %s — library may have been updated" % filepath) - return - - content = content.replace(OLD_DC, NEW_DC, 1) - - # Patch 2: Guard the successive approximation pMCU[0] write. - # This path is taken on subsequent scans (cApproxBitsHigh != 0), which we - # don't normally hit (we only decode first scan), but guard it for safety. - OLD_SA = """\ - pMCU[0] |= iPositive; - } - goto mcu_done; // that's it""" - - NEW_SA = """\ - if (iMCU >= 0) - pMCU[0] |= iPositive; - } - goto mcu_done; // that's it""" - - if OLD_SA in content: - content = content.replace(OLD_SA, NEW_SA, 1) - - with open(filepath, "w") as f: - f.write(content) - print("Patched JPEGDEC: guard pMCU writes for MCU_SKIP in JPEGDecodeMCU_P: %s" % filepath) - -# Apply patches immediately when this pre: script runs, before compilation starts. -# Previously used env.AddPreAction("buildprog", ...) which deferred patching until -# the link step — after the library was already compiled from unpatched source. +# Run immediately at script import time (before compilation). patch_jpegdec(env) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index aca87bdc5..4ba71cfd7 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -126,7 +126,6 @@ bool CrossPointSettings::loadFromBinaryFile() { serialization::readPod(inputFile, version); if (version != SETTINGS_FILE_VERSION) { LOG_ERR("CPS", "Deserialization failed: Unknown version %u", version); - inputFile.close(); return false; } @@ -220,7 +219,6 @@ bool CrossPointSettings::loadFromBinaryFile() { applyLegacyFrontButtonLayout(*this); } - inputFile.close(); LOG_DBG("CPS", "Settings loaded from binary file"); return true; } diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 9a0e298b0..43dbcba6b 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -126,7 +126,7 @@ class CrossPointSettings { }; // Short power button press actions - enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, SHORT_PWRBTN_COUNT }; + enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, FORCE_REFRESH = 3, SHORT_PWRBTN_COUNT }; // Hide battery percentage enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT }; diff --git a/src/CrossPointState.cpp b/src/CrossPointState.cpp index af8bdda59..7d084da6e 100644 --- a/src/CrossPointState.cpp +++ b/src/CrossPointState.cpp @@ -55,7 +55,6 @@ bool CrossPointState::loadFromBinaryFile() { serialization::readPod(inputFile, version); if (version > STATE_FILE_VERSION) { LOG_ERR("CPS", "Deserialization failed: Unknown version %u", version); - inputFile.close(); return false; } @@ -76,6 +75,5 @@ bool CrossPointState::loadFromBinaryFile() { lastSleepFromReader = false; } - inputFile.close(); return true; } diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index f5a2c0483..b34f8523b 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -163,6 +163,7 @@ bool RecentBooksStore::loadFromBinaryFile() { } if (omitted > 0) { + // Explicitly close() file before saveToFile() rewrites the same file inputFile.close(); saveToFile(); LOG_DBG("RBS", "Omitted %u recent book(s) with missing title", omitted); @@ -170,11 +171,9 @@ bool RecentBooksStore::loadFromBinaryFile() { } } else { LOG_ERR("RBS", "Deserialization failed: Unknown version %u", version); - inputFile.close(); return false; } - inputFile.close(); LOG_DBG("RBS", "Recent books loaded from binary file (%d entries)", static_cast(recentBooks.size())); return true; } diff --git a/src/SettingsList.h b/src/SettingsList.h index cdbee372c..50225c467 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -71,8 +71,8 @@ inline const std::vector& getSettingsList() { SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip", StrId::STR_CAT_CONTROLS), SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, - {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN}, "shortPwrBtn", - StrId::STR_CAT_CONTROLS), + {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH}, + "shortPwrBtn", StrId::STR_CAT_CONTROLS), // --- System --- SettingInfo::Enum(StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout, diff --git a/src/WifiCredentialStore.cpp b/src/WifiCredentialStore.cpp index a95be3142..82102898d 100644 --- a/src/WifiCredentialStore.cpp +++ b/src/WifiCredentialStore.cpp @@ -76,7 +76,6 @@ bool WifiCredentialStore::loadFromBinaryFile() { serialization::readPod(file, version); if (version > WIFI_FILE_VERSION) { LOG_DBG("WCS", "Unknown file version: %u", version); - file.close(); return false; } @@ -98,7 +97,6 @@ bool WifiCredentialStore::loadFromBinaryFile() { credentials.push_back(cred); } - file.close(); // LOG_DBG("WCS", "Loaded %zu WiFi credentials from binary file", credentials.size()); return true; } diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index 3cf115b13..bd15abbaf 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -5,6 +5,7 @@ #include "boot_sleep/BootActivity.h" #include "boot_sleep/SleepActivity.h" #include "browser/OpdsBookBrowserActivity.h" +#include "home/CrashActivity.h" #include "home/FileBrowserActivity.h" #include "home/HomeActivity.h" #include "home/RecentBooksActivity.h" @@ -196,6 +197,8 @@ void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily:: replaceActivity(std::make_unique(renderer, mappedInput, std::move(message), style)); } +void ActivityManager::goToCrashReport() { replaceActivity(std::make_unique(renderer, mappedInput)); } + void ActivityManager::goHome() { replaceActivity(std::make_unique(renderer, mappedInput)); } void ActivityManager::pushActivity(std::unique_ptr&& activity) { diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h index bc24c42e3..bc975e919 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -86,6 +86,7 @@ class ActivityManager { void goToSleep(); void goToBoot(); void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR); + void goToCrashReport(); void goHome(); // This will move current activity to stack instead of deleting it diff --git a/src/activities/ActivityResult.h b/src/activities/ActivityResult.h index 4062fce97..0416285ee 100644 --- a/src/activities/ActivityResult.h +++ b/src/activities/ActivityResult.h @@ -59,7 +59,8 @@ struct ActivityResult { explicit ActivityResult() = default; - template >> + template + requires std::is_constructible_v // cppcheck-suppress noExplicitConstructor ActivityResult(ResultType&& result) : data{std::forward(result)} {} }; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index d6a566df3..88a15aa13 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -10,13 +10,22 @@ #include "CrossPointSettings.h" #include "CrossPointState.h" +#include "activities/reader/ReaderUtils.h" #include "components/UITheme.h" #include "fontIds.h" #include "images/Logo120.h" void SleepActivity::onEnter() { Activity::onEnter(); - GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP)); + + // Show popup with reader orientation only when going to sleep from reader + if (APP_STATE.lastSleepFromReader) { + ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); + GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP)); + renderer.setOrientation(GfxRenderer::Orientation::Portrait); + } else { + GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP)); + } switch (SETTINGS.sleepScreen) { case (CrossPointSettings::SLEEP_SCREEN_MODE::BLANK): @@ -24,8 +33,13 @@ void SleepActivity::onEnter() { case (CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM): return renderCustomSleepScreen(); case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER): - case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER_CUSTOM): return renderCoverSleepScreen(); + case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER_CUSTOM): + if (APP_STATE.lastSleepFromReader) { + return renderCoverSleepScreen(); + } else { + return renderCustomSleepScreen(); + } default: return renderDefaultSleepScreen(); } @@ -38,7 +52,6 @@ void SleepActivity::renderCustomSleepScreen() const { if (dir && dir.isDirectory()) { sleepDir = "/.sleep"; } else { - if (dir) dir.close(); dir = Storage.open("/sleep"); if (dir && dir.isDirectory()) { sleepDir = "/sleep"; @@ -51,29 +64,24 @@ void SleepActivity::renderCustomSleepScreen() const { // collect all valid BMP files for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { if (file.isDirectory()) { - file.close(); continue; } file.getName(name, sizeof(name)); auto filename = std::string(name); if (filename[0] == '.') { - file.close(); continue; } if (!FsHelpers::hasBmpExtension(filename)) { LOG_DBG("SLP", "Skipping non-.bmp file name: %s", name); - file.close(); continue; } Bitmap bitmap(file); if (bitmap.parseHeaders() != BmpReaderError::Ok) { LOG_DBG("SLP", "Skipping invalid BMP file: %s", name); - file.close(); continue; } files.emplace_back(filename); - file.close(); } const auto numFiles = files.size(); if (numFiles > 0) { @@ -93,16 +101,11 @@ void SleepActivity::renderCustomSleepScreen() const { Bitmap bitmap(file, true); if (bitmap.parseHeaders() == BmpReaderError::Ok) { renderBitmapSleepScreen(bitmap); - file.close(); - dir.close(); return; } - file.close(); } } } - if (dir) dir.close(); - // Look for sleep.bmp on the root of the sd card to determine if we should // render a custom sleep screen instead of the default. FsFile file; @@ -111,10 +114,8 @@ void SleepActivity::renderCustomSleepScreen() const { if (bitmap.parseHeaders() == BmpReaderError::Ok) { LOG_DBG("SLP", "Loading: /sleep.bmp"); renderBitmapSleepScreen(bitmap); - file.close(); return; } - file.close(); } renderDefaultSleepScreen(); @@ -281,10 +282,8 @@ void SleepActivity::renderCoverSleepScreen() const { if (bitmap.parseHeaders() == BmpReaderError::Ok) { LOG_DBG("SLP", "Rendering sleep cover: %s", coverBmpPath.c_str()); renderBitmapSleepScreen(bitmap); - file.close(); return; } - file.close(); } return (this->*renderNoCoverSleepScreen)(); diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index f6a58385d..e4c6f9fd9 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -10,6 +10,7 @@ #include "CrossPointSettings.h" #include "MappedInputManager.h" #include "activities/network/WifiSelectionActivity.h" +#include "activities/util/KeyboardEntryActivity.h" #include "components/UITheme.h" #include "fontIds.h" #include "network/HttpDownloader.h" @@ -18,7 +19,7 @@ namespace { constexpr int PAGE_ITEMS = 23; -} // namespace +} void OpdsBookBrowserActivity::onEnter() { Activity::onEnter(); @@ -26,47 +27,47 @@ void OpdsBookBrowserActivity::onEnter() { state = BrowserState::CHECK_WIFI; entries.clear(); navigationHistory.clear(); - currentPath = ""; // Root path - user provides full URL in settings + searchTemplate = ""; + currentPath = ""; selectorIndex = 0; + consumeConfirm = false; + consumeBack = false; errorMessage.clear(); statusMessage = tr(STR_CHECKING_WIFI); requestUpdate(); - // Check WiFi and connect if needed, then fetch feed checkAndConnectWifi(); } void OpdsBookBrowserActivity::onExit() { Activity::onExit(); - - // Turn off WiFi when exiting WiFi.mode(WIFI_OFF); - entries.clear(); navigationHistory.clear(); } void OpdsBookBrowserActivity::loop() { - // Handle WiFi selection subactivity - if (state == BrowserState::WIFI_SELECTION) { - // Should already handled by the WifiSelectionActivity + if (state == BrowserState::WIFI_SELECTION || state == BrowserState::SEARCH_INPUT) { + return; + } + + if (consumeConfirm && mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + consumeConfirm = false; + return; + } + if (consumeBack && mappedInput.wasReleased(MappedInputManager::Button::Back)) { + consumeBack = false; return; } - // Handle error state - Confirm retries, Back goes back or home if (state == BrowserState::ERROR) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - // Check if WiFi is still connected if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { - // WiFi connected - just retry fetching the feed - LOG_DBG("OPDS", "Retry: WiFi connected, retrying fetch"); state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); requestUpdate(); fetchFeed(currentPath); } else { - // WiFi not connected - launch WiFi selection - LOG_DBG("OPDS", "Retry: WiFi not connected, launching selection"); launchWifiSelection(); } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { @@ -75,59 +76,40 @@ void OpdsBookBrowserActivity::loop() { return; } - // Handle WiFi check state - only Back works - if (state == BrowserState::CHECK_WIFI) { + if (state == BrowserState::CHECK_WIFI || state == BrowserState::LOADING) { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - onGoHome(); + state == BrowserState::CHECK_WIFI ? onGoHome() : navigateBack(); } return; } - // Handle loading state - only Back works - if (state == BrowserState::LOADING) { - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - navigateBack(); - } - return; - } + if (state == BrowserState::DOWNLOADING) return; - // Handle downloading state - no input allowed - if (state == BrowserState::DOWNLOADING) { - return; - } - - // Handle browsing state if (state == BrowserState::BROWSING) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (!entries.empty()) { const auto& entry = entries[selectorIndex]; - if (entry.type == OpdsEntryType::BOOK) { - downloadBook(entry); - } else { - navigateToEntry(entry); - } + entry.type == OpdsEntryType::BOOK ? downloadBook(entry) : navigateToEntry(entry); } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { navigateBack(); + } else if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { + if (!searchTemplate.empty() && selectorIndex == 0) launchSearch(); } - // Handle navigation if (!entries.empty()) { buttonNavigator.onNextRelease([this] { selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size()); requestUpdate(); }); - buttonNavigator.onPreviousRelease([this] { selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size()); requestUpdate(); }); - buttonNavigator.onNextContinuous([this] { selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); requestUpdate(); }); - buttonNavigator.onPreviousContinuous([this] { selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); requestUpdate(); @@ -138,21 +120,12 @@ void OpdsBookBrowserActivity::loop() { void OpdsBookBrowserActivity::render(RenderLock&&) { renderer.clearScreen(); - const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD); - if (state == BrowserState::CHECK_WIFI) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str()); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); - return; - } - - if (state == BrowserState::LOADING) { + if (state == BrowserState::CHECK_WIFI || state == BrowserState::LOADING) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str()); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); @@ -171,76 +144,50 @@ void OpdsBookBrowserActivity::render(RenderLock&&) { if (state == BrowserState::DOWNLOADING) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, tr(STR_DOWNLOADING)); - const auto maxWidth = pageWidth - 40; - // Trim long titles to keep them within the screen bounds. - auto title = renderer.truncatedText(UI_10_FONT_ID, statusMessage.c_str(), maxWidth); + auto title = renderer.truncatedText(UI_10_FONT_ID, statusMessage.c_str(), pageWidth - 40); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, title.c_str()); if (downloadTotal > 0) { - const int barWidth = pageWidth - 100; - constexpr int barHeight = 20; - constexpr int barX = 50; - const int barY = pageHeight / 2 + 20; - GUI.drawProgressBar(renderer, Rect{barX, barY, barWidth, barHeight}, downloadProgress, downloadTotal); + GUI.drawProgressBar(renderer, Rect{50, pageHeight / 2 + 20, pageWidth - 100, 20}, downloadProgress, + downloadTotal); } renderer.displayBuffer(); return; } - // Browsing state - // Show appropriate button hint based on selected entry type - const char* confirmLabel = tr(STR_OPEN); - if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) { - confirmLabel = tr(STR_DOWNLOAD); - } - const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + const char* confirmLabel = + (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) ? tr(STR_DOWNLOAD) : tr(STR_OPEN); + const char* searchLabel = (!searchTemplate.empty() && selectorIndex == 0) ? tr(STR_SEARCH) : tr(STR_DIR_UP); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, searchLabel, tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); if (entries.empty()) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_NO_ENTRIES)); - renderer.displayBuffer(); - return; - } + } else { + const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; + renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); - const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; - renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); - - for (size_t i = pageStartIndex; i < entries.size() && i < static_cast(pageStartIndex + PAGE_ITEMS); i++) { - const auto& entry = entries[i]; - - // Format display text with type indicator - std::string displayText; - if (entry.type == OpdsEntryType::NAVIGATION) { - displayText = "> " + entry.title; // Folder/navigation indicator - } else { - // Book: "Title - Author" or just "Title" - displayText = entry.title; - if (!entry.author.empty()) { - displayText += " - " + entry.author; - } + for (size_t i = pageStartIndex; i < entries.size() && i < static_cast(pageStartIndex + PAGE_ITEMS); i++) { + const auto& entry = entries[i]; + std::string displayText = (entry.type == OpdsEntryType::NAVIGATION) ? "> " + entry.title : entry.title; + if (entry.type == OpdsEntryType::BOOK && !entry.author.empty()) displayText += " - " + entry.author; + auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), pageWidth - 40); + renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), + i != static_cast(selectorIndex)); } - - auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), renderer.getScreenWidth() - 40); - renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), - i != static_cast(selectorIndex)); } - renderer.displayBuffer(); } void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { - const char* serverUrl = SETTINGS.opdsServerUrl; - if (strlen(serverUrl) == 0) { + if (strlen(SETTINGS.opdsServerUrl) == 0) { state = BrowserState::ERROR; errorMessage = tr(STR_NO_SERVER_URL); requestUpdate(); return; } - std::string url = UrlUtils::buildUrl(serverUrl, path); - LOG_DBG("OPDS", "Fetching: %s", url.c_str()); - + std::string url = (path.find("http") == 0) ? path : UrlUtils::buildUrl(SETTINGS.opdsServerUrl, path); OpdsParser parser; - { OpdsParserStream stream{parser}; if (!HttpDownloader::fetchUrl(url, stream)) { @@ -258,50 +205,46 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { return; } + searchTemplate = parser.getSearchTemplate(); + const auto& nextUrl = parser.getNextPageUrl(); + const auto& prevUrl = parser.getPrevPageUrl(); entries = std::move(parser).getEntries(); - LOG_DBG("OPDS", "Found %d entries", entries.size()); - selectorIndex = 0; - if (entries.empty()) { - state = BrowserState::ERROR; - errorMessage = tr(STR_NO_ENTRIES); - requestUpdate(); - return; + if (!prevUrl.empty()) { + entries.insert(entries.begin(), OpdsEntry{OpdsEntryType::NAVIGATION, tr(STR_PREV_PAGE), "", prevUrl, ""}); + } + if (!nextUrl.empty()) { + entries.push_back(OpdsEntry{OpdsEntryType::NAVIGATION, tr(STR_NEXT_PAGE), "", nextUrl, ""}); } - state = BrowserState::BROWSING; + selectorIndex = 0; + state = entries.empty() ? BrowserState::ERROR : BrowserState::BROWSING; + if (entries.empty()) errorMessage = tr(STR_NO_ENTRIES); requestUpdate(); } void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) { - // Push current path to history before navigating navigationHistory.push_back(currentPath); currentPath = entry.href; - state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); entries.clear(); selectorIndex = 0; - requestUpdate(true); // Force update to show loading state immediately before fetch - + requestUpdate(true); fetchFeed(currentPath); } void OpdsBookBrowserActivity::navigateBack() { if (navigationHistory.empty()) { - // At root, go home onGoHome(); } else { - // Go back to previous catalog currentPath = navigationHistory.back(); navigationHistory.pop_back(); - state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); entries.clear(); selectorIndex = 0; requestUpdate(); - fetchFeed(currentPath); } } @@ -309,48 +252,84 @@ void OpdsBookBrowserActivity::navigateBack() { void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) { state = BrowserState::DOWNLOADING; statusMessage = book.title; - downloadProgress = 0; - downloadTotal = 0; + downloadProgress = downloadTotal = 0; requestUpdate(true); - // Build full download URL - std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href); - - // Create sanitized filename: "Title - Author.epub" or just "Title.epub" if no author - std::string baseName = book.title; - if (!book.author.empty()) { - baseName += " - " + book.author; - } - std::string filename = "/" + StringUtils::sanitizeFilename(baseName) + ".epub"; - - LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str()); + std::string downloadUrl = + (book.href.find("http") == 0) ? book.href : UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href); + std::string filename = + "/" + StringUtils::sanitizeFilename(book.title + (book.author.empty() ? "" : " - " + book.author)) + ".epub"; const auto result = HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) { downloadProgress = downloaded; downloadTotal = total; - requestUpdate(true); // Force update to refresh progress bar + requestUpdate(true); }); if (result == HttpDownloader::OK) { - LOG_DBG("OPDS", "Download complete: %s", filename.c_str()); - - // Invalidate any existing cache for this file to prevent stale metadata issues - Epub epub(filename, "/.crosspoint"); - epub.clearCache(); - LOG_DBG("OPDS", "Cleared cache for: %s", filename.c_str()); - + Epub(filename, "/.crosspoint").clearCache(); state = BrowserState::BROWSING; - requestUpdate(); } else { state = BrowserState::ERROR; errorMessage = tr(STR_DOWNLOAD_FAILED); - requestUpdate(); } + requestUpdate(); +} + +void OpdsBookBrowserActivity::launchSearch() { + consumeConfirm = true; + state = BrowserState::SEARCH_INPUT; + requestUpdate(); + + auto keyboard = std::make_unique(renderer, mappedInput, tr(STR_SEARCH)); + startActivityForResult(std::move(keyboard), [this](const ActivityResult& result) { + state = BrowserState::BROWSING; + if (!result.isCancelled) { + performSearch(std::get(result.data).text); + } else { + requestUpdate(); + } + }); +} + +void OpdsBookBrowserActivity::performSearch(const std::string& query) { + if (query.empty() || searchTemplate.empty()) { + state = BrowserState::BROWSING; + requestUpdate(); + return; + } + + auto urlEncode = [](const std::string& s) { + std::string out; + out.reserve(s.size() * 3); + for (unsigned char c : s) { + if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') + out += static_cast(c); + else { + char buf[4]; + snprintf(buf, sizeof(buf), "%%%02X", c); + out += buf; + } + } + return out; + }; + + std::string url = searchTemplate; + const std::string placeholder = "{searchTerms}"; + const size_t pos = url.find(placeholder); + if (pos != std::string::npos) url.replace(pos, placeholder.length(), urlEncode(query)); + + navigationHistory.push_back(currentPath); // <-- add this + currentPath = url; // <-- add this + + state = BrowserState::LOADING; + statusMessage = tr(STR_LOADING); + requestUpdate(true); + fetchFeed(url); } void OpdsBookBrowserActivity::checkAndConnectWifi() { - // Already connected? Verify connection is valid by checking IP if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); @@ -358,12 +337,11 @@ void OpdsBookBrowserActivity::checkAndConnectWifi() { fetchFeed(currentPath); return; } - - // Not connected - launch WiFi selection screen directly launchWifiSelection(); } void OpdsBookBrowserActivity::launchWifiSelection() { + consumeBack = consumeConfirm = true; state = BrowserState::WIFI_SELECTION; requestUpdate(); @@ -373,15 +351,11 @@ void OpdsBookBrowserActivity::launchWifiSelection() { void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) { if (connected) { - LOG_DBG("OPDS", "WiFi connected via selection, fetching feed"); state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); - requestUpdate(true); // Force update to show loading state immediately before fetch + requestUpdate(true); fetchFeed(currentPath); } else { - LOG_DBG("OPDS", "WiFi selection cancelled/failed"); - // Force disconnect to ensure clean state for next retry - // This prevents stale connection status from interfering WiFi.disconnect(); WiFi.mode(WIFI_OFF); state = BrowserState::ERROR; diff --git a/src/activities/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index fa716cbd2..8b55343a3 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -11,21 +11,13 @@ /** * Activity for browsing and downloading books from an OPDS server. * Supports navigation through catalog hierarchy and downloading EPUBs. - * When WiFi connection fails, launches WiFi selection to let user connect. */ class OpdsBookBrowserActivity final : public Activity { public: - enum class BrowserState { - CHECK_WIFI, // Checking WiFi connection - WIFI_SELECTION, // WiFi selection subactivity is active - LOADING, // Fetching OPDS feed - BROWSING, // Displaying entries (navigation or books) - DOWNLOADING, // Downloading selected EPUB - ERROR // Error state with message - }; + enum class BrowserState { CHECK_WIFI, WIFI_SELECTION, LOADING, BROWSING, DOWNLOADING, ERROR, SEARCH_INPUT }; explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("OpdsBookBrowser", renderer, mappedInput) {} + : Activity("OpdsBookBrowser", renderer, mappedInput), buttonNavigator() {} void onEnter() override; void onExit() override; @@ -36,8 +28,11 @@ class OpdsBookBrowserActivity final : public Activity { ButtonNavigator buttonNavigator; BrowserState state = BrowserState::LOADING; std::vector entries; - std::vector navigationHistory; // Stack of previous feed paths for back navigation - std::string currentPath; // Current feed path being displayed + std::vector navigationHistory; + std::string currentPath; + std::string searchTemplate; + bool consumeConfirm = false; + bool consumeBack = false; // Added missing member int selectorIndex = 0; std::string errorMessage; std::string statusMessage; @@ -51,5 +46,7 @@ class OpdsBookBrowserActivity final : public Activity { void navigateToEntry(const OpdsEntry& entry); void navigateBack(); void downloadBook(const OpdsEntry& book); + void launchSearch(); + void performSearch(const std::string& query); bool preventAutoSleep() override { return true; } }; diff --git a/src/activities/home/CrashActivity.cpp b/src/activities/home/CrashActivity.cpp new file mode 100644 index 000000000..4f5a392b9 --- /dev/null +++ b/src/activities/home/CrashActivity.cpp @@ -0,0 +1,61 @@ +#include "CrashActivity.h" + +#include +#include +#include + +#include "components/UITheme.h" +#include "fontIds.h" + +void CrashActivity::onEnter() { + Activity::onEnter(); + + panicMessage = HalSystem::getPanicInfo(false); + if (panicMessage.empty()) { + panicMessage = tr(STR_CRASH_NO_REASON); + } + HalSystem::clearPanic(); + + requestUpdateAndWait(); +} + +void CrashActivity::loop() { + if (mappedInput.isPressed(MappedInputManager::Button::Back)) { + finish(); + } +} + +void CrashActivity::render(RenderLock&&) { + renderer.clearScreen(); + + const auto& metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto contentWidth = pageWidth - 2 * metrics.contentSidePadding; + const auto x = metrics.contentSidePadding; + const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CRASH_TITLE)); + + int y = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + + auto descLines = renderer.wrappedText(UI_10_FONT_ID, tr(STR_CRASH_DESCRIPTION), contentWidth, 10); + for (const auto& line : descLines) { + renderer.drawText(UI_10_FONT_ID, x, y, line.c_str()); + y += lineHeight; + } + + y += metrics.verticalSpacing * 2; + renderer.drawText(UI_10_FONT_ID, x, y, tr(STR_CRASH_REASON)); + y += lineHeight + metrics.verticalSpacing; + + auto panicLines = renderer.wrappedText(UI_10_FONT_ID, panicMessage.c_str(), contentWidth, 5); + for (const auto& line : panicLines) { + renderer.drawText(UI_10_FONT_ID, x, y, line.c_str()); + y += lineHeight; + } + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} diff --git a/src/activities/home/CrashActivity.h b/src/activities/home/CrashActivity.h new file mode 100644 index 000000000..9b4641ed1 --- /dev/null +++ b/src/activities/home/CrashActivity.h @@ -0,0 +1,13 @@ +#pragma once +#include "../Activity.h" + +class CrashActivity final : public Activity { + std::string panicMessage; + + public: + explicit CrashActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("Crash", renderer, mappedInput) {} + void onEnter() override; + void loop() override; + void render(RenderLock&&) override; +}; diff --git a/src/activities/home/FileBrowserActivity.cpp b/src/activities/home/FileBrowserActivity.cpp index 3b14031b5..1a4b45513 100644 --- a/src/activities/home/FileBrowserActivity.cpp +++ b/src/activities/home/FileBrowserActivity.cpp @@ -75,7 +75,6 @@ void FileBrowserActivity::loadFiles() { auto root = Storage.open(basepath.c_str()); if (!root || !root.isDirectory()) { - if (root) root.close(); return; } @@ -85,7 +84,6 @@ void FileBrowserActivity::loadFiles() { for (auto file = root.openNextFile(); file; file = root.openNextFile()) { file.getName(name, sizeof(name)); if ((!SETTINGS.showHiddenFiles && name[0] == '.') || strcmp(name, "System Volume Information") == 0) { - file.close(); continue; } @@ -99,18 +97,33 @@ void FileBrowserActivity::loadFiles() { files.emplace_back(filename); } } - file.close(); } - root.close(); sortFileList(files); } void FileBrowserActivity::onEnter() { Activity::onEnter(); - loadFiles(); selectorIndex = 0; + auto root = Storage.open(basepath.c_str()); + if (!root) { + basepath = "/"; + loadFiles(); + } else if (!root.isDirectory()) { + lockLongPressBack = mappedInput.isPressed(MappedInputManager::Button::Back); + + const std::string oldPath = basepath; + basepath = FsHelpers::extractFolderPath(basepath); + loadFiles(); + + const auto pos = oldPath.find_last_of('/'); + const std::string fileName = oldPath.substr(pos + 1); + selectorIndex = findEntry(fileName); + } else { + loadFiles(); + } + requestUpdate(); } @@ -129,15 +142,24 @@ void FileBrowserActivity::clearFileMetadata(const std::string& fullPath) { void FileBrowserActivity::loop() { // Long press BACK (1s+) goes to root folder + // but Long press BACK (1s+) from ReaderActivity sends us here with the MappedInput already set. + // So ignore it the first time. if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS && - basepath != "/") { + basepath != "/" && !lockLongPressBack) { basepath = "/"; loadFiles(); selectorIndex = 0; + requestUpdate(); return; } - const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); + if (lockLongPressBack && mappedInput.wasReleased(MappedInputManager::Button::Back)) { + lockLongPressBack = false; + return; + } + + const int pathReserved = renderer.getLineHeight(SMALL_FONT_ID) + UITheme::getInstance().getMetrics().verticalSpacing; + const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false, pathReserved); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (files.empty()) return; @@ -249,6 +271,14 @@ std::string getFileName(std::string filename) { return filename.substr(0, pos); } +std::string getFileExtension(std::string filename) { + if (filename.back() == '/') { + return ""; + } + const auto pos = filename.rfind('.'); + return filename.substr(pos); +} + void FileBrowserActivity::render(RenderLock&&) { renderer.clearScreen(); @@ -259,15 +289,46 @@ void FileBrowserActivity::render(RenderLock&&) { std::string folderName = (basepath == "/") ? tr(STR_SD_CARD) : basepath.substr(basepath.rfind('/') + 1); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName.c_str()); + const int pathLineHeight = renderer.getLineHeight(SMALL_FONT_ID); + const int pathReserved = pathLineHeight + metrics.verticalSpacing; const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; + const int contentHeight = + pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing - pathReserved; if (files.empty()) { renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_FILES_FOUND)); } else { GUI.drawList( renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex, [this](int index) { return getFileName(files[index]); }, nullptr, - [this](int index) { return UITheme::getFileIcon(files[index]); }); + [this](int index) { return UITheme::getFileIcon(files[index]); }, + [this](int index) { return getFileExtension(files[index]); }, false); + } + + // Full path display + { + const int pathY = pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - pathLineHeight; + const int separatorY = pathY - metrics.verticalSpacing / 2; + renderer.drawLine(0, separatorY, pageWidth - 1, separatorY, 3, true); + const int pathMaxWidth = pageWidth - metrics.contentSidePadding * 2; + // Left-truncate so the deepest directory is always visible + const char* pathStr = basepath.c_str(); + const char* pathDisplay = pathStr; + char leftTruncBuf[256]; + if (renderer.getTextWidth(SMALL_FONT_ID, pathStr) > pathMaxWidth) { + const char ellipsis[] = "\xe2\x80\xa6"; // UTF-8 ellipsis (…) + const int ellipsisWidth = renderer.getTextWidth(SMALL_FONT_ID, ellipsis); + const int available = pathMaxWidth - ellipsisWidth; + // Walk forward from the start until the suffix fits, skipping UTF-8 continuation bytes + const char* p = pathStr; + while (*p) { + if (renderer.getTextWidth(SMALL_FONT_ID, p) <= available) break; + ++p; + while (*p && (static_cast(*p) & 0xC0) == 0x80) ++p; + } + snprintf(leftTruncBuf, sizeof(leftTruncBuf), "%s%s", ellipsis, p); + pathDisplay = leftTruncBuf; + } + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, pathY, pathDisplay); } // Help text @@ -283,4 +344,4 @@ size_t FileBrowserActivity::findEntry(const std::string& name) const { for (size_t i = 0; i < files.size(); i++) if (files[i] == name) return i; return 0; -} \ No newline at end of file +} diff --git a/src/activities/home/FileBrowserActivity.h b/src/activities/home/FileBrowserActivity.h index c4f359ddf..a8235088b 100644 --- a/src/activities/home/FileBrowserActivity.h +++ b/src/activities/home/FileBrowserActivity.h @@ -17,6 +17,8 @@ class FileBrowserActivity final : public Activity { size_t selectorIndex = 0; + bool lockLongPressBack = false; + // Files state std::string basepath = "/"; std::vector files; diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 85650df9e..235a55c1f 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -217,7 +217,10 @@ void WifiSelectionActivity::attemptConnection() { connectionError.clear(); requestUpdate(); + WiFi.persistent(false); // Credentials are managed by WifiCredentialStore; suppress SDK NVS auto-connect WiFi.mode(WIFI_STA); + WiFi.disconnect(true, true); // Abort any in-progress SDK auto-connect and clear NVS-saved SSID + delay(100); // Set hostname so routers show "CrossPoint-Reader-AABBCCDDEEFF" instead of "esp32-XXXXXXXXXXXX" String mac = WiFi.macAddress(); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 16d403aaa..21e6f82e2 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -69,7 +69,6 @@ void EpubReaderActivity::onEnter() { if (dataSize == 6) { cachedChapterTotalPageCount = data[4] + (data[5] << 8); } - f.close(); } // We may want a better condition to detect if we are opening for the first time. // This will trigger if the book is re-opened at Chapter 0. @@ -181,16 +180,25 @@ void EpubReaderActivity::loop() { return; } - // any botton press when at end of the book goes back to the last page + // At end of the book, forward button goes home and back button returns to last page if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) { - currentSpineIndex = epub->getSpineItemsCount() - 1; - nextPageNumber = UINT16_MAX; - requestUpdate(); + if (nextTriggered) { + onGoHome(); + } else { + currentSpineIndex = epub->getSpineItemsCount() - 1; + nextPageNumber = UINT16_MAX; + requestUpdate(); + } return; } const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs; + // Don't skip chapter after screenshot + if (gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.wasReleased(HalGPIO::BTN_DOWN)) { + return; + } + if (skipChapter) { lastPageTurnTime = millis(); // We don't want to delete the section mid-render, so grab the semaphore @@ -687,7 +695,6 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC data[4] = pageCount & 0xFF; data[5] = (pageCount >> 8) & 0xFF; f.write(data, 6); - f.close(); LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d", spineIndex, currentPage); } else { LOG_ERR("ERS", "Could not save progress!"); diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.h b/src/activities/reader/EpubReaderChapterSelectionActivity.h index 20b53aa43..216eadf46 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.h +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.h @@ -32,4 +32,5 @@ class EpubReaderChapterSelectionActivity final : public Activity { void onExit() override; void loop() override; void render(RenderLock&&) override; + bool isReaderActivity() const override { return true; } }; diff --git a/src/activities/reader/EpubReaderFootnotesActivity.h b/src/activities/reader/EpubReaderFootnotesActivity.h index 7336d038c..85fe692d1 100644 --- a/src/activities/reader/EpubReaderFootnotesActivity.h +++ b/src/activities/reader/EpubReaderFootnotesActivity.h @@ -19,6 +19,7 @@ class EpubReaderFootnotesActivity final : public Activity { void onExit() override; void loop() override; void render(RenderLock&&) override; + bool isReaderActivity() const override { return true; } private: const std::vector& footnotes; diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 9ddba93db..3937d62c7 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -32,6 +32,7 @@ class EpubReaderMenuActivity final : public Activity { void onExit() override; void loop() override; void render(RenderLock&&) override; + bool isReaderActivity() const override { return true; } private: struct MenuItem { diff --git a/src/activities/reader/EpubReaderPercentSelectionActivity.h b/src/activities/reader/EpubReaderPercentSelectionActivity.h index 8cba8664f..ad68fc32c 100644 --- a/src/activities/reader/EpubReaderPercentSelectionActivity.h +++ b/src/activities/reader/EpubReaderPercentSelectionActivity.h @@ -15,6 +15,7 @@ class EpubReaderPercentSelectionActivity final : public Activity { void onExit() override; void loop() override; void render(RenderLock&&) override; + bool isReaderActivity() const override { return true; } private: // Current percent value (0-100) shown on the slider. diff --git a/src/activities/reader/KOReaderSyncActivity.h b/src/activities/reader/KOReaderSyncActivity.h index 71bdbf2f4..afd331ece 100644 --- a/src/activities/reader/KOReaderSyncActivity.h +++ b/src/activities/reader/KOReaderSyncActivity.h @@ -38,6 +38,7 @@ class KOReaderSyncActivity final : public Activity { void loop() override; void render(RenderLock&&) override; bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; } + bool isReaderActivity() const override { return true; } private: enum State { diff --git a/src/activities/reader/QrDisplayActivity.h b/src/activities/reader/QrDisplayActivity.h index 3cfdb6b37..d6ff236a4 100644 --- a/src/activities/reader/QrDisplayActivity.h +++ b/src/activities/reader/QrDisplayActivity.h @@ -14,6 +14,7 @@ class QrDisplayActivity final : public Activity { void onExit() override; void loop() override; void render(RenderLock&&) override; + bool isReaderActivity() const override { return true; } private: std::string textPayload; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 2164a7f66..989e6d223 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -13,14 +13,6 @@ #include "activities/util/BmpViewerActivity.h" #include "activities/util/FullScreenMessageActivity.h" -std::string ReaderActivity::extractFolderPath(const std::string& filePath) { - const auto lastSlash = filePath.find_last_of('/'); - if (lastSlash == std::string::npos || lastSlash == 0) { - return "/"; - } - return filePath.substr(0, lastSlash); -} - bool ReaderActivity::isXtcFile(const std::string& path) { return FsHelpers::hasXtcExtension(path); } bool ReaderActivity::isTxtFile(const std::string& path) { @@ -77,7 +69,7 @@ std::unique_ptr ReaderActivity::loadTxt(const std::string& path) { void ReaderActivity::goToLibrary(const std::string& fromBookPath) { // If coming from a book, start in that book's folder; otherwise start from root - auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath); + auto initialPath = fromBookPath.empty() ? "/" : FsHelpers::extractFolderPath(fromBookPath); activityManager.goToFileBrowser(std::move(initialPath)); } diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h index 6a3756db2..f5c61a393 100644 --- a/src/activities/reader/ReaderActivity.h +++ b/src/activities/reader/ReaderActivity.h @@ -18,7 +18,6 @@ class ReaderActivity final : public Activity { static bool isTxtFile(const std::string& path); static bool isBmpFile(const std::string& path); - static std::string extractFolderPath(const std::string& filePath); void goToLibrary(const std::string& fromBookPath = ""); void onGoToEpubReader(std::unique_ptr epub); void onGoToXtcReader(std::unique_ptr xtc); diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index c723bcb15..fba7bc0bc 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -79,9 +79,13 @@ void TxtReaderActivity::loop() { if (prevTriggered && currentPage > 0) { currentPage--; requestUpdate(); - } else if (nextTriggered && currentPage < totalPages - 1) { - currentPage++; - requestUpdate(); + } else if (nextTriggered) { + if (currentPage < totalPages - 1) { + currentPage++; + requestUpdate(); + } else { + onGoHome(); + } } } @@ -401,7 +405,6 @@ void TxtReaderActivity::saveProgress() const { data[2] = 0; data[3] = 0; f.write(data, 4); - f.close(); } } @@ -419,7 +422,6 @@ void TxtReaderActivity::loadProgress() { } LOG_DBG("TRS", "Loaded progress: page %d/%d", currentPage, totalPages); } - f.close(); } } @@ -448,7 +450,6 @@ bool TxtReaderActivity::loadPageIndexCache() { serialization::readPod(f, magic); if (magic != CACHE_MAGIC) { LOG_DBG("TRS", "Cache magic mismatch, rebuilding"); - f.close(); return false; } @@ -456,7 +457,6 @@ bool TxtReaderActivity::loadPageIndexCache() { serialization::readPod(f, version); if (version != CACHE_VERSION) { LOG_DBG("TRS", "Cache version mismatch (%d != %d), rebuilding", version, CACHE_VERSION); - f.close(); return false; } @@ -464,7 +464,6 @@ bool TxtReaderActivity::loadPageIndexCache() { serialization::readPod(f, fileSize); if (fileSize != txt->getFileSize()) { LOG_DBG("TRS", "Cache file size mismatch, rebuilding"); - f.close(); return false; } @@ -472,7 +471,6 @@ bool TxtReaderActivity::loadPageIndexCache() { serialization::readPod(f, cachedWidth); if (cachedWidth != viewportWidth) { LOG_DBG("TRS", "Cache viewport width mismatch, rebuilding"); - f.close(); return false; } @@ -480,7 +478,6 @@ bool TxtReaderActivity::loadPageIndexCache() { serialization::readPod(f, cachedLines); if (cachedLines != linesPerPage) { LOG_DBG("TRS", "Cache lines per page mismatch, rebuilding"); - f.close(); return false; } @@ -488,7 +485,6 @@ bool TxtReaderActivity::loadPageIndexCache() { serialization::readPod(f, fontId); if (fontId != cachedFontId) { LOG_DBG("TRS", "Cache font ID mismatch (%d != %d), rebuilding", fontId, cachedFontId); - f.close(); return false; } @@ -496,7 +492,6 @@ bool TxtReaderActivity::loadPageIndexCache() { serialization::readPod(f, margin); if (margin != cachedScreenMargin) { LOG_DBG("TRS", "Cache screen margin mismatch, rebuilding"); - f.close(); return false; } @@ -504,7 +499,6 @@ bool TxtReaderActivity::loadPageIndexCache() { serialization::readPod(f, alignment); if (alignment != cachedParagraphAlignment) { LOG_DBG("TRS", "Cache paragraph alignment mismatch, rebuilding"); - f.close(); return false; } @@ -521,7 +515,6 @@ bool TxtReaderActivity::loadPageIndexCache() { pageOffsets.push_back(offset); } - f.close(); totalPages = pageOffsets.size(); LOG_DBG("TRS", "Loaded page index cache: %d pages", totalPages); return true; @@ -551,6 +544,5 @@ void TxtReaderActivity::savePageIndexCache() const { serialization::writePod(f, static_cast(offset)); } - f.close(); LOG_DBG("TRS", "Saved page index cache: %d pages", totalPages); } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 84cc51da2..d2b6f18ea 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -98,10 +98,14 @@ void XtcReaderActivity::loop() { return; } - // Handle end of book + // At end of the book, forward button goes home and back button returns to last page if (currentPage >= xtc->getPageCount()) { - currentPage = xtc->getPageCount() - 1; - requestUpdate(); + if (nextTriggered) { + onGoHome(); + } else { + currentPage = xtc->getPageCount() - 1; + requestUpdate(); + } return; } diff --git a/src/activities/util/BmpViewerActivity.cpp b/src/activities/util/BmpViewerActivity.cpp index 51fd12b10..37fa8fe19 100644 --- a/src/activities/util/BmpViewerActivity.cpp +++ b/src/activities/util/BmpViewerActivity.cpp @@ -61,7 +61,7 @@ void BmpViewerActivity::onEnter() { GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); // Single pass for non-grayscale images - renderer.displayBuffer(HalDisplay::FULL_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } else { // Handle file parsing error @@ -69,7 +69,7 @@ void BmpViewerActivity::onEnter() { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Invalid BMP File"); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(HalDisplay::FAST_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } file.close(); @@ -79,14 +79,14 @@ void BmpViewerActivity::onEnter() { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Could not open file"); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(HalDisplay::FULL_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } } void BmpViewerActivity::onExit() { Activity::onExit(); renderer.clearScreen(); - renderer.displayBuffer(HalDisplay::FAST_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } void BmpViewerActivity::loop() { @@ -94,7 +94,7 @@ void BmpViewerActivity::loop() { Activity::loop(); if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - onGoHome(); + activityManager.goToFileBrowser(filePath); return; } } \ No newline at end of file diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 57e494840..425981a21 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -49,7 +49,7 @@ void UITheme::setTheme(CrossPointSettings::UI_THEME type) { } int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints, - bool hasSubtitle) { + bool hasSubtitle, int extraReservedHeight) { const ThemeMetrics& metrics = UITheme::getInstance().getMetrics(); int reservedHeight = metrics.topPadding; if (hasHeader) { @@ -61,7 +61,7 @@ int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader if (hasButtonHints) { reservedHeight += metrics.verticalSpacing + metrics.buttonHintsHeight; } - const int availableHeight = renderer.getScreenHeight() - reservedHeight; + const int availableHeight = renderer.getScreenHeight() - reservedHeight - extraReservedHeight; int rowHeight = hasSubtitle ? metrics.listWithSubtitleRowHeight : metrics.listRowHeight; return availableHeight / rowHeight; } diff --git a/src/components/UITheme.h b/src/components/UITheme.h index daa1ec452..c4c37235c 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -19,7 +19,7 @@ class UITheme { void reload(); void setTheme(CrossPointSettings::UI_THEME type); static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints, - bool hasSubtitle); + bool hasSubtitle, int extraReservedHeight = 0); static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight); static UIIcon getFileIcon(const std::string& filename); static int getStatusBarHeight(); diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 055b37a7a..dff5ec065 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -16,24 +16,14 @@ // Internal constants namespace { -constexpr int batteryPercentSpacing = 4; constexpr int homeMenuMargin = 20; constexpr int homeMarginTop = 30; constexpr int subtitleY = 738; // Helper: draw battery icon at given position void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) { - // Top line - renderer.drawLine(x + 1, y, x + battWidth - 3, y); - // Bottom line - renderer.drawLine(x + 1, y + rectHeight - 1, x + battWidth - 3, y + rectHeight - 1); - // Left line - renderer.drawLine(x, y + 1, x, y + rectHeight - 2); - // Battery end - renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rectHeight - 2); - renderer.drawPixel(x + battWidth - 1, y + 3); - renderer.drawPixel(x + battWidth - 1, y + rectHeight - 4); - renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rectHeight - 5); + // Draw battery outline (shared code) + BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight); const bool charging = gpio.isUsbConnected(); @@ -58,20 +48,37 @@ void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, i // Draw lightning bolt when charging (white/inverted on black fill for visibility) if (charging) { - const int boltX = x + 4; - const int boltY = y + 2; - renderer.drawLine(boltX + 4, boltY + 0, boltX + 5, boltY + 0, false); - renderer.drawLine(boltX + 3, boltY + 1, boltX + 4, boltY + 1, false); - renderer.drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 2, false); - renderer.drawLine(boltX + 3, boltY + 3, boltX + 4, boltY + 3, false); - renderer.drawLine(boltX + 2, boltY + 4, boltX + 3, boltY + 4, false); - renderer.drawLine(boltX + 1, boltY + 5, boltX + 4, boltY + 5, false); - renderer.drawLine(boltX + 2, boltY + 6, boltX + 3, boltY + 6, false); - renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false); + BaseTheme::drawBatteryLightningBolt(renderer, x + 4, y + 2); } } } // namespace +void BaseTheme::drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight) { + // Top line + renderer.drawLine(x + 1, y, x + battWidth - 3, y); + // Bottom line + renderer.drawLine(x + 1, y + rectHeight - 1, x + battWidth - 3, y + rectHeight - 1); + // Left line + renderer.drawLine(x, y + 1, x, y + rectHeight - 2); + // Battery end + renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rectHeight - 2); + renderer.drawPixel(x + battWidth - 1, y + 3); + renderer.drawPixel(x + battWidth - 1, y + rectHeight - 4); + renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rectHeight - 5); +} + +void BaseTheme::drawBatteryLightningBolt(const GfxRenderer& renderer, int boltX, int boltY) { + // Draw lightning bolt (white/inverted on black fill for visibility) + renderer.drawLine(boltX + 4, boltY + 0, boltX + 5, boltY + 0, false); + renderer.drawLine(boltX + 3, boltY + 1, boltX + 4, boltY + 1, false); + renderer.drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 2, false); + renderer.drawLine(boltX + 3, boltY + 3, boltX + 4, boltY + 3, false); + renderer.drawLine(boltX + 2, boltY + 4, boltX + 3, boltY + 4, false); + renderer.drawLine(boltX + 1, boltY + 5, boltX + 4, boltY + 5, false); + renderer.drawLine(boltX + 2, boltY + 6, boltX + 3, boltY + 6, false); + renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false); +} + void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { // Left aligned: icon on left, percentage on right (reader mode) const uint16_t percentage = powerManager.getBatteryPercentage(); @@ -79,8 +86,8 @@ void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bo if (showPercentage) { const auto percentageText = std::to_string(percentage) + "%"; - renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + BaseMetrics::values.batteryWidth, rect.y, - percentageText.c_str()); + renderer.drawText(SMALL_FONT_ID, rect.x + BaseTheme::batteryPercentSpacing + BaseMetrics::values.batteryWidth, + rect.y, percentageText.c_str()); } drawBatteryIcon(renderer, rect.x, y, BaseMetrics::values.batteryWidth, rect.height, percentage); @@ -97,9 +104,10 @@ void BaseTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const b const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); // Clear the area where we're going to draw the text to prevent ghosting const auto textHeight = renderer.getTextHeight(SMALL_FONT_ID); - renderer.fillRect(rect.x - textWidth - batteryPercentSpacing, rect.y, textWidth, textHeight, false); + renderer.fillRect(rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y, textWidth, textHeight, false); // Draw text to the left of the icon - renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - batteryPercentSpacing, rect.y, percentageText.c_str()); + renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y, + percentageText.c_str()); } // Icon is already at correct position from rect.x @@ -422,7 +430,6 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: bookWidth = rect.width / 2; // Fallback } } - file.close(); } } @@ -476,7 +483,6 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4); } } - file.close(); } } @@ -642,7 +648,8 @@ void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount Rect BaseTheme::drawPopup(const GfxRenderer& renderer, const char* message) const { constexpr int margin = 15; - constexpr int y = 60; + // Scale y position proportionally to screen height (7.5% from top) + const int y = static_cast(renderer.getScreenHeight() * 0.075f); const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::BOLD); const int textHeight = renderer.getLineHeight(UI_12_FONT_ID); const int w = textWidth + margin * 2; diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index 6878f5558..5476c085e 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -142,4 +142,9 @@ class BaseTheme { virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const; virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const; virtual bool showsFileIcons() const { return false; } + + // Shared constants and helpers for battery drawing (used by all themes) + static constexpr int batteryPercentSpacing = 4; + static void drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight); + static void drawBatteryLightningBolt(const GfxRenderer& renderer, int boltX, int boltY); }; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 0eee84a29..56903f892 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -30,7 +30,6 @@ // Internal constants namespace { -constexpr int batteryPercentSpacing = 4; constexpr int hPaddingInSelection = 8; constexpr int cornerRadius = 6; constexpr int topHintButtonY = 345; @@ -45,42 +44,25 @@ int coverWidth = 0; void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) { - // Top line - renderer.drawLine(x + 1, y, x + battWidth - 3, y); - // Bottom line - renderer.drawLine(x + 1, y + rectHeight - 1, x + battWidth - 3, y + rectHeight - 1); - // Left line - renderer.drawLine(x, y + 1, x, y + rectHeight - 2); - // Battery end - renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rectHeight - 2); - renderer.drawPixel(x + battWidth - 1, y + 3); - renderer.drawPixel(x + battWidth - 1, y + rectHeight - 4); - renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rectHeight - 5); + BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight); const bool charging = gpio.isUsbConnected(); - // Draw bars - if (percentage > 10 || charging) { - renderer.fillRect(x + 2, y + 2, 3, rectHeight - 4); - } - if (percentage > 40 || charging) { - renderer.fillRect(x + 6, y + 2, 3, rectHeight - 4); - } - if (percentage > 70) { - renderer.fillRect(x + 10, y + 2, 3, rectHeight - 4); - } - if (charging) { - const int boltX = x + 4; - const int boltY = y + 2; - renderer.drawLine(boltX + 4, boltY + 0, boltX + 5, boltY + 0, false); - renderer.drawLine(boltX + 3, boltY + 1, boltX + 4, boltY + 1, false); - renderer.drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 2, false); - renderer.drawLine(boltX + 3, boltY + 3, boltX + 4, boltY + 3, false); - renderer.drawLine(boltX + 2, boltY + 4, boltX + 3, boltY + 4, false); - renderer.drawLine(boltX + 1, boltY + 5, boltX + 4, boltY + 5, false); - renderer.drawLine(boltX + 2, boltY + 6, boltX + 3, boltY + 6, false); - renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false); + // Draw solid fill when charging so lightning bolt is visible + renderer.fillRect(x + 2, y + 2, battWidth - 5, rectHeight - 4); + BaseTheme::drawBatteryLightningBolt(renderer, x + 4, y + 2); + } else { + // Draw bars when not charging + if (percentage > 10) { + renderer.fillRect(x + 2, y + 2, 3, rectHeight - 4); + } + if (percentage > 40) { + renderer.fillRect(x + 6, y + 2, 3, rectHeight - 4); + } + if (percentage > 70) { + renderer.fillRect(x + 10, y + 2, 3, rectHeight - 4); + } } } @@ -132,8 +114,8 @@ void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bo if (showPercentage) { const auto percentageText = std::to_string(percentage) + "%"; - renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + LyraMetrics::values.batteryWidth, rect.y, - percentageText.c_str()); + renderer.drawText(SMALL_FONT_ID, rect.x + BaseTheme::batteryPercentSpacing + LyraMetrics::values.batteryWidth, + rect.y, percentageText.c_str()); } drawLyraBatteryIcon(renderer, rect.x, rect.y + 6, LyraMetrics::values.batteryWidth, rect.height, percentage); @@ -148,9 +130,10 @@ void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const b const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); // Clear the area where we're going to draw the text to prevent ghosting const auto textHeight = renderer.getTextHeight(SMALL_FONT_ID); - renderer.fillRect(rect.x - textWidth - batteryPercentSpacing, rect.y, textWidth, textHeight, false); + renderer.fillRect(rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y, textWidth, textHeight, false); // Draw text to the left of the icon - renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - batteryPercentSpacing, rect.y, percentageText.c_str()); + renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y, + percentageText.c_str()); } drawLyraBatteryIcon(renderer, rect.x, rect.y + 6, LyraMetrics::values.batteryWidth, rect.height, percentage); @@ -558,7 +541,8 @@ void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount } Rect LyraTheme::drawPopup(const GfxRenderer& renderer, const char* message) const { - constexpr int y = 132; + // Scale y position proportionally to screen height (16.5% from top) + const int y = static_cast(renderer.getScreenHeight() * 0.165f); constexpr int outline = 2; const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::REGULAR); const int textHeight = renderer.getLineHeight(UI_12_FONT_ID); diff --git a/src/main.cpp b/src/main.cpp index 5ba58cbcb..e51a24333 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -253,7 +253,6 @@ void setup() { } HalSystem::checkPanic(); - HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info SETTINGS.loadFromFile(); I18N.loadSettings(); @@ -290,10 +289,13 @@ void setup() { APP_STATE.loadFromFile(); RECENT_BOOKS.loadFromFile(); - // Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity - // crashed (indicated by readerActivityLoadCount > 0) - if (APP_STATE.openEpubPath.empty() || !APP_STATE.lastSleepFromReader || - mappedInputManager.isPressed(MappedInputManager::Button::Back) || APP_STATE.readerActivityLoadCount > 0) { + if (HalSystem::isRebootFromPanic()) { + // If we rebooted from a panic, go to crash report screen to show the panic info + activityManager.goToCrashReport(); + } else if (APP_STATE.openEpubPath.empty() || !APP_STATE.lastSleepFromReader || + mappedInputManager.isPressed(MappedInputManager::Button::Back) || APP_STATE.readerActivityLoadCount > 0) { + // Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity + // crashed (indicated by readerActivityLoadCount > 0) activityManager.goHome(); } else { // Clear app state to avoid getting into a boot loop if the epub doesn't load @@ -379,6 +381,14 @@ void loop() { return; } + // Refresh screen when power button is short-pressed with FORCE_REFRESH setting. + if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::FORCE_REFRESH && + mappedInputManager.wasReleased(MappedInputManager::Button::Power)) { + LOG_DBG("MAIN", "Manual screen refresh triggered"); + RenderLock lock; + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + } + // Refresh the battery icon when USB is plugged or unplugged. // Placed after sleep guards so we never queue a render that won't be processed. if (gpio.wasUsbStateChanged()) { @@ -413,4 +423,4 @@ void loop() { delay(10); } } -} \ No newline at end of file +} diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index dba8faa2d..d18eebd13 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -193,6 +193,7 @@ void CrossPointWebServer::begin() { } void CrossPointWebServer::abortWsUpload(const char* tag) { + // Explicit close() required: file-scope global persists beyond function scope wsUploadFile.close(); String filePath = wsUploadPath; if (!filePath.endsWith("/")) filePath += "/"; @@ -959,16 +960,31 @@ void CrossPointWebServer::handleMove() const { } void CrossPointWebServer::handleDelete() const { - // Check if 'paths' argument is provided - if (!server->hasArg("paths")) { - server->send(400, "text/plain", "Missing paths"); + // To ensure backwards compatibility, plain `path` is mapped + // to a single element JSON array. + bool hasPathArg = server->hasArg("path"); + bool hasPathsArg = server->hasArg("paths"); + // Check 'paths' or `path` argument is provided + if (!(hasPathArg || hasPathsArg)) { + server->send(400, "text/plain", "Missing `path` or `paths` argument"); + return; + } + if (hasPathArg && hasPathsArg) { + server->send(400, "text/plain", "Provide either 'path' or 'paths', not both"); return; } // Parse paths - String pathsArg = server->arg("paths"); + String pathsArg; JsonDocument doc; - DeserializationError error = deserializeJson(doc, pathsArg); + DeserializationError error = DeserializationError(DeserializationError::Code::Ok); + if (hasPathsArg) { + pathsArg = server->arg("paths"); + error = deserializeJson(doc, pathsArg); + } else { + pathsArg = server->arg("path"); + doc.add(pathsArg); + } if (error) { server->send(400, "text/plain", "Invalid paths format"); return; @@ -1327,6 +1343,7 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* // Zero-byte upload: complete immediately without waiting for BIN frames if (wsUploadSize == 0) { + // Explicit close() required: file-scope global persists beyond function scope wsUploadFile.close(); wsLastCompleteName = wsUploadFileName; wsLastCompleteSize = 0; @@ -1382,6 +1399,7 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* // Check if upload complete if (wsUploadReceived >= wsUploadSize) { + // Explicit close() required: file-scope global persists beyond function scope wsUploadFile.close(); wsUploadInProgress = false; wsUploadClientNum = 255; diff --git a/src/util/ScreenshotUtil.cpp b/src/util/ScreenshotUtil.cpp index 6713bd305..a152488d7 100644 --- a/src/util/ScreenshotUtil.cpp +++ b/src/util/ScreenshotUtil.cpp @@ -14,8 +14,8 @@ void ScreenshotUtil::takeScreenshot(GfxRenderer& renderer) { const uint8_t* fb = renderer.getFrameBuffer(); if (fb) { String filename_str = "/screenshots/screenshot-" + String(millis()) + ".bmp"; - if (ScreenshotUtil::saveFramebufferAsBmp(filename_str.c_str(), fb, HalDisplay::DISPLAY_WIDTH, - HalDisplay::DISPLAY_HEIGHT)) { + if (ScreenshotUtil::saveFramebufferAsBmp(filename_str.c_str(), fb, renderer.getDisplayWidth(), + renderer.getDisplayHeight())) { LOG_DBG("SCR", "Screenshot saved to %s", filename_str.c_str()); } else { LOG_ERR("SCR", "Failed to save screenshot"); @@ -26,7 +26,7 @@ void ScreenshotUtil::takeScreenshot(GfxRenderer& renderer) { // Display a border around the screen to indicate a screenshot was taken if (renderer.storeBwBuffer()) { - renderer.drawRect(6, 6, HalDisplay::DISPLAY_HEIGHT - 12, HalDisplay::DISPLAY_WIDTH - 12, 2, true); + renderer.drawRect(6, 6, renderer.getDisplayHeight() - 12, renderer.getDisplayWidth() - 12, 2, true); renderer.displayBuffer(); delay(1000); renderer.restoreBwBuffer(); @@ -62,7 +62,7 @@ bool ScreenshotUtil::saveFramebufferAsBmp(const char* filename, const uint8_t* f BmpHeader header; - createBmpHeader(&header, phyWidth, phyHeight); + createBmpHeader(&header, phyWidth, phyHeight, BmpRowOrder::BottomUp); bool write_error = false; if (file.write(reinterpret_cast(&header), sizeof(header)) != sizeof(header)) { @@ -70,16 +70,18 @@ bool ScreenshotUtil::saveFramebufferAsBmp(const char* filename, const uint8_t* f } if (write_error) { + // Explicitly close() file before calling Storage.remove() file.close(); Storage.remove(filename); return false; } const uint32_t rowSizePadded = (phyWidth + 31) / 32 * 4; - // Max row size for 480px width = 60 bytes; use fixed buffer to avoid VLA - constexpr size_t kMaxRowSize = 64; + // Max row size for 528px height (X3) after rotation = 68 bytes; use fixed buffer to avoid VLA + constexpr size_t kMaxRowSize = 68; if (rowSizePadded > kMaxRowSize) { LOG_ERR("SCR", "Row size %u exceeds buffer capacity", rowSizePadded); + // Explicitly close() file before calling Storage.remove() file.close(); Storage.remove(filename); return false; @@ -106,6 +108,7 @@ bool ScreenshotUtil::saveFramebufferAsBmp(const char* filename, const uint8_t* f memset(rowBuffer, 0, rowSizePadded); // Clear the buffer for the next row } + // Explicitly close() file before calling Storage.remove() file.close(); if (write_error) { diff --git a/test/differential_rounding/DifferentialRoundingTest.cpp b/test/differential_rounding/DifferentialRoundingTest.cpp new file mode 100644 index 000000000..fd4548399 --- /dev/null +++ b/test/differential_rounding/DifferentialRoundingTest.cpp @@ -0,0 +1,381 @@ +#include +#include +#include +#include + +#include "lib/EpdFont/EpdFont.h" +#include "lib/EpdFont/EpdFontData.h" + +static int testsPassed = 0; +static int testsFailed = 0; + +#define ASSERT_EQ(a, b) \ + do { \ + if ((a) != (b)) { \ + fprintf(stderr, " FAIL: %s:%d: %s == %d, expected %d\n", __FILE__, __LINE__, #a, (a), (b)); \ + testsFailed++; \ + return; \ + } \ + } while (0) + +#define ASSERT_TRUE(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + testsFailed++; \ + return; \ + } \ + } while (0) + +#define PASS() testsPassed++ + +// ============================================================================ +// Synthetic test font +// +// Glyphs: 'T' (0x54), 'a' (0x61), 'o' (0x6F), 'x' (0x78) +// - 'x' advance is 136 FP (8.5px) -- frac = 8, exactly at the rounding +// boundary where absolute vs differential snapping diverges for "oo". +// - No U+FFFD replacement glyph, so unknown codepoints trigger the +// null-glyph path in getTextBounds. +// +// Kern pairs (4.4 fixed-point): +// T->a: -5 (-0.3125px) T->o: -7 (-0.4375px) +// o->a: -2 (-0.125px) o->o: -3 (-0.1875px) +// ============================================================================ + +// clang-format off +static const EpdGlyph kGlyphs[] = { + // idx width height advanceX left top dataLength dataOffset + /* 0 'T' */ { 8, 12, 137, 0, 12, 0, 0 }, + /* 1 'a' */ { 7, 8, 130, 0, 8, 0, 0 }, + /* 2 'o' */ { 8, 8, 145, 0, 8, 0, 0 }, + /* 3 'x' */ { 7, 8, 136, 0, 8, 0, 0 }, +}; + +static const EpdUnicodeInterval kIntervals[] = { + { 0x54, 0x54, 0 }, // 'T' -> glyph[0] + { 0x61, 0x61, 1 }, // 'a' -> glyph[1] + { 0x6F, 0x6F, 2 }, // 'o' -> glyph[2] + { 0x78, 0x78, 3 }, // 'x' -> glyph[3] +}; + +static const EpdKernClassEntry kKernLeft[] = { + { 0x54, 1 }, // 'T' -> left class 1 + { 0x6F, 2 }, // 'o' -> left class 2 +}; + +static const EpdKernClassEntry kKernRight[] = { + { 0x61, 1 }, // 'a' -> right class 1 + { 0x6F, 2 }, // 'o' -> right class 2 +}; + +// Flat matrix: leftClassCount(2) x rightClassCount(2), 4.4 fixed-point +// [L1,R1]=kern(T,a) [L1,R2]=kern(T,o) [L2,R1]=kern(o,a) [L2,R2]=kern(o,o) +static const int8_t kKernMatrix[] = { -5, -7, -2, -3 }; + +static const EpdFontData kTestFontData = { + .bitmap = nullptr, + .glyph = kGlyphs, + .intervals = kIntervals, + .intervalCount = 4, + .advanceY = 16, + .ascender = 12, + .descender = 0, + .is2Bit = false, + .groups = nullptr, + .groupCount = 0, + .glyphToGroup = nullptr, + .kernLeftClasses = kKernLeft, + .kernRightClasses = kKernRight, + .kernMatrix = kKernMatrix, + .kernLeftEntryCount = 2, + .kernRightEntryCount = 2, + .kernLeftClassCount = 2, + .kernRightClassCount = 2, + .ligaturePairs = nullptr, + .ligaturePairCount = 0, +}; +// clang-format on + +static EpdFont testFont(&kTestFontData); + +// Helper: return width from getTextDimensions +static int textWidth(const char* str) { + int w = 0, h = 0; + testFont.getTextDimensions(str, &w, &h); + return w; +} + +static int textHeight(const char* str) { + int w = 0, h = 0; + testFont.getTextDimensions(str, &w, &h); + return h; +} + +// ============================================================================ +// Part 1: Pure fp4 math tests +// ============================================================================ + +// Simulate the old absolute-snap gap for comparison +static int absoluteGap(int32_t startFP, int32_t advanceFP, int32_t kernFP) { + int32_t nextFP = startFP + advanceFP + kernFP; + return fp4::toPixel(nextFP) - fp4::toPixel(startFP); +} + +void testFp4Basics() { + printf("testFp4Basics...\n"); + + for (int px = 0; px < 500; px++) { + ASSERT_EQ(fp4::toPixel(fp4::fromPixel(px)), px); + } + + ASSERT_EQ(fp4::toPixel(0), 0); + ASSERT_EQ(fp4::toPixel(7), 0); // 0.4375 -> 0 + ASSERT_EQ(fp4::toPixel(8), 1); // 0.5 -> 1 (round half up) + ASSERT_EQ(fp4::toPixel(15), 1); // 0.9375 -> 1 + ASSERT_EQ(fp4::toPixel(16), 1); // 1.0 -> 1 + ASSERT_EQ(fp4::toPixel(24), 2); // 1.5 -> 2 + ASSERT_EQ(fp4::toPixel(-8), 0); // -0.5 -> 0 + ASSERT_EQ(fp4::toPixel(-9), -1); // -0.5625 -> -1 + ASSERT_EQ(fp4::toPixel(-16), -1); + + ASSERT_EQ(fp4::toPixel(137 + (-9)), 8); // 128 = 8.0 exact + ASSERT_EQ(fp4::toPixel(137 + (-5)), 8); // 132 = 8.25 + ASSERT_EQ(fp4::toPixel(137 + (-1)), 9); // 136 = 8.5 (half rounds up) + + printf(" All fp4 basics passed\n"); + PASS(); +} + +void testOldApproachInconsistency() { + printf("testOldApproachInconsistency...\n"); + + // 'oo' pair: advance=145 (9.0625px), kern=-3 (-0.1875px), combined=142 (8.875px) + const int32_t advance = 145; + const int32_t kern = -3; + + int minGap = 999, maxGap = -999; + for (int startPx = 0; startPx < 100; startPx++) { + for (int frac = 0; frac < 16; frac++) { + int32_t startFP = fp4::fromPixel(startPx) + frac; + int gap = absoluteGap(startFP, advance, kern); + if (gap < minGap) minGap = gap; + if (gap > maxGap) maxGap = gap; + } + } + + ASSERT_TRUE(maxGap - minGap >= 1); + printf(" Old absolute gap range: [%d, %d] -- varies by %d px\n", minGap, maxGap, maxGap - minGap); + + int diffStep = fp4::toPixel(advance + kern); + printf(" Differential step: always %d px\n", diffStep); + PASS(); +} + +void testExhaustiveKernRange() { + printf("testExhaustiveKernRange...\n"); + + const int32_t baseAdvance = 128; + int checked = 0; + + for (int advFrac = 0; advFrac < 16; advFrac++) { + int32_t advance = baseAdvance + advFrac; + for (int kern = -128; kern <= 127; kern++) { + int step = fp4::toPixel(advance + static_cast(kern)); + float idealPx = fp4::toFloat(advance + kern); + if (std::abs(step - idealPx) >= 1.0f) { + fprintf(stderr, " FAIL: advance=%d, kern=%d, step=%d, ideal=%.4f\n", advance, kern, step, idealPx); + testsFailed++; + return; + } + checked++; + } + } + + printf(" Checked %d (advance, kern) combinations -- all within 1px of ideal\n", checked); + PASS(); +} + +// ============================================================================ +// Part 2: Integration tests using real EpdFont::getTextDimensions +// ============================================================================ + +void testKernLookup() { + printf("testKernLookup...\n"); + + ASSERT_EQ(testFont.getKerning('T', 'a'), -5); + ASSERT_EQ(testFont.getKerning('T', 'o'), -7); + ASSERT_EQ(testFont.getKerning('o', 'a'), -2); + ASSERT_EQ(testFont.getKerning('o', 'o'), -3); + ASSERT_EQ(testFont.getKerning('a', 'o'), 0); // 'a' has no left class + ASSERT_EQ(testFont.getKerning('x', 'o'), 0); // 'x' has no left class + ASSERT_EQ(testFont.getKerning('T', 'x'), 0); // 'x' has no right class + ASSERT_EQ(testFont.getKerning('T', 'T'), 0); // 'T' has no right class + + printf(" All kern lookups correct\n"); + PASS(); +} + +void testGlyphLookup() { + printf("testGlyphLookup...\n"); + + ASSERT_TRUE(testFont.getGlyph('T') != nullptr); + ASSERT_TRUE(testFont.getGlyph('a') != nullptr); + ASSERT_TRUE(testFont.getGlyph('o') != nullptr); + ASSERT_TRUE(testFont.getGlyph('x') != nullptr); + ASSERT_EQ(testFont.getGlyph('T')->advanceX, 137); + ASSERT_EQ(testFont.getGlyph('a')->advanceX, 130); + ASSERT_EQ(testFont.getGlyph('o')->advanceX, 145); + ASSERT_EQ(testFont.getGlyph('x')->advanceX, 136); + + // No U+FFFD in font, so unknown codepoints return nullptr + ASSERT_TRUE(testFont.getGlyph('Z') == nullptr); + ASSERT_TRUE(testFont.getGlyph('b') == nullptr); + + printf(" All glyph lookups correct\n"); + PASS(); +} + +// Known-value regression tests. Expected widths are computed by hand using +// differential rounding. If someone reverts to absolute snapping, specific +// test cases will fail. +// +// Layout trace for each string (all glyphs have left=0): +// width = max glyph right edge = lastBaseX + glyph.width +// +// Differential step from glyph A to glyph B: +// step = fp4::toPixel(advanceA + kern(A,B)) +void testKnownWidths() { + printf("testKnownWidths...\n"); + + // "o": single glyph at x=0, width=8 + // w = 0 + 8 = 8 + ASSERT_EQ(textWidth("o"), 8); + + // "oo": step = toPixel(145 + (-3)) = toPixel(142) = 9 + // o1 at 0, o2 at 9. w = 9 + 8 = 17 + ASSERT_EQ(textWidth("oo"), 17); + + // "ooo": two steps of 9 + // o1 at 0, o2 at 9, o3 at 18. w = 18 + 8 = 26 + ASSERT_EQ(textWidth("ooo"), 26); + + // "To": step = toPixel(137 + (-7)) = toPixel(130) = 8 + // T at 0, o at 8. w = 8 + 8 = 16 + ASSERT_EQ(textWidth("To"), 16); + + // "Ta": step = toPixel(137 + (-5)) = toPixel(132) = 8 + // T at 0, a at 8. w = 8 + 7 = 15 + ASSERT_EQ(textWidth("Ta"), 15); + + // "oa": step = toPixel(145 + (-2)) = toPixel(143) = 9 + // o at 0, a at 9. w = 9 + 7 = 16 + ASSERT_EQ(textWidth("oa"), 16); + + // "Too": T at 0. + // step T->o = toPixel(137 + (-7)) = 8. o1 at 8. + // step o->o = toPixel(145 + (-3)) = 9. o2 at 17. + // w = 17 + 8 = 25 + ASSERT_EQ(textWidth("Too"), 25); + + // "xo": step = toPixel(136 + 0) = toPixel(136) = 9 (no kern: x has no left class) + // x at 0, o at 9. w = 9 + 8 = 17 + ASSERT_EQ(textWidth("xo"), 17); + + printf(" All known widths correct\n"); + PASS(); +} + +// "oo" pair consistency: the pixel gap between two o's must be the same +// regardless of what prefix precedes them. This is THE key property of +// differential rounding. With absolute snapping, "xoo" would produce a +// different oo gap than "oo" because 'x' advance (136 FP) puts the first +// 'o' at fractional phase 8, crossing the rounding boundary differently. +void testPairConsistencyViaFont() { + printf("testPairConsistencyViaFont...\n"); + + // The oo gap = width(prefix + "oo") - width(prefix + "o") + // This isolates the pixel distance contributed by the second 'o'. + const int oo_gap_bare = textWidth("oo") - textWidth("o"); + const int oo_gap_after_x = textWidth("xoo") - textWidth("xo"); + const int oo_gap_after_T = textWidth("Too") - textWidth("To"); + const int oo_gap_after_o = textWidth("ooo") - textWidth("oo"); + + printf(" oo gap (bare): %d\n", oo_gap_bare); + printf(" oo gap (after x): %d\n", oo_gap_after_x); + printf(" oo gap (after T): %d\n", oo_gap_after_T); + printf(" oo gap (after o): %d\n", oo_gap_after_o); + + // All must be identical + ASSERT_EQ(oo_gap_after_x, oo_gap_bare); + ASSERT_EQ(oo_gap_after_T, oo_gap_bare); + ASSERT_EQ(oo_gap_after_o, oo_gap_bare); + + printf(" All oo gaps identical (%d px) regardless of prefix\n", oo_gap_bare); + PASS(); +} + +// Null-glyph handling: when a codepoint has no glyph (and no replacement +// glyph), the pending advance from the previous glyph must still be flushed. +// Without the flush fix, the glyph after the null would overlap the one before. +void testNullGlyphAdvancePreserved() { + printf("testNullGlyphAdvancePreserved...\n"); + + // 'Z' (0x5A) is not in our font and there's no U+FFFD, so getGlyph returns null. + // "oZo" should lay out as: o1 at 0, Z skipped (advance flushed), o2 at 9. + // toPixel(145) = 9 (o's advance, no kern since Z resets prevCp). + // w = 9 + 8 = 17 + int w = textWidth("oZo"); + printf(" width(\"oZo\") = %d\n", w); + + // Without the flush fix, o2 would land at 0 (overlapping o1), giving w = 8. + ASSERT_TRUE(w > 8); + ASSERT_EQ(w, 17); + + // Multi-null: "oZZo" -- two consecutive nulls, advance still preserved. + w = textWidth("oZZo"); + printf(" width(\"oZZo\") = %d\n", w); + ASSERT_EQ(w, 17); + + // Null at start: "Zo" -- no pending advance to flush, o renders at 0. + w = textWidth("Zo"); + printf(" width(\"Zo\") = %d\n", w); + ASSERT_EQ(w, 8); + + printf(" Null-glyph advance correctly preserved\n"); + PASS(); +} + +void testHeightCalculation() { + printf("testHeightCalculation...\n"); + + // 'T' is tallest: top=12, height=12 -> extent [0, 12) + // 'o' and 'a': top=8, height=8 -> extent [0, 8) + ASSERT_EQ(textHeight("o"), 8); + ASSERT_EQ(textHeight("T"), 12); + ASSERT_EQ(textHeight("To"), 12); + ASSERT_EQ(textHeight("oo"), 8); + + printf(" All heights correct\n"); + PASS(); +} + +int main() { + printf("=== Differential Rounding Tests ===\n\n"); + + // Part 1: Pure fp4 math + testFp4Basics(); + testOldApproachInconsistency(); + testExhaustiveKernRange(); + + // Part 2: Integration tests against real EpdFont + testKernLookup(); + testGlyphLookup(); + testKnownWidths(); + testPairConsistencyViaFont(); + testNullGlyphAdvancePreserved(); + testHeightCalculation(); + + printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed); + return testsFailed > 0 ? 1 : 0; +} diff --git a/test/run_differential_rounding_test.sh b/test/run_differential_rounding_test.sh new file mode 100755 index 000000000..dd7aa5c7a --- /dev/null +++ b/test/run_differential_rounding_test.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BUILD_DIR="$ROOT_DIR/build/differential_rounding" +BINARY="$BUILD_DIR/DifferentialRoundingTest" + +mkdir -p "$BUILD_DIR" + +SOURCES=( + "$ROOT_DIR/test/differential_rounding/DifferentialRoundingTest.cpp" + "$ROOT_DIR/lib/EpdFont/EpdFont.cpp" + "$ROOT_DIR/lib/Utf8/Utf8.cpp" +) + +CXXFLAGS=( + -std=c++20 + -O2 + -Wall + -Wextra + -pedantic + -I"$ROOT_DIR" + -I"$ROOT_DIR/lib" + -I"$ROOT_DIR/lib/EpdFont" + -I"$ROOT_DIR/lib/Utf8" +) + +c++ "${CXXFLAGS[@]}" "${SOURCES[@]}" -o "$BINARY" + +"$BINARY" "$@"