Merge pull request #243 from jpirnay/fi-rebuildcovers

fix: rebuild covers
This commit is contained in:
jpirnay
2026-05-19 13:49:26 +02:00
committed by GitHub
10 changed files with 232 additions and 32 deletions
+37 -5
View File
@@ -457,9 +457,41 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
return false; return false;
} }
const int effectiveSrcW = progressive ? (srcWidth + 7) / 8 : srcWidth; // Progressive JPEGs must stay at 1/8 (DC-only mode, only safe scale with MCU_SKIP patch).
const int effectiveSrcH = progressive ? (srcHeight + 7) / 8 : srcHeight; // For baseline JPEGs with a target size, pick the largest built-in DCT scale that still
const int decodeFlags = progressive ? JPEG_SCALE_EIGHTH : 0; // keeps the decoded image >= the target — the custom scaler handles the fine remainder.
// This avoids decoding millions of pixels that are immediately thrown away.
int jpegDecodeFlags = 0;
int jpegScaleDenom = 1;
if (progressive) {
jpegDecodeFlags = JPEG_SCALE_EIGHTH;
jpegScaleDenom = 8;
} else if (targetWidth > 0 && targetHeight > 0) {
// Use max(scaleX, scaleY) as the constraint: the DCT pre-scale must keep BOTH axes
// >= target so the fine scaler always downscales (never upscales) on either axis.
// This is safe for both crop=true (which uses max scale) and crop=false (uses min scale).
const float scaleX = static_cast<float>(targetWidth) / srcWidth;
const float scaleY = static_cast<float>(targetHeight) / srcHeight;
const float scaleMax = scaleX > scaleY ? scaleX : scaleY;
if (scaleMax <= 0.125f) {
jpegDecodeFlags = JPEG_SCALE_EIGHTH;
jpegScaleDenom = 8;
} else if (scaleMax <= 0.25f) {
jpegDecodeFlags = JPEG_SCALE_QUARTER;
jpegScaleDenom = 4;
} else if (scaleMax <= 0.5f) {
jpegDecodeFlags = JPEG_SCALE_HALF;
jpegScaleDenom = 2;
}
}
const int effectiveSrcW = (srcWidth + jpegScaleDenom - 1) / jpegScaleDenom;
const int effectiveSrcH = (srcHeight + jpegScaleDenom - 1) / jpegScaleDenom;
if (jpegScaleDenom > 1) {
LOG_DBG("JPG", "Using JPEGDEC 1/%d DCT scale: %dx%d -> %dx%d", jpegScaleDenom, srcWidth, srcHeight, effectiveSrcW,
effectiveSrcH);
}
// Calculate output dimensions (pre-scale to fit display exactly) // Calculate output dimensions (pre-scale to fit display exactly)
int outWidth = effectiveSrcW; int outWidth = effectiveSrcW;
@@ -487,7 +519,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
scaleY_fp = (static_cast<uint32_t>(effectiveSrcH) << 16) / outHeight; scaleY_fp = (static_cast<uint32_t>(effectiveSrcH) << 16) / outHeight;
needsScaling = true; needsScaling = true;
LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", effectiveSrcW, effectiveSrcH, outWidth, outHeight, LOG_DBG("JPG", "Fine-scaling %dx%d -> %dx%d (target %dx%d)", effectiveSrcW, effectiveSrcH, outWidth, outHeight,
targetWidth, targetHeight); targetWidth, targetHeight);
} }
@@ -570,7 +602,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE); jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
jpeg->setUserPointer(&ctx); jpeg->setUserPointer(&ctx);
rc = jpeg->decode(0, 0, decodeFlags); rc = jpeg->decode(0, 0, jpegDecodeFlags);
if (rc != 1 || ctx.error) { if (rc != 1 || ctx.error) {
LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError()); LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError());
+94
View File
@@ -1,5 +1,7 @@
#include "Txt.h" #include "Txt.h"
#include <Bitmap.h>
#include <BitmapHelpers.h>
#include <FsHelpers.h> #include <FsHelpers.h>
#include <JpegToBmpConverter.h> #include <JpegToBmpConverter.h>
#include <Logging.h> #include <Logging.h>
@@ -184,6 +186,98 @@ bool Txt::generateCoverBmp() const {
return false; return false;
} }
std::string Txt::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }
std::string Txt::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
std::string Txt::getThumbBmpPath(int width, int height) const {
return cachePath + "/thumb_" + std::to_string(width) + "x" + std::to_string(height) + ".bmp";
}
bool Txt::generateThumbBmp(int height) const {
const std::string destPath = getThumbBmpPath(height);
if (Storage.exists(destPath.c_str())) return true;
const int width = static_cast<int>(height * 0.6f);
if (!generateThumbBmp(width, height)) return false;
const std::string srcPath = getThumbBmpPath(width, height);
Storage.rename(srcPath.c_str(), destPath.c_str());
return Storage.exists(destPath.c_str());
}
bool Txt::generateThumbBmp(int width, int height) const {
const std::string thumbPath = getThumbBmpPath(width, height);
if (Storage.exists(thumbPath.c_str())) return true;
setupCacheDir();
FsFile thumbBmp;
if (!Storage.openFileForWrite("TXT", thumbPath, thumbBmp)) return false;
const uint32_t rowSize = (static_cast<uint32_t>(width) + 31) / 32 * 4;
BmpHeader bmpHeader;
createBmpHeader(&bmpHeader, width, height, BmpRowOrder::TopDown);
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeader), sizeof(BmpHeader));
uint8_t* rowBuffer = static_cast<uint8_t*>(malloc(rowSize));
if (!rowBuffer) {
thumbBmp.close();
Storage.remove(thumbPath.c_str());
return false;
}
// Matches the Lyra "no cover" placeholder: 1px border, white top third, black bottom two thirds.
// Book icon (32x32, 1=white/transparent, 0=black) centered in the white area.
// In 1-bit BMP: 1=white, 0=black.
static const uint8_t kIcon[] = {
0xFF, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFE, 0x1F, 0xE0, 0x00, 0x02, 0x1F, 0xE0, 0x00, 0x03,
0x1F, 0xE0, 0x00, 0x03, 0x1F, 0xF0, 0x00, 0x01, 0x1F, 0xF0, 0x00, 0x01, 0x1F, 0xF0, 0x00, 0x01, 0x9F, 0xF8, 0x00,
0x00, 0x9F, 0xF8, 0x00, 0x00, 0xDF, 0xFC, 0x00, 0x00, 0x6F, 0xFE, 0x00, 0x00, 0x3F, 0xFF, 0x00, 0x00, 0x1F, 0xFF,
0x80, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x0F, 0xFF, 0x00, 0x00,
0x1F, 0xFE, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x6F, 0xF8, 0x00, 0x00, 0xDF, 0xF8, 0x00, 0x00, 0x9F, 0xF0, 0x00,
0x01, 0x9F, 0xF0, 0x00, 0x01, 0x1F, 0xE0, 0x00, 0x01, 0x1F, 0xE0, 0x00, 0x03, 0x1F, 0xE0, 0x00, 0x02, 0x1F, 0xE0,
0x00, 0x02, 0x1F, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0x1F};
static constexpr int kIconSize = 32;
static constexpr int kIconStride = 4; // bytes per icon row (32 bits)
const int splitY = height / 3; // white above, black below
const int iconX = (width - kIconSize) / 2;
const int iconY = (splitY - kIconSize) / 2;
for (int y = 0; y < height; y++) {
const bool blackRegion = (y >= splitY);
memset(rowBuffer, blackRegion ? 0x00 : 0xFF, rowSize);
// 1px border
if (y == 0 || y == height - 1) {
memset(rowBuffer, 0x00, rowSize);
} else {
// Left and right border pixels
rowBuffer[0] &= 0x7F; // clear MSB (x=0)
rowBuffer[(width - 1) / 8] &= ~(0x80u >> ((width - 1) % 8)); // clear x=width-1
// Overlay icon row if within icon bounds (icon is on white area)
const int iconRow = y - iconY;
if (!blackRegion && iconRow >= 0 && iconRow < kIconSize && iconX >= 0 && iconX + kIconSize <= width) {
// Icon format: 0=dark pixel, 1=white/transparent. In BMP: 0=black, 1=white.
// The icon pixels (0=dark) should clear bits in the white BMP region.
for (int ix = 0; ix < kIconSize; ix++) {
const int iconByte = iconRow * kIconStride + ix / 8;
const int iconBit = 7 - (ix % 8);
const bool iconDark = !((kIcon[iconByte] >> iconBit) & 1);
if (iconDark) {
const int bx = iconX + ix;
rowBuffer[bx / 8] &= ~(0x80u >> (bx % 8));
}
}
}
}
thumbBmp.write(rowBuffer, rowSize);
}
free(rowBuffer);
thumbBmp.close();
LOG_DBG("TXT", "Generated surrogate thumb BMP (%dx%d): %s", width, height, thumbPath.c_str());
return true;
}
bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const { bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const {
if (!loaded) { if (!loaded) {
return false; return false;
+7
View File
@@ -28,6 +28,13 @@ class Txt {
[[nodiscard]] bool generateCoverBmp() const; [[nodiscard]] bool generateCoverBmp() const;
[[nodiscard]] std::string findCoverImage() const; [[nodiscard]] std::string findCoverImage() const;
// Thumbnail support - generates a surrogate placeholder cover BMP
[[nodiscard]] std::string getThumbBmpPath() const;
[[nodiscard]] std::string getThumbBmpPath(int height) const;
[[nodiscard]] std::string getThumbBmpPath(int width, int height) const;
[[nodiscard]] bool generateThumbBmp(int height) const;
[[nodiscard]] bool generateThumbBmp(int width, int height) const;
// Read content from file // Read content from file
[[nodiscard]] bool readContent(uint8_t* buffer, size_t offset, size_t length) const; [[nodiscard]] bool readContent(uint8_t* buffer, size_t offset, size_t length) const;
}; };
+81 -24
View File
@@ -9,6 +9,7 @@
#include <JpegToBmpConverter.h> #include <JpegToBmpConverter.h>
#include <Logging.h> #include <Logging.h>
#include <PngToBmpConverter.h> #include <PngToBmpConverter.h>
#include <Txt.h>
#include <Utf8.h> #include <Utf8.h>
#include <Xtc.h> #include <Xtc.h>
@@ -198,8 +199,62 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
const auto thumbSizes = GUI.getCoverThumbSizes(coverHeight); const auto thumbSizes = GUI.getCoverThumbSizes(coverHeight);
// Called at every early-return point where a cover was just written to disk.
// Invalidates the carousel frame cache so tryFastHomeRender re-renders the tile
// with the new BMP rather than compositing the stale placeholder frame.
const auto onCoverGenerated = [this]() {
UITheme::getInstance().getMutableTheme().invalidateFrameCache();
coverRendered = false;
nextRecentCoverIndex++;
recentsLoading = false;
requestUpdate();
};
for (; nextRecentCoverIndex < recentBooks.size(); nextRecentCoverIndex++) { for (; nextRecentCoverIndex < recentBooks.size(); nextRecentCoverIndex++) {
RecentBook& book = recentBooks[nextRecentCoverIndex]; RecentBook& book = recentBooks[nextRecentCoverIndex];
// If coverBmpPath was previously cleared (failed cover extraction) or is missing after
// a cache wipe, attempt to generate it now using the full load path (buildIfMissing=true).
if (book.coverBmpPath.empty() && Storage.exists(book.path.c_str())) {
if (FsHelpers::hasEpubExtension(book.path)) {
Epub epub(book.path, "/.crosspoint");
if (epub.load(true, true)) {
const std::string thumbPath = epub.getThumbBmpPath();
bool success = thumbSizes.empty() ? epub.generateThumbBmp(coverHeight)
: epub.generateThumbBmp(thumbSizes[0].first, thumbSizes[0].second);
if (success) {
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, thumbPath);
book.coverBmpPath = thumbPath;
LOG_DBG("HOME", "Generated missing cover for %s", book.path.c_str());
}
}
} else if (FsHelpers::hasXtcExtension(book.path)) {
Xtc xtc(book.path, "/.crosspoint");
if (xtc.load()) {
const std::string thumbPath = xtc.getThumbBmpPath();
bool success = thumbSizes.empty() ? xtc.generateThumbBmp(coverHeight)
: xtc.generateThumbBmp(thumbSizes[0].first, thumbSizes[0].second);
if (success) {
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, thumbPath);
book.coverBmpPath = thumbPath;
LOG_DBG("HOME", "Generated missing cover for %s", book.path.c_str());
}
}
} else if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) {
Txt txt(book.path, "/.crosspoint");
const std::string thumbPath = txt.getThumbBmpPath();
bool success = thumbSizes.empty() ? txt.generateThumbBmp(coverHeight)
: txt.generateThumbBmp(thumbSizes[0].first, thumbSizes[0].second);
if (success) {
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, thumbPath);
book.coverBmpPath = thumbPath;
LOG_DBG("HOME", "Generated surrogate cover for %s", book.path.c_str());
}
}
onCoverGenerated();
return;
}
if (!book.coverBmpPath.empty()) { if (!book.coverBmpPath.empty()) {
// Sidecar covers (JPG/PNG paths stored directly) must be converted to BMP thumbnails // Sidecar covers (JPG/PNG paths stored directly) must be converted to BMP thumbnails
// and the stored coverBmpPath updated to the cache path with [WIDTH]x[HEIGHT] placeholder. // and the stored coverBmpPath updated to the cache path with [WIDTH]x[HEIGHT] placeholder.
@@ -242,10 +297,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
// Don't permanently clear the path on failure — keep the raw sidecar path // Don't permanently clear the path on failure — keep the raw sidecar path
// so the next home visit can retry (e.g. after more memory becomes available). // so the next home visit can retry (e.g. after more memory becomes available).
} }
coverRendered = false; onCoverGenerated();
nextRecentCoverIndex++;
recentsLoading = false;
requestUpdate();
return; return;
} }
} }
@@ -266,7 +318,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
bool success = true; bool success = true;
if (FsHelpers::hasEpubExtension(book.path)) { if (FsHelpers::hasEpubExtension(book.path)) {
Epub epub(book.path, "/.crosspoint"); Epub epub(book.path, "/.crosspoint");
epub.load(false, true); epub.load(true, true);
for (const auto& sz : thumbSizes) { for (const auto& sz : thumbSizes) {
const std::string path = UITheme::getCoverThumbPath(book.coverBmpPath, sz.first, sz.second); const std::string path = UITheme::getCoverThumbPath(book.coverBmpPath, sz.first, sz.second);
if (!Storage.exists(path.c_str())) success = epub.generateThumbBmp(sz.first, sz.second) && success; if (!Storage.exists(path.c_str())) success = epub.generateThumbBmp(sz.first, sz.second) && success;
@@ -279,15 +331,18 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
if (!Storage.exists(path.c_str())) success = xtc.generateThumbBmp(sz.first, sz.second) && success; if (!Storage.exists(path.c_str())) success = xtc.generateThumbBmp(sz.first, sz.second) && success;
} }
} }
} else if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) {
Txt txt(book.path, "/.crosspoint");
for (const auto& sz : thumbSizes) {
const std::string path = UITheme::getCoverThumbPath(book.coverBmpPath, sz.first, sz.second);
if (!Storage.exists(path.c_str())) success = txt.generateThumbBmp(sz.first, sz.second) && success;
}
} }
if (!success) { if (!success) {
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, ""); RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, "");
book.coverBmpPath = ""; book.coverBmpPath = "";
} }
coverRendered = false; onCoverGenerated();
nextRecentCoverIndex++;
recentsLoading = false;
requestUpdate();
return; return;
} }
} else { } else {
@@ -295,16 +350,13 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
if (!Storage.exists(coverPath.c_str())) { if (!Storage.exists(coverPath.c_str())) {
if (FsHelpers::hasEpubExtension(book.path)) { if (FsHelpers::hasEpubExtension(book.path)) {
Epub epub(book.path, "/.crosspoint"); Epub epub(book.path, "/.crosspoint");
epub.load(false, true); epub.load(true, true);
bool success = epub.generateThumbBmp(coverHeight); bool success = epub.generateThumbBmp(coverHeight);
if (!success) { if (!success) {
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, ""); RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, "");
book.coverBmpPath = ""; book.coverBmpPath = "";
} }
coverRendered = false; onCoverGenerated();
nextRecentCoverIndex++;
recentsLoading = false;
requestUpdate();
return; return;
} else if (FsHelpers::hasXtcExtension(book.path)) { } else if (FsHelpers::hasXtcExtension(book.path)) {
Xtc xtc(book.path, "/.crosspoint"); Xtc xtc(book.path, "/.crosspoint");
@@ -314,12 +366,18 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, ""); RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, "");
book.coverBmpPath = ""; book.coverBmpPath = "";
} }
coverRendered = false; onCoverGenerated();
nextRecentCoverIndex++;
recentsLoading = false;
requestUpdate();
return; return;
} }
} else if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) {
Txt txt(book.path, "/.crosspoint");
bool success = txt.generateThumbBmp(coverHeight);
if (!success) {
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, "");
book.coverBmpPath = "";
}
onCoverGenerated();
return;
} }
} }
} }
@@ -425,6 +483,11 @@ void HomeActivity::loop() {
const bool isCarousel = (GUI.getHomeNavigation() == HomeNavigation::Carousel); const bool isCarousel = (GUI.getHomeNavigation() == HomeNavigation::Carousel);
if (isCarousel) { if (isCarousel) {
if (firstRenderDone && !recentsLoaded && !recentsLoading) {
loadRecentCovers(UITheme::getInstance().getMetrics().homeCoverHeight);
return;
}
const int bookCount = static_cast<int>(recentBooks.size()); const int bookCount = static_cast<int>(recentBooks.size());
const int menuItemCount = static_cast<int>(menuEntries.size()); const int menuItemCount = static_cast<int>(menuEntries.size());
const bool inCarouselRow = (selectorIndex < bookCount); const bool inCarouselRow = (selectorIndex < bookCount);
@@ -521,9 +584,6 @@ void HomeActivity::render(RenderLock&&) {
if (!firstRenderDone) { if (!firstRenderDone) {
firstRenderDone = true; firstRenderDone = true;
requestUpdate(); requestUpdate();
} else if (!recentsLoaded && !recentsLoading) {
recentsLoading = true;
loadRecentCovers(metrics.homeCoverHeight);
} }
return; return;
} }
@@ -568,9 +628,6 @@ void HomeActivity::render(RenderLock&&) {
if (!firstRenderDone) { if (!firstRenderDone) {
firstRenderDone = true; firstRenderDone = true;
requestUpdate(); requestUpdate();
} else if (!recentsLoaded && !recentsLoading) {
recentsLoading = true;
loadRecentCovers(getHomeCoverRenderHeight(computeHomeScreenLayout(metrics, contentRect.height, menuCount)));
} }
} }
+3 -1
View File
@@ -54,7 +54,9 @@ void MdReaderActivity::onEnter() {
auto fileName = filePath.substr(filePath.rfind('/') + 1); auto fileName = filePath.substr(filePath.rfind('/') + 1);
APP_STATE.openEpubPath = filePath; APP_STATE.openEpubPath = filePath;
APP_STATE.saveToFile(); APP_STATE.saveToFile();
RECENT_BOOKS.addBook(filePath, fileName, "", "", ReaderActivity::sidecarCoverPath(filePath)); const std::string txtSidecar = ReaderActivity::sidecarCoverPath(filePath);
const std::string txtCover = txtSidecar.empty() ? txt->getThumbBmpPath() : txtSidecar;
RECENT_BOOKS.addBook(filePath, fileName, "", "", txtCover);
requestUpdate(); requestUpdate();
} }
+3 -1
View File
@@ -118,7 +118,9 @@ void TxtReaderActivity::onEnter() {
auto fileName = filePath.substr(filePath.rfind('/') + 1); auto fileName = filePath.substr(filePath.rfind('/') + 1);
APP_STATE.openEpubPath = filePath; APP_STATE.openEpubPath = filePath;
APP_STATE.saveToFile(); APP_STATE.saveToFile();
RECENT_BOOKS.addBook(filePath, fileName, "", "", ReaderActivity::sidecarCoverPath(filePath)); const std::string txtSidecar = ReaderActivity::sidecarCoverPath(filePath);
const std::string txtCover = txtSidecar.empty() ? txt->getThumbBmpPath() : txtSidecar;
RECENT_BOOKS.addBook(filePath, fileName, "", "", txtCover);
// Trigger first update // Trigger first update
requestUpdate(); requestUpdate();
+1 -1
View File
@@ -516,7 +516,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
if (bitmap.parseHeaders() == BmpReaderError::Ok) { if (bitmap.parseHeaders() == BmpReaderError::Ok) {
LOG_DBG("THEME", "Rendering bmp"); LOG_DBG("THEME", "Rendering bmp");
// Draw the cover image (bookWidth and bookHeight already match image aspect ratio) renderer.fillRect(bookX, bookY, bookWidth, bookHeight, false);
renderer.drawBitmap(bitmap, bookX, bookY, bookWidth, bookHeight); renderer.drawBitmap(bitmap, bookX, bookY, bookWidth, bookHeight);
// Draw border around the card // Draw border around the card
@@ -57,6 +57,10 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
static_cast<float>(tileWidth - 2 * hPaddingInSelection) / static_cast<float>(coverHeight); static_cast<float>(tileWidth - 2 * hPaddingInSelection) / static_cast<float>(coverHeight);
const float cropX = std::max(0.0f, 1.0f - (tileRatio / ratio)); const float cropX = std::max(0.0f, 1.0f - (tileRatio / ratio));
// Clear tile to white before drawing: 1-bit BMPs only draw dark pixels,
// leaving white pixels transparent — any stale dark content shows through.
renderer.fillRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection,
tileWidth - 2 * hPaddingInSelection, coverHeight, false);
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
tileWidth - 2 * hPaddingInSelection, coverHeight, cropX); tileWidth - 2 * hPaddingInSelection, coverHeight, cropX);
} else { } else {
@@ -338,6 +338,7 @@ void LyraCarouselTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect,
const float bmpRatio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight()); const float bmpRatio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
const float tileRatio = static_cast<float>(maxW) / static_cast<float>(maxH); const float tileRatio = static_cast<float>(maxW) / static_cast<float>(maxH);
const float cropX = (bmpRatio > tileRatio) ? (1.0f - tileRatio / bmpRatio) : 0.0f; const float cropX = (bmpRatio > tileRatio) ? (1.0f - tileRatio / bmpRatio) : 0.0f;
renderer.fillRect(x, y, maxW, maxH, false);
renderer.drawBitmap(bitmap, x, y, maxW, maxH, cropX, 0.0f); renderer.drawBitmap(bitmap, x, y, maxW, maxH, cropX, 0.0f);
// Clear only the pixels outside the arc in each corner. // Clear only the pixels outside the arc in each corner.
// The arc centre for the top-left corner is (x+r, y+r). A pixel at // The arc centre for the top-left corner is (x+r, y+r). A pixel at
+1
View File
@@ -568,6 +568,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
Bitmap bitmap(file); Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) { if (bitmap.parseHeaders() == BmpReaderError::Ok) {
coverWidth = bitmap.getWidth(); coverWidth = bitmap.getWidth();
renderer.fillRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth, coverHeight, false);
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth, renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth,
coverHeight); coverHeight);
} else { } else {