Epub thumbnail display
This commit is contained in:
parent
5ecc277824
commit
666d3a9179
@ -263,10 +263,13 @@ const std::string& Epub::getTitle() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
||||||
|
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
|
||||||
|
|
||||||
|
bool Epub::generateCoverBmp(bool thumb) const {
|
||||||
|
std::string path = thumb ? getThumbBmpPath() : getCoverBmpPath();
|
||||||
|
|
||||||
bool Epub::generateCoverBmp() const {
|
|
||||||
// Already generated, return true
|
// Already generated, return true
|
||||||
if (SD.exists(getCoverBmpPath().c_str())) {
|
if (SD.exists(path.c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,11 +301,11 @@ bool Epub::generateCoverBmp() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
File coverBmp;
|
File coverBmp;
|
||||||
if (!FsHelpers::openFileForWrite("EBP", getCoverBmpPath(), coverBmp)) {
|
if (!FsHelpers::openFileForWrite("EBP", path, coverBmp)) {
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp);
|
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, thumb ? 1 : 2, thumb ? 90 : 480, thumb ? 120 : 800);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
coverBmp.close();
|
coverBmp.close();
|
||||||
SD.remove(coverJpgTempPath.c_str());
|
SD.remove(coverJpgTempPath.c_str());
|
||||||
|
|||||||
@ -39,8 +39,9 @@ class Epub {
|
|||||||
const std::string& getCachePath() const;
|
const std::string& getCachePath() const;
|
||||||
const std::string& getPath() const;
|
const std::string& getPath() const;
|
||||||
const std::string& getTitle() const;
|
const std::string& getTitle() const;
|
||||||
|
std::string getThumbBmpPath() const;
|
||||||
std::string getCoverBmpPath() const;
|
std::string getCoverBmpPath() const;
|
||||||
bool generateCoverBmp() const;
|
bool generateCoverBmp(bool thumb) const;
|
||||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||||
bool trailingNullByte = false) const;
|
bool trailingNullByte = false) const;
|
||||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||||
|
|||||||
@ -16,7 +16,6 @@ struct JpegReadContext {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// IMAGE PROCESSING OPTIONS - Toggle these to test different configurations
|
// IMAGE PROCESSING OPTIONS - Toggle these to test different configurations
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
constexpr bool USE_8BIT_OUTPUT = false; // true: 8-bit grayscale (no quantization), false: 2-bit (4 levels)
|
|
||||||
// Dithering method selection (only one should be true, or all false for simple quantization):
|
// Dithering method selection (only one should be true, or all false for simple quantization):
|
||||||
constexpr bool USE_ATKINSON = true; // Atkinson dithering (cleaner than F-S, less error diffusion)
|
constexpr bool USE_ATKINSON = true; // Atkinson dithering (cleaner than F-S, less error diffusion)
|
||||||
constexpr bool USE_FLOYD_STEINBERG = false; // Floyd-Steinberg error diffusion (can cause "worm" artifacts)
|
constexpr bool USE_FLOYD_STEINBERG = false; // Floyd-Steinberg error diffusion (can cause "worm" artifacts)
|
||||||
@ -74,16 +73,41 @@ static inline int adjustPixel(int gray) {
|
|||||||
return gray;
|
return gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple quantization without dithering - just divide into 4 levels
|
// Quantize a brightness-adjusted gray value into evenly spaced levels
|
||||||
static inline uint8_t quantizeSimple(int gray) {
|
static inline uint8_t quantizeAdjustedSimple(int gray, int levelCount) {
|
||||||
|
if (levelCount <= 1) return 0;
|
||||||
|
if (gray < 0) gray = 0;
|
||||||
|
if (gray > 255) gray = 255;
|
||||||
|
int level = (gray * levelCount) >> 8; // Divide by 256
|
||||||
|
if (level >= levelCount) level = levelCount - 1;
|
||||||
|
return static_cast<uint8_t>(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quantize adjusted gray and also return the reconstructed 0-255 value
|
||||||
|
static inline uint8_t quantizeAdjustedWithValue(int gray, int levelCount, int& quantizedValue) {
|
||||||
|
if (levelCount <= 1) {
|
||||||
|
quantizedValue = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (gray < 0) gray = 0;
|
||||||
|
if (gray > 255) gray = 255;
|
||||||
|
int level = (gray * levelCount) >> 8;
|
||||||
|
if (level >= levelCount) level = levelCount - 1;
|
||||||
|
const int denom = levelCount - 1;
|
||||||
|
quantizedValue = denom > 0 ? (level * 255) / denom : 0;
|
||||||
|
return static_cast<uint8_t>(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple quantization without dithering - divide into 2^bits levels
|
||||||
|
static inline uint8_t quantizeSimple(int gray, int levelCount) {
|
||||||
gray = adjustPixel(gray);
|
gray = adjustPixel(gray);
|
||||||
// Simple 2-bit quantization: 0-63=0, 64-127=1, 128-191=2, 192-255=3
|
return quantizeAdjustedSimple(gray, levelCount);
|
||||||
return static_cast<uint8_t>(gray >> 6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash-based noise dithering - survives downsampling without moiré artifacts
|
// Hash-based noise dithering - survives downsampling without moiré artifacts
|
||||||
// Uses integer hash to generate pseudo-random threshold per pixel
|
// Uses integer hash to generate pseudo-random threshold per pixel
|
||||||
static inline uint8_t quantizeNoise(int gray, int x, int y) {
|
static inline uint8_t quantizeNoise(int gray, int x, int y, int levelCount) {
|
||||||
|
if (levelCount <= 1) return 0;
|
||||||
gray = adjustPixel(gray);
|
gray = adjustPixel(gray);
|
||||||
|
|
||||||
// Generate noise threshold using integer hash (no regular pattern to alias)
|
// Generate noise threshold using integer hash (no regular pattern to alias)
|
||||||
@ -91,24 +115,23 @@ static inline uint8_t quantizeNoise(int gray, int x, int y) {
|
|||||||
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
||||||
const int threshold = static_cast<int>(hash >> 24); // 0-255
|
const int threshold = static_cast<int>(hash >> 24); // 0-255
|
||||||
|
|
||||||
// Map gray (0-255) to 4 levels with dithering
|
// Map gray (0-255) to N levels with dithering
|
||||||
const int scaled = gray * 3;
|
const int scaled = gray * levelCount;
|
||||||
|
int level = scaled >> 8;
|
||||||
if (scaled < 255) {
|
if (level >= levelCount) level = levelCount - 1;
|
||||||
return (scaled + threshold >= 255) ? 1 : 0;
|
const int remainder = scaled & 0xFF;
|
||||||
} else if (scaled < 510) {
|
if (level < levelCount - 1 && remainder + threshold >= 256) {
|
||||||
return ((scaled - 255) + threshold >= 255) ? 2 : 1;
|
level++;
|
||||||
} else {
|
|
||||||
return ((scaled - 510) + threshold >= 255) ? 3 : 2;
|
|
||||||
}
|
}
|
||||||
|
return static_cast<uint8_t>(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main quantization function - selects between methods based on config
|
// Main quantization function - selects between methods based on config
|
||||||
static inline uint8_t quantize(int gray, int x, int y) {
|
static inline uint8_t quantize(int gray, int x, int y, int levelCount) {
|
||||||
if (USE_NOISE_DITHERING) {
|
if (USE_NOISE_DITHERING) {
|
||||||
return quantizeNoise(gray, x, y);
|
return quantizeNoise(gray, x, y, levelCount);
|
||||||
} else {
|
} else {
|
||||||
return quantizeSimple(gray);
|
return quantizeSimple(gray, levelCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +143,7 @@ static inline uint8_t quantize(int gray, int x, int y) {
|
|||||||
// Less error buildup = fewer artifacts than Floyd-Steinberg
|
// Less error buildup = fewer artifacts than Floyd-Steinberg
|
||||||
class AtkinsonDitherer {
|
class AtkinsonDitherer {
|
||||||
public:
|
public:
|
||||||
AtkinsonDitherer(int width) : width(width) {
|
AtkinsonDitherer(int width, int levelCount) : width(width), levelCount(levelCount) {
|
||||||
errorRow0 = new int16_t[width + 4](); // Current row
|
errorRow0 = new int16_t[width + 4](); // Current row
|
||||||
errorRow1 = new int16_t[width + 4](); // Next row
|
errorRow1 = new int16_t[width + 4](); // Next row
|
||||||
errorRow2 = new int16_t[width + 4](); // Row after next
|
errorRow2 = new int16_t[width + 4](); // Row after next
|
||||||
@ -142,21 +165,8 @@ class AtkinsonDitherer {
|
|||||||
if (adjusted > 255) adjusted = 255;
|
if (adjusted > 255) adjusted = 255;
|
||||||
|
|
||||||
// Quantize to 4 levels
|
// Quantize to 4 levels
|
||||||
uint8_t quantized;
|
int quantizedValue = 0;
|
||||||
int quantizedValue;
|
uint8_t quantized = quantizeAdjustedWithValue(adjusted, levelCount, quantizedValue);
|
||||||
if (adjusted < 43) {
|
|
||||||
quantized = 0;
|
|
||||||
quantizedValue = 0;
|
|
||||||
} else if (adjusted < 128) {
|
|
||||||
quantized = 1;
|
|
||||||
quantizedValue = 85;
|
|
||||||
} else if (adjusted < 213) {
|
|
||||||
quantized = 2;
|
|
||||||
quantizedValue = 170;
|
|
||||||
} else {
|
|
||||||
quantized = 3;
|
|
||||||
quantizedValue = 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate error (only distribute 6/8 = 75%)
|
// Calculate error (only distribute 6/8 = 75%)
|
||||||
int error = (adjusted - quantizedValue) >> 3; // error/8
|
int error = (adjusted - quantizedValue) >> 3; // error/8
|
||||||
@ -188,6 +198,7 @@ class AtkinsonDitherer {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
int width;
|
int width;
|
||||||
|
int levelCount;
|
||||||
int16_t* errorRow0;
|
int16_t* errorRow0;
|
||||||
int16_t* errorRow1;
|
int16_t* errorRow1;
|
||||||
int16_t* errorRow2;
|
int16_t* errorRow2;
|
||||||
@ -203,7 +214,7 @@ class AtkinsonDitherer {
|
|||||||
// 7/16 X
|
// 7/16 X
|
||||||
class FloydSteinbergDitherer {
|
class FloydSteinbergDitherer {
|
||||||
public:
|
public:
|
||||||
FloydSteinbergDitherer(int width) : width(width), rowCount(0) {
|
FloydSteinbergDitherer(int width, int levelCount) : width(width), levelCount(levelCount), rowCount(0) {
|
||||||
errorCurRow = new int16_t[width + 2](); // +2 for boundary handling
|
errorCurRow = new int16_t[width + 2](); // +2 for boundary handling
|
||||||
errorNextRow = new int16_t[width + 2]();
|
errorNextRow = new int16_t[width + 2]();
|
||||||
}
|
}
|
||||||
@ -216,6 +227,7 @@ class FloydSteinbergDitherer {
|
|||||||
// Process a single pixel and return quantized 2-bit value
|
// Process a single pixel and return quantized 2-bit value
|
||||||
// x is the logical x position (0 to width-1), direction handled internally
|
// x is the logical x position (0 to width-1), direction handled internally
|
||||||
uint8_t processPixel(int gray, int x, bool reverseDirection) {
|
uint8_t processPixel(int gray, int x, bool reverseDirection) {
|
||||||
|
gray = adjustPixel(gray);
|
||||||
// Add accumulated error to this pixel
|
// Add accumulated error to this pixel
|
||||||
int adjusted = gray + errorCurRow[x + 1];
|
int adjusted = gray + errorCurRow[x + 1];
|
||||||
|
|
||||||
@ -223,22 +235,9 @@ class FloydSteinbergDitherer {
|
|||||||
if (adjusted < 0) adjusted = 0;
|
if (adjusted < 0) adjusted = 0;
|
||||||
if (adjusted > 255) adjusted = 255;
|
if (adjusted > 255) adjusted = 255;
|
||||||
|
|
||||||
// Quantize to 4 levels (0, 85, 170, 255)
|
// Quantize to the requested level count
|
||||||
uint8_t quantized;
|
int quantizedValue = 0;
|
||||||
int quantizedValue;
|
uint8_t quantized = quantizeAdjustedWithValue(adjusted, levelCount, quantizedValue);
|
||||||
if (adjusted < 43) {
|
|
||||||
quantized = 0;
|
|
||||||
quantizedValue = 0;
|
|
||||||
} else if (adjusted < 128) {
|
|
||||||
quantized = 1;
|
|
||||||
quantizedValue = 85;
|
|
||||||
} else if (adjusted < 213) {
|
|
||||||
quantized = 2;
|
|
||||||
quantizedValue = 170;
|
|
||||||
} else {
|
|
||||||
quantized = 3;
|
|
||||||
quantizedValue = 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate error
|
// Calculate error
|
||||||
int error = adjusted - quantizedValue;
|
int error = adjusted - quantizedValue;
|
||||||
@ -292,6 +291,7 @@ class FloydSteinbergDitherer {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
int width;
|
int width;
|
||||||
|
int levelCount;
|
||||||
int rowCount;
|
int rowCount;
|
||||||
int16_t* errorCurRow;
|
int16_t* errorCurRow;
|
||||||
int16_t* errorNextRow;
|
int16_t* errorNextRow;
|
||||||
@ -316,12 +316,38 @@ inline void write32Signed(Print& out, const int32_t value) {
|
|||||||
out.write((value >> 24) & 0xFF);
|
out.write((value >> 24) & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void writeIndexedPixel(uint8_t* rowBuffer, int x, int bitsPerPixel, uint8_t value) {
|
||||||
|
const int bitPos = x * bitsPerPixel;
|
||||||
|
const int byteIndex = bitPos >> 3;
|
||||||
|
const int bitOffset = 8 - bitsPerPixel - (bitPos & 7);
|
||||||
|
rowBuffer[byteIndex] |= static_cast<uint8_t>(value << bitOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getBytesPerRow(int width, int bitsPerPixel) {
|
||||||
|
if (bitsPerPixel == 8) {
|
||||||
|
return (width + 3) / 4 * 4; // 8 bits per pixel, padded
|
||||||
|
} else if (bitsPerPixel == 2) {
|
||||||
|
return (width * 2 + 31) / 32 * 4; // 2 bits per pixel, round up
|
||||||
|
}
|
||||||
|
return (width + 31) / 32 * 4; // 1 bit per pixel, round up
|
||||||
|
}
|
||||||
|
|
||||||
|
int getColorsUsed(int bitsPerPixel) {
|
||||||
|
if (bitsPerPixel == 8) {
|
||||||
|
return 256;
|
||||||
|
} else if (bitsPerPixel == 2) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function: Write BMP header with 8-bit grayscale (256 levels)
|
// Helper function: Write BMP header with 8-bit grayscale (256 levels)
|
||||||
void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) {
|
void writeBmpHeader(Print& bmpOut, const int width, const int height, int bitsPerPixel) {
|
||||||
// Calculate row padding (each row must be multiple of 4 bytes)
|
// Calculate row padding (each row must be multiple of 4 bytes)
|
||||||
const int bytesPerRow = (width + 3) / 4 * 4; // 8 bits per pixel, padded
|
const int bytesPerRow = getBytesPerRow(width, bitsPerPixel);
|
||||||
|
const int colorsUsed = getColorsUsed(bitsPerPixel);
|
||||||
|
const int paletteSize = colorsUsed * 4; // Size of color palette
|
||||||
const int imageSize = bytesPerRow * height;
|
const int imageSize = bytesPerRow * height;
|
||||||
const uint32_t paletteSize = 256 * 4; // 256 colors * 4 bytes (BGRA)
|
|
||||||
const uint32_t fileSize = 14 + 40 + paletteSize + imageSize;
|
const uint32_t fileSize = 14 + 40 + paletteSize + imageSize;
|
||||||
|
|
||||||
// BMP File Header (14 bytes)
|
// BMP File Header (14 bytes)
|
||||||
@ -336,60 +362,45 @@ void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) {
|
|||||||
write32Signed(bmpOut, width);
|
write32Signed(bmpOut, width);
|
||||||
write32Signed(bmpOut, -height); // Negative height = top-down bitmap
|
write32Signed(bmpOut, -height); // Negative height = top-down bitmap
|
||||||
write16(bmpOut, 1); // Color planes
|
write16(bmpOut, 1); // Color planes
|
||||||
write16(bmpOut, 8); // Bits per pixel (8 bits)
|
write16(bmpOut, bitsPerPixel); // Bits per pixel (8 bits)
|
||||||
write32(bmpOut, 0); // BI_RGB (no compression)
|
write32(bmpOut, 0); // BI_RGB (no compression)
|
||||||
write32(bmpOut, imageSize);
|
write32(bmpOut, imageSize);
|
||||||
write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI)
|
write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI)
|
||||||
write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI)
|
write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI)
|
||||||
write32(bmpOut, 256); // colorsUsed
|
write32(bmpOut, colorsUsed); // colorsUsed
|
||||||
write32(bmpOut, 256); // colorsImportant
|
write32(bmpOut, colorsUsed); // colorsImportant
|
||||||
|
|
||||||
// Color Palette (256 grayscale entries x 4 bytes = 1024 bytes)
|
if (bitsPerPixel == 8) {
|
||||||
for (int i = 0; i < 256; i++) {
|
// Color Palette (256 grayscale entries x 4 bytes = 1024 bytes)
|
||||||
bmpOut.write(static_cast<uint8_t>(i)); // Blue
|
for (int i = 0; i < 256; i++) {
|
||||||
bmpOut.write(static_cast<uint8_t>(i)); // Green
|
bmpOut.write(static_cast<uint8_t>(i)); // Blue
|
||||||
bmpOut.write(static_cast<uint8_t>(i)); // Red
|
bmpOut.write(static_cast<uint8_t>(i)); // Green
|
||||||
bmpOut.write(static_cast<uint8_t>(0)); // Reserved
|
bmpOut.write(static_cast<uint8_t>(i)); // Red
|
||||||
}
|
bmpOut.write(static_cast<uint8_t>(0)); // Reserved
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
// Helper function: Write BMP header with 2-bit color depth
|
} else if (bitsPerPixel == 2) {
|
||||||
void JpegToBmpConverter::writeBmpHeader(Print& bmpOut, const int width, const int height) {
|
// Color Palette (4 colors x 4 bytes = 16 bytes)
|
||||||
// Calculate row padding (each row must be multiple of 4 bytes)
|
// Format: Blue, Green, Red, Reserved (BGRA)
|
||||||
const int bytesPerRow = (width * 2 + 31) / 32 * 4; // 2 bits per pixel, round up
|
uint8_t palette[16] = {
|
||||||
const int imageSize = bytesPerRow * height;
|
0x00, 0x00, 0x00, 0x00, // Color 0: Black
|
||||||
const uint32_t fileSize = 70 + imageSize; // 14 (file header) + 40 (DIB header) + 16 (palette) + image
|
0x55, 0x55, 0x55, 0x00, // Color 1: Dark gray (85)
|
||||||
|
0xAA, 0xAA, 0xAA, 0x00, // Color 2: Light gray (170)
|
||||||
// BMP File Header (14 bytes)
|
0xFF, 0xFF, 0xFF, 0x00 // Color 3: White
|
||||||
bmpOut.write('B');
|
};
|
||||||
bmpOut.write('M');
|
for (const uint8_t i : palette) {
|
||||||
write32(bmpOut, fileSize); // File size
|
bmpOut.write(i);
|
||||||
write32(bmpOut, 0); // Reserved
|
}
|
||||||
write32(bmpOut, 70); // Offset to pixel data
|
} else {
|
||||||
|
// Color Palette (2 colors x 4 bytes = 8 bytes)
|
||||||
// DIB Header (BITMAPINFOHEADER - 40 bytes)
|
// Format: Blue, Green, Red, Reserved (BGRA)
|
||||||
write32(bmpOut, 40);
|
uint8_t palette[8] = {
|
||||||
write32Signed(bmpOut, width);
|
0x00, 0x00, 0x00, 0x00, // Color 0: Black
|
||||||
write32Signed(bmpOut, -height); // Negative height = top-down bitmap
|
0xFF, 0xFF, 0xFF, 0x00 // Color 1: White
|
||||||
write16(bmpOut, 1); // Color planes
|
};
|
||||||
write16(bmpOut, 2); // Bits per pixel (2 bits)
|
for (const uint8_t i : palette) {
|
||||||
write32(bmpOut, 0); // BI_RGB (no compression)
|
bmpOut.write(i);
|
||||||
write32(bmpOut, imageSize);
|
}
|
||||||
write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI)
|
|
||||||
write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI)
|
|
||||||
write32(bmpOut, 4); // colorsUsed
|
|
||||||
write32(bmpOut, 4); // colorsImportant
|
|
||||||
|
|
||||||
// Color Palette (4 colors x 4 bytes = 16 bytes)
|
|
||||||
// Format: Blue, Green, Red, Reserved (BGRA)
|
|
||||||
uint8_t palette[16] = {
|
|
||||||
0x00, 0x00, 0x00, 0x00, // Color 0: Black
|
|
||||||
0x55, 0x55, 0x55, 0x00, // Color 1: Dark gray (85)
|
|
||||||
0xAA, 0xAA, 0xAA, 0x00, // Color 2: Light gray (170)
|
|
||||||
0xFF, 0xFF, 0xFF, 0x00 // Color 3: White
|
|
||||||
};
|
|
||||||
for (const uint8_t i : palette) {
|
|
||||||
bmpOut.write(i);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,10 +436,19 @@ unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const un
|
|||||||
return 0; // Success
|
return 0; // Success
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core function: Convert JPEG file to 2-bit BMP
|
|
||||||
bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
||||||
|
return jpegFileToBmpStream(jpegFile, bmpOut, 2, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core function: Convert JPEG file to BMP
|
||||||
|
bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut, int bitsPerPixel, int targetWidth, int targetHeight) {
|
||||||
Serial.printf("[%lu] [JPG] Converting JPEG to BMP\n", millis());
|
Serial.printf("[%lu] [JPG] Converting JPEG to BMP\n", millis());
|
||||||
|
|
||||||
|
if (bitsPerPixel != 1 && bitsPerPixel != 2 && bitsPerPixel != 8) {
|
||||||
|
Serial.printf("[%lu] [JPG] Unsupported bitsPerPixel: %d\n", millis(), bitsPerPixel);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Setup context for picojpeg callback
|
// Setup context for picojpeg callback
|
||||||
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
|
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
|
||||||
|
|
||||||
@ -462,10 +482,10 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
|||||||
uint32_t scaleY_fp = 65536;
|
uint32_t scaleY_fp = 65536;
|
||||||
bool needsScaling = false;
|
bool needsScaling = false;
|
||||||
|
|
||||||
if (USE_PRESCALE && (imageInfo.m_width > TARGET_MAX_WIDTH || imageInfo.m_height > TARGET_MAX_HEIGHT)) {
|
if (USE_PRESCALE && (imageInfo.m_width > targetWidth || imageInfo.m_height > targetHeight)) {
|
||||||
// Calculate scale to fit within target dimensions while maintaining aspect ratio
|
// Calculate scale to fit within target dimensions while maintaining aspect ratio
|
||||||
const float scaleToFitWidth = static_cast<float>(TARGET_MAX_WIDTH) / imageInfo.m_width;
|
const float scaleToFitWidth = static_cast<float>(targetWidth) / imageInfo.m_width;
|
||||||
const float scaleToFitHeight = static_cast<float>(TARGET_MAX_HEIGHT) / imageInfo.m_height;
|
const float scaleToFitHeight = static_cast<float>(targetHeight) / imageInfo.m_height;
|
||||||
const float scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
const float scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||||
|
|
||||||
outWidth = static_cast<int>(imageInfo.m_width * scale);
|
outWidth = static_cast<int>(imageInfo.m_width * scale);
|
||||||
@ -482,18 +502,14 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
|||||||
needsScaling = true;
|
needsScaling = true;
|
||||||
|
|
||||||
Serial.printf("[%lu] [JPG] Pre-scaling %dx%d -> %dx%d (fit to %dx%d)\n", millis(), imageInfo.m_width,
|
Serial.printf("[%lu] [JPG] Pre-scaling %dx%d -> %dx%d (fit to %dx%d)\n", millis(), imageInfo.m_width,
|
||||||
imageInfo.m_height, outWidth, outHeight, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT);
|
imageInfo.m_height, outWidth, outHeight, targetWidth, targetHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write BMP header with output dimensions
|
// Write BMP header with output dimensions
|
||||||
int bytesPerRow;
|
writeBmpHeader(bmpOut, outWidth, outHeight, bitsPerPixel);
|
||||||
if (USE_8BIT_OUTPUT) {
|
const int bytesPerRow = getBytesPerRow(outWidth, bitsPerPixel);
|
||||||
writeBmpHeader8bit(bmpOut, outWidth, outHeight);
|
const int levelCount = 1 << bitsPerPixel;
|
||||||
bytesPerRow = (outWidth + 3) / 4 * 4;
|
const bool indexedOutput = bitsPerPixel != 8;
|
||||||
} else {
|
|
||||||
writeBmpHeader(bmpOut, outWidth, outHeight);
|
|
||||||
bytesPerRow = (outWidth * 2 + 31) / 32 * 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate row buffer
|
// Allocate row buffer
|
||||||
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
||||||
@ -522,15 +538,15 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create ditherer if enabled (only for 2-bit output)
|
// Create ditherer if enabled (only for indexed output)
|
||||||
// Use OUTPUT dimensions for dithering (after prescaling)
|
// Use OUTPUT dimensions for dithering (after prescaling)
|
||||||
AtkinsonDitherer* atkinsonDitherer = nullptr;
|
AtkinsonDitherer* atkinsonDitherer = nullptr;
|
||||||
FloydSteinbergDitherer* fsDitherer = nullptr;
|
FloydSteinbergDitherer* fsDitherer = nullptr;
|
||||||
if (!USE_8BIT_OUTPUT) {
|
if (indexedOutput) {
|
||||||
if (USE_ATKINSON) {
|
if (USE_ATKINSON) {
|
||||||
atkinsonDitherer = new AtkinsonDitherer(outWidth);
|
atkinsonDitherer = new AtkinsonDitherer(outWidth, levelCount);
|
||||||
} else if (USE_FLOYD_STEINBERG) {
|
} else if (USE_FLOYD_STEINBERG) {
|
||||||
fsDitherer = new FloydSteinbergDitherer(outWidth);
|
fsDitherer = new FloydSteinbergDitherer(outWidth, levelCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,7 +628,7 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
|||||||
// No scaling - direct output (1:1 mapping)
|
// No scaling - direct output (1:1 mapping)
|
||||||
memset(rowBuffer, 0, bytesPerRow);
|
memset(rowBuffer, 0, bytesPerRow);
|
||||||
|
|
||||||
if (USE_8BIT_OUTPUT) {
|
if (!indexedOutput) {
|
||||||
for (int x = 0; x < outWidth; x++) {
|
for (int x = 0; x < outWidth; x++) {
|
||||||
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
|
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
|
||||||
rowBuffer[x] = adjustPixel(gray);
|
rowBuffer[x] = adjustPixel(gray);
|
||||||
@ -620,17 +636,15 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
|||||||
} else {
|
} else {
|
||||||
for (int x = 0; x < outWidth; x++) {
|
for (int x = 0; x < outWidth; x++) {
|
||||||
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
|
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
|
||||||
uint8_t twoBit;
|
uint8_t indexedValue;
|
||||||
if (atkinsonDitherer) {
|
if (atkinsonDitherer) {
|
||||||
twoBit = atkinsonDitherer->processPixel(gray, x);
|
indexedValue = atkinsonDitherer->processPixel(gray, x);
|
||||||
} else if (fsDitherer) {
|
} else if (fsDitherer) {
|
||||||
twoBit = fsDitherer->processPixel(gray, x, fsDitherer->isReverseRow());
|
indexedValue = fsDitherer->processPixel(gray, x, fsDitherer->isReverseRow());
|
||||||
} else {
|
} else {
|
||||||
twoBit = quantize(gray, x, y);
|
indexedValue = quantize(gray, x, y, levelCount);
|
||||||
}
|
}
|
||||||
const int byteIndex = (x * 2) / 8;
|
writeIndexedPixel(rowBuffer, x, bitsPerPixel, indexedValue);
|
||||||
const int bitOffset = 6 - ((x * 2) % 8);
|
|
||||||
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
|
||||||
}
|
}
|
||||||
if (atkinsonDitherer)
|
if (atkinsonDitherer)
|
||||||
atkinsonDitherer->nextRow();
|
atkinsonDitherer->nextRow();
|
||||||
@ -675,7 +689,7 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
|||||||
if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) {
|
if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) {
|
||||||
memset(rowBuffer, 0, bytesPerRow);
|
memset(rowBuffer, 0, bytesPerRow);
|
||||||
|
|
||||||
if (USE_8BIT_OUTPUT) {
|
if (!indexedOutput) {
|
||||||
for (int x = 0; x < outWidth; x++) {
|
for (int x = 0; x < outWidth; x++) {
|
||||||
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
||||||
rowBuffer[x] = adjustPixel(gray);
|
rowBuffer[x] = adjustPixel(gray);
|
||||||
@ -683,17 +697,15 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
|||||||
} else {
|
} else {
|
||||||
for (int x = 0; x < outWidth; x++) {
|
for (int x = 0; x < outWidth; x++) {
|
||||||
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
||||||
uint8_t twoBit;
|
uint8_t indexedValue;
|
||||||
if (atkinsonDitherer) {
|
if (atkinsonDitherer) {
|
||||||
twoBit = atkinsonDitherer->processPixel(gray, x);
|
indexedValue = atkinsonDitherer->processPixel(gray, x);
|
||||||
} else if (fsDitherer) {
|
} else if (fsDitherer) {
|
||||||
twoBit = fsDitherer->processPixel(gray, x, fsDitherer->isReverseRow());
|
indexedValue = fsDitherer->processPixel(gray, x, fsDitherer->isReverseRow());
|
||||||
} else {
|
} else {
|
||||||
twoBit = quantize(gray, x, currentOutY);
|
indexedValue = quantize(gray, x, currentOutY, levelCount);
|
||||||
}
|
}
|
||||||
const int byteIndex = (x * 2) / 8;
|
writeIndexedPixel(rowBuffer, x, bitsPerPixel, indexedValue);
|
||||||
const int bitOffset = 6 - ((x * 2) % 8);
|
|
||||||
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
|
||||||
}
|
}
|
||||||
if (atkinsonDitherer)
|
if (atkinsonDitherer)
|
||||||
atkinsonDitherer->nextRow();
|
atkinsonDitherer->nextRow();
|
||||||
|
|||||||
@ -5,11 +5,11 @@
|
|||||||
class ZipFile;
|
class ZipFile;
|
||||||
|
|
||||||
class JpegToBmpConverter {
|
class JpegToBmpConverter {
|
||||||
static void writeBmpHeader(Print& bmpOut, int width, int height);
|
|
||||||
// [COMMENTED OUT] static uint8_t grayscaleTo2Bit(uint8_t grayscale, int x, int y);
|
// [COMMENTED OUT] static uint8_t grayscaleTo2Bit(uint8_t grayscale, int x, int y);
|
||||||
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||||
unsigned char* pBytes_actually_read, void* pCallback_data);
|
unsigned char* pBytes_actually_read, void* pCallback_data);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static bool jpegFileToBmpStream(File& jpegFile, Print& bmpOut);
|
static bool jpegFileToBmpStream(File& jpegFile, Print& bmpOut);
|
||||||
|
static bool jpegFileToBmpStream(File& jpegFile, Print& bmpOut, int bitsPerPixel, int targetWidth, int targetHeight);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -182,7 +182,7 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
return renderDefaultSleepScreen();
|
return renderDefaultSleepScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lastEpub.generateCoverBmp()) {
|
if (!lastEpub.generateCoverBmp(false)) {
|
||||||
Serial.println("[SLP] Failed to generate cover bmp");
|
Serial.println("[SLP] Failed to generate cover bmp");
|
||||||
return renderDefaultSleepScreen();
|
return renderDefaultSleepScreen();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
|
#include <Epub.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "../../images/FolderIcon.h"
|
#include "../../images/FolderIcon.h"
|
||||||
@ -33,17 +34,39 @@ void GridBrowserActivity::sortFileList(std::vector<FileInfo>& strs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void GridBrowserActivity::taskTrampoline(void* param) {
|
void GridBrowserActivity::displayTaskTrampoline(void* param) {
|
||||||
auto* self = static_cast<GridBrowserActivity*>(param);
|
auto* self = static_cast<GridBrowserActivity*>(param);
|
||||||
self->displayTaskLoop();
|
self->displayTaskLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// void GridBrowserActivity::loadThumbsTaskTrampoline(void* param) {
|
||||||
|
// auto* self = static_cast<GridBrowserActivity*>(param);
|
||||||
|
// self->displayTaskLoop();
|
||||||
|
// }
|
||||||
|
|
||||||
|
std::string GridBrowserActivity::loadEpubThumb(std::string path) {
|
||||||
|
File file;
|
||||||
|
Epub epubFile(path, "/.crosspoint");
|
||||||
|
if (!epubFile.load()) {
|
||||||
|
Serial.printf("[%lu] Failed to load epub: %s\n", millis(), path.c_str());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (!epubFile.generateCoverBmp(true)) {
|
||||||
|
Serial.printf("[%lu] Failed to generate epub thumb\n", millis());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
std::string thumbPath = epubFile.getThumbBmpPath();
|
||||||
|
Serial.printf("[%lu] epub has thumb at %s\n", millis(), thumbPath.c_str());
|
||||||
|
return thumbPath;
|
||||||
|
}
|
||||||
|
|
||||||
void GridBrowserActivity::loadFiles() {
|
void GridBrowserActivity::loadFiles() {
|
||||||
files.clear();
|
files.clear();
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
previousSelectorIndex = -1;
|
previousSelectorIndex = -1;
|
||||||
page = 0;
|
page = 0;
|
||||||
auto root = SD.open(basepath.c_str());
|
auto root = SD.open(basepath.c_str());
|
||||||
|
int count = 0;
|
||||||
for (File file = root.openNextFile(); file; file = root.openNextFile()) {
|
for (File file = root.openNextFile(); file; file = root.openNextFile()) {
|
||||||
const std::string filename = std::string(file.name());
|
const std::string filename = std::string(file.name());
|
||||||
if (filename.empty() || filename[0] == '.') {
|
if (filename.empty() || filename[0] == '.') {
|
||||||
@ -52,7 +75,7 @@ void GridBrowserActivity::loadFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
files.emplace_back(FileInfo{ filename, filename, F_DIRECTORY });
|
files.emplace_back(FileInfo{ filename, filename, F_DIRECTORY, "" });
|
||||||
} else {
|
} else {
|
||||||
FileType type = F_FILE;
|
FileType type = F_FILE;
|
||||||
size_t dot = filename.find_first_of('.');
|
size_t dot = filename.find_first_of('.');
|
||||||
@ -64,15 +87,22 @@ void GridBrowserActivity::loadFiles() {
|
|||||||
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c){ return std::tolower(c); });
|
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c){ return std::tolower(c); });
|
||||||
if (ext == ".epub") {
|
if (ext == ".epub") {
|
||||||
type = F_EPUB;
|
type = F_EPUB;
|
||||||
} else if (ext == ".thumb.bmp") {
|
// xTaskCreate(&GridBrowserActivity::taskTrampoline, "GridFileBrowserTask",
|
||||||
|
// 2048, // Stack size
|
||||||
|
// this, // Parameters
|
||||||
|
// 1, // Priority
|
||||||
|
// &displayTaskHandle // Task handle
|
||||||
|
// );
|
||||||
|
} else if (ext == ".bmp") {
|
||||||
type = F_BMP;
|
type = F_BMP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type != F_FILE) {
|
if (type != F_FILE) {
|
||||||
files.emplace_back(FileInfo{ filename, basename, type });
|
files.emplace_back(FileInfo{ filename, basename, type, "" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file.close();
|
file.close();
|
||||||
|
count ++;
|
||||||
}
|
}
|
||||||
root.close();
|
root.close();
|
||||||
Serial.printf("Files loaded\n");
|
Serial.printf("Files loaded\n");
|
||||||
@ -90,7 +120,7 @@ void GridBrowserActivity::onEnter() {
|
|||||||
// Trigger first render
|
// Trigger first render
|
||||||
renderRequired = true;
|
renderRequired = true;
|
||||||
|
|
||||||
xTaskCreate(&GridBrowserActivity::taskTrampoline, "GridFileBrowserTask",
|
xTaskCreate(&GridBrowserActivity::displayTaskTrampoline, "GridFileBrowserTask",
|
||||||
8192, // Stack size
|
8192, // Stack size
|
||||||
this, // Parameters
|
this, // Parameters
|
||||||
1, // Priority
|
1, // Priority
|
||||||
@ -199,69 +229,36 @@ void GridBrowserActivity::render(bool clear) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!files.empty()) {
|
if (!files.empty()) {
|
||||||
bool hasGeyscaleBitmaps = false;
|
for (size_t i = 0; i < min(PAGE_ITEMS, files.size() - page * PAGE_ITEMS); i++) {
|
||||||
for (int pass = 0; pass < 3; pass++) {
|
const auto file = files[i + page * PAGE_ITEMS];
|
||||||
if (pass > 0) {
|
|
||||||
renderer.clearScreen(0x00);
|
const int16_t tileX = gridLeftOffset + i % 3 * TILE_W;
|
||||||
if (pass == 1) {
|
const int16_t tileY = gridTopOffset + i / 3 * TILE_H;
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
|
||||||
} else if (pass == 2) {
|
if (file.type == F_DIRECTORY) {
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
constexpr int iconOffsetX = (TILE_W - FOLDERICON_WIDTH) / 2;
|
||||||
}
|
constexpr int iconOffsetY = (TILE_H - TILE_TEXT_H - FOLDERICON_HEIGHT) / 2;
|
||||||
|
renderer.drawIcon(FolderIcon, tileX + iconOffsetX, tileY + iconOffsetY, FOLDERICON_WIDTH, FOLDERICON_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t i = 0; i < min(PAGE_ITEMS, files.size() - page * PAGE_ITEMS); i++) {
|
if (!file.thumbPath.empty()) {
|
||||||
const auto file = files[i + page * PAGE_ITEMS];
|
Serial.printf("Rendering file thumb: %s\n", file.thumbPath.c_str());
|
||||||
|
File bmpFile = SD.open(file.thumbPath.c_str());
|
||||||
const int16_t tileX = gridLeftOffset + i % 3 * TILE_W;
|
if (bmpFile) {
|
||||||
const int16_t tileY = gridTopOffset + i / 3 * TILE_H;
|
Bitmap bitmap(bmpFile);
|
||||||
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
if (pass == 0) {
|
constexpr int thumbOffsetX = (TILE_W - THUMB_W) / 2;
|
||||||
if (file.type == F_DIRECTORY) {
|
constexpr int thumbOffsetY = (TILE_H - TILE_TEXT_H - THUMB_H) / 2;
|
||||||
constexpr int iconOffsetX = (TILE_W - FOLDERICON_WIDTH) / 2;
|
renderer.drawBitmap(bitmap, tileX + thumbOffsetX, tileY + thumbOffsetY, THUMB_W, THUMB_H);
|
||||||
constexpr int iconOffsetY = (TILE_H - TILE_TEXT_H - FOLDERICON_HEIGHT) / 2;
|
|
||||||
renderer.drawIcon(FolderIcon, tileX + iconOffsetX, tileY + iconOffsetY, FOLDERICON_WIDTH, FOLDERICON_HEIGHT);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.type == F_BMP) {
|
|
||||||
File bmpFile = SD.open((basepath + "/" + file.name).c_str());
|
|
||||||
if (bmpFile) {
|
|
||||||
Bitmap bitmap(bmpFile);
|
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
|
||||||
if (bitmap.hasGreyscale()) {
|
|
||||||
hasGeyscaleBitmaps = true;
|
|
||||||
}
|
|
||||||
constexpr int thumbOffsetX = (TILE_W - THUMB_W) / 2;
|
|
||||||
constexpr int thumbOffsetY = (TILE_H - TILE_TEXT_H - THUMB_H) / 2;
|
|
||||||
renderer.drawBitmap(bitmap, tileX + thumbOffsetX, tileY + thumbOffsetY, THUMB_W, THUMB_H);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pass == 0) {
|
|
||||||
renderer.drawTextInBox(UI_FONT_ID, tileX + TILE_PADDING, tileY + TILE_H - TILE_TEXT_H, TILE_W - 2 * TILE_PADDING, TILE_TEXT_H, file.basename.c_str(), true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pass == 0) {
|
renderer.drawTextInBox(UI_FONT_ID, tileX + TILE_PADDING, tileY + TILE_H - TILE_TEXT_H, TILE_W - 2 * TILE_PADDING, TILE_TEXT_H, file.basename.c_str(), true);
|
||||||
update(false);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
if (hasGeyscaleBitmaps) {
|
|
||||||
renderer.storeBwBuffer();
|
|
||||||
} else {
|
|
||||||
// we can skip grayscale passes if no bitmaps use it
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (pass == 1) {
|
|
||||||
renderer.copyGrayscaleLsbBuffers();
|
|
||||||
} else {
|
|
||||||
renderer.copyGrayscaleMsbBuffers();
|
|
||||||
renderer.displayGrayBuffer();
|
|
||||||
renderer.setRenderMode(GfxRenderer::BW);
|
|
||||||
renderer.restoreBwBuffer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update(false);
|
||||||
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ struct FileInfo {
|
|||||||
std::string name;
|
std::string name;
|
||||||
std::string basename;
|
std::string basename;
|
||||||
FileType type;
|
FileType type;
|
||||||
|
std::string thumbPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
class GridBrowserActivity final : public Activity {
|
class GridBrowserActivity final : public Activity {
|
||||||
@ -36,12 +37,14 @@ class GridBrowserActivity final : public Activity {
|
|||||||
const std::function<void(const std::string&)> onSelect;
|
const std::function<void(const std::string&)> onSelect;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void displayTaskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
static void loadThumbsTaskTrampoline(void* param);
|
||||||
void render(bool clear) const;
|
void render(bool clear) const;
|
||||||
void update(bool render) const;
|
void update(bool render) const;
|
||||||
void loadFiles();
|
void loadFiles();
|
||||||
void drawSelectionRectangle(int tileIndex, bool black) const;
|
void drawSelectionRectangle(int tileIndex, bool black) const;
|
||||||
|
std::string loadEpubThumb(std::string path);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit GridBrowserActivity(GfxRenderer& renderer, InputManager& inputManager,
|
explicit GridBrowserActivity(GfxRenderer& renderer, InputManager& inputManager,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user