Default covers + optimisations
This commit is contained in:
@@ -457,9 +457,41 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
return false;
|
||||
}
|
||||
|
||||
const int effectiveSrcW = progressive ? (srcWidth + 7) / 8 : srcWidth;
|
||||
const int effectiveSrcH = progressive ? (srcHeight + 7) / 8 : srcHeight;
|
||||
const int decodeFlags = progressive ? JPEG_SCALE_EIGHTH : 0;
|
||||
// Progressive JPEGs must stay at 1/8 (DC-only mode, only safe scale with MCU_SKIP patch).
|
||||
// For baseline JPEGs with a target size, pick the largest built-in DCT scale that still
|
||||
// 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)
|
||||
int outWidth = effectiveSrcW;
|
||||
@@ -487,7 +519,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
scaleY_fp = (static_cast<uint32_t>(effectiveSrcH) << 16) / outHeight;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -570,7 +602,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
||||
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
|
||||
jpeg->setUserPointer(&ctx);
|
||||
|
||||
rc = jpeg->decode(0, 0, decodeFlags);
|
||||
rc = jpeg->decode(0, 0, jpegDecodeFlags);
|
||||
|
||||
if (rc != 1 || ctx.error) {
|
||||
LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "Txt.h"
|
||||
|
||||
#include <Bitmap.h>
|
||||
#include <BitmapHelpers.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <JpegToBmpConverter.h>
|
||||
#include <Logging.h>
|
||||
@@ -184,6 +186,98 @@ bool Txt::generateCoverBmp() const {
|
||||
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 {
|
||||
if (!loaded) {
|
||||
return false;
|
||||
|
||||
@@ -28,6 +28,13 @@ class Txt {
|
||||
[[nodiscard]] bool generateCoverBmp() 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
|
||||
[[nodiscard]] bool readContent(uint8_t* buffer, size_t offset, size_t length) const;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user