mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4970ef1f6b |
No files matched your search
+6
-6
@@ -501,7 +501,7 @@ The **Sleep Screen** setting controls what is displayed when the device goes to
|
||||
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
|
||||
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
|
||||
| **Cover + Custom** | The cover of the currently open book, shown only while actively reading. Falls back to **Custom** behavior when not reading. |
|
||||
| **Transparent** | A BMP overlay drawn over the current screen. Supports alpha transparency in 32-bit BGRA images and treats white as transparent in regular BMPs. Falls back to **Dark** if no valid overlay image is found. |
|
||||
| **Transparent** | A BMP or PNG overlay drawn over the current screen. Supports PNG and 32-bit BGRA alpha transparency, and treats white as transparent in regular BMPs. Falls back to **Dark** if no valid overlay image is found. |
|
||||
| **None** | A blank screen. |
|
||||
|
||||
#### Cover settings
|
||||
@@ -520,17 +520,17 @@ To use custom sleep images, set the sleep screen mode to **Custom** or **Cover +
|
||||
|
||||
#### Transparent overlay images
|
||||
|
||||
To use transparent sleep overlays, set the sleep screen mode to **Transparent**, then place BMP files on the SD card:
|
||||
To use transparent sleep overlays, set the sleep screen mode to **Transparent**, then place BMP or PNG files on the SD card:
|
||||
|
||||
- **Multiple Images (recommended):** Create a `.sleep-overlay` directory in the root of the SD card and place any number of valid overlay `.bmp` images inside. One will be randomly selected each time the device sleeps. A directory named `sleep-overlay` is also accepted as a fallback.
|
||||
- **Single Image:** Place a file named `sleep-overlay.bmp` in the root directory. This takes priority over the `.sleep-overlay`/`sleep-overlay` directories.
|
||||
- **Multiple Images (recommended):** Create a `.sleep-overlay` directory in the root of the SD card and place any number of valid overlay `.bmp` or `.png` images inside. One will be randomly selected each time the device sleeps. A directory named `sleep-overlay` is also accepted as a fallback.
|
||||
- **Single Image:** Place `sleep-overlay.bmp` or `sleep-overlay.png` in the root directory. A root BMP takes priority over a root PNG, and both take priority over the `.sleep-overlay`/`sleep-overlay` directories.
|
||||
|
||||
Transparent overlay files are intentionally separate from normal sleep images. Regular BMP formats supported by CrossPoint are accepted; white pixels leave the existing screen unchanged. For per-pixel alpha transparency, use a 32-bit BGRA BMP with both visible and non-opaque pixels. Opaque white pixels in these alpha images erase the content behind them.
|
||||
Transparent overlay files are intentionally separate from normal sleep images. Regular BMP formats supported by CrossPoint are accepted; white pixels leave the existing screen unchanged. For per-pixel alpha transparency, use a PNG with an alpha channel or a 32-bit BGRA BMP with both visible and non-opaque pixels. Opaque white pixels in alpha images erase the content behind them.
|
||||
|
||||
> [!TIP]
|
||||
> For best results:
|
||||
> - For non-transparent **Custom** mode, use uncompressed BMP files with 24-bit color depth.
|
||||
> - For **Transparent** mode, use a regular BMP for white-as-transparent artwork or an uncompressed 32-bit BGRA BMP for per-pixel alpha.
|
||||
> - For **Transparent** mode, use a PNG or uncompressed 32-bit BGRA BMP for per-pixel alpha, or a regular BMP for white-as-transparent artwork.
|
||||
> - X4: Use a resolution of 480x800 pixels to match the device's screen resolution.
|
||||
> - X3: Use a resolution of 528x792 pixels to match the device's screen resolution.
|
||||
|
||||
|
||||
@@ -144,14 +144,14 @@ struct DirectPixelWriter {
|
||||
// Write a single 2-bit dithered pixel value to the framebuffer.
|
||||
// Must be called after beginRow() for the current row.
|
||||
// No bounds checking — caller guarantees coordinates are valid.
|
||||
inline void writePixel(int logicalX, uint8_t pixelValue) const {
|
||||
inline void writePixel(int logicalX, uint8_t pixelValue, bool writeWhiteInBw = false) const {
|
||||
// Determine whether to draw based on render mode
|
||||
bool draw;
|
||||
bool state;
|
||||
switch (mode) {
|
||||
case GfxRenderer::BW:
|
||||
draw = (pixelValue < 3);
|
||||
state = true;
|
||||
draw = writeWhiteInBw || pixelValue < 3;
|
||||
state = pixelValue < 3;
|
||||
break;
|
||||
case GfxRenderer::GRAYSCALE_MSB:
|
||||
draw = (pixelValue == 1 || pixelValue == 2);
|
||||
|
||||
@@ -18,6 +18,9 @@ struct RenderConfig {
|
||||
bool useDithering = true;
|
||||
bool performanceMode = false;
|
||||
bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation)
|
||||
float sourceCropX = 0.0f; // Fraction cropped equally from the left and right edges
|
||||
float sourceCropY = 0.0f; // Fraction cropped equally from the top and bottom edges
|
||||
bool preserveAlpha = false; // Skip transparent pixels instead of compositing them against white
|
||||
std::string cachePath; // If non-empty, decoder will write pixel cache to this path
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <Memory.h>
|
||||
#include <PNGdec.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
@@ -21,6 +22,7 @@ namespace {
|
||||
// The draw callback receives this via pDraw->pUser (set by png.decode()).
|
||||
// The file I/O callbacks receive the HalFile* via pFile->fHandle (set by pngOpen()).
|
||||
struct PngContext {
|
||||
PNG* decoder{nullptr};
|
||||
GfxRenderer* renderer{nullptr};
|
||||
const RenderConfig* config{nullptr};
|
||||
int screenWidth{0};
|
||||
@@ -30,14 +32,18 @@ struct PngContext {
|
||||
float scale{1.f};
|
||||
int srcWidth{0};
|
||||
int srcHeight{0};
|
||||
int cropLeft{0};
|
||||
int cropTop{0};
|
||||
int visibleWidth{0};
|
||||
int visibleHeight{0};
|
||||
int dstWidth{0};
|
||||
int dstHeight{0};
|
||||
int lastDstY{-1}; // Track last rendered destination Y to avoid duplicates
|
||||
|
||||
PixelCache cache;
|
||||
bool caching{false};
|
||||
|
||||
uint8_t* grayLineBuffer{nullptr};
|
||||
uint8_t* alphaLineBuffer{nullptr};
|
||||
};
|
||||
|
||||
// File I/O callbacks use pFile->fHandle to access the HalFile*,
|
||||
@@ -131,19 +137,31 @@ uint8_t expandSampleToByte(uint8_t sample, int bitsPerSample) {
|
||||
return static_cast<uint8_t>((sample * 255U) / maxSample);
|
||||
}
|
||||
|
||||
// Convert entire source line to grayscale with alpha blending to white background.
|
||||
uint8_t alphaThreshold4x4(const int x, const int y) {
|
||||
static constexpr uint8_t BAYER_4X4[16] = {0, 128, 32, 160, 192, 64, 224, 96, 48, 176, 16, 144, 240, 112, 208, 80};
|
||||
return BAYER_4X4[((y & 0x03) << 2) | (x & 0x03)];
|
||||
}
|
||||
|
||||
// Convert an entire source line to grayscale and optionally retain alpha.
|
||||
// Low-bit-depth grayscale/indexed scanlines are packed most-significant sample first.
|
||||
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
||||
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
||||
void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, int bitsPerSample,
|
||||
uint8_t* palette, int hasAlpha) {
|
||||
uint8_t* palette, int hasAlpha, uint32_t transparentColor, uint8_t* alphaLine) {
|
||||
const auto store = [grayLine, alphaLine](const int x, const uint8_t gray, const uint8_t alpha) {
|
||||
grayLine[x] = alphaLine ? gray : static_cast<uint8_t>((gray * alpha + 255u * (255u - alpha)) / 255u);
|
||||
if (alphaLine) alphaLine[x] = alpha;
|
||||
};
|
||||
|
||||
switch (pixelType) {
|
||||
case PNG_PIXEL_GRAYSCALE:
|
||||
if (bitsPerSample == 8) {
|
||||
if (bitsPerSample == 8 && !hasAlpha && !alphaLine) {
|
||||
memcpy(grayLine, pPixels, width);
|
||||
} else {
|
||||
for (int x = 0; x < width; x++) {
|
||||
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||
const uint8_t sample = readPackedSample(pPixels, x, bitsPerSample);
|
||||
const uint8_t gray = expandSampleToByte(sample, bitsPerSample);
|
||||
store(x, gray, hasAlpha && sample == static_cast<uint8_t>(transparentColor) ? 0 : 255);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -151,53 +169,44 @@ void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int
|
||||
case PNG_PIXEL_TRUECOLOR:
|
||||
for (int x = 0; x < width; x++) {
|
||||
const uint8_t* p = &pPixels[x * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
const uint8_t gray = static_cast<uint8_t>((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
const uint32_t color = (static_cast<uint32_t>(p[0]) << 16) | (static_cast<uint32_t>(p[1]) << 8) | p[2];
|
||||
store(x, gray, hasAlpha && color == transparentColor ? 0 : 255);
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_INDEXED:
|
||||
if (palette) {
|
||||
if (hasAlpha) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||
uint8_t* p = &palette[idx * 3];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = palette[768 + idx];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||
uint8_t* p = &palette[idx * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
for (int x = 0; x < width; x++) {
|
||||
const uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||
const uint8_t* p = &palette[idx * 3];
|
||||
const uint8_t gray = static_cast<uint8_t>((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
store(x, gray, hasAlpha ? palette[768 + idx] : 255);
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < width; x++) {
|
||||
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||
store(x, expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample), 255);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_GRAY_ALPHA:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t gray = pPixels[x * 2];
|
||||
uint8_t alpha = pPixels[x * 2 + 1];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
store(x, pPixels[x * 2], pPixels[x * 2 + 1]);
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||
for (int x = 0; x < width; x++) {
|
||||
const uint8_t* p = &pPixels[x * 4];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = p[3];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
const uint8_t gray = static_cast<uint8_t>((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
store(x, gray, p[3]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
memset(grayLine, 128, width);
|
||||
if (alphaLine) memset(alphaLine, 255, width);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -208,25 +217,29 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
|
||||
int srcY = pDraw->y;
|
||||
int srcWidth = ctx->srcWidth;
|
||||
if (srcY < ctx->cropTop || srcY >= ctx->cropTop + ctx->visibleHeight) return 1;
|
||||
const int visibleSrcY = srcY - ctx->cropTop;
|
||||
|
||||
// Map source rows with the exact output-height ratio. During downscaling,
|
||||
// multiple source rows can select the same output row; during upscaling, one
|
||||
// source row must be repeated across every output row in its range. Emitting
|
||||
// only the first row of an upscale leaves zero-filled (black) gaps in the
|
||||
// streamed pixel cache.
|
||||
int firstDstY = (srcY * ctx->dstHeight) / ctx->srcHeight;
|
||||
int firstDstY = (visibleSrcY * ctx->dstHeight) / ctx->visibleHeight;
|
||||
int endDstY = firstDstY + 1;
|
||||
if (ctx->dstHeight > ctx->srcHeight) {
|
||||
endDstY = ((srcY + 1) * ctx->dstHeight) / ctx->srcHeight;
|
||||
if (ctx->dstHeight > ctx->visibleHeight) {
|
||||
endDstY = ((visibleSrcY + 1) * ctx->dstHeight) / ctx->visibleHeight;
|
||||
}
|
||||
|
||||
if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1;
|
||||
if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1;
|
||||
if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight;
|
||||
|
||||
// Convert entire source line to grayscale (improves cache locality)
|
||||
// PNGdec parses tRNS while decoding, after open() returns, so query the
|
||||
// transparent color here rather than caching its pre-decode value.
|
||||
const uint32_t transparentColor = ctx->decoder ? ctx->decoder->getTransparentColor() : 0;
|
||||
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->iBpp, pDraw->pPalette,
|
||||
pDraw->iHasAlpha);
|
||||
pDraw->iHasAlpha, transparentColor, ctx->alphaLineBuffer);
|
||||
|
||||
// Render scaled rows using Bresenham-style integer stepping (no floating-point division)
|
||||
int dstWidth = ctx->dstWidth;
|
||||
@@ -261,27 +274,30 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
}
|
||||
}
|
||||
|
||||
int srcX = 0;
|
||||
int srcX = ctx->cropLeft;
|
||||
int error = 0;
|
||||
|
||||
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||
int outX = outXBase + dstX;
|
||||
if (outX < screenWidth) {
|
||||
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||
const uint8_t alpha = ctx->alphaLineBuffer ? ctx->alphaLineBuffer[srcX] : 255;
|
||||
if (alpha >= 8 && alpha > alphaThreshold4x4(outX, outY)) {
|
||||
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||
|
||||
uint8_t ditheredGray;
|
||||
if (useDithering) {
|
||||
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||
} else {
|
||||
ditheredGray = gray / 85;
|
||||
if (ditheredGray > 3) ditheredGray = 3;
|
||||
uint8_t ditheredGray;
|
||||
if (useDithering) {
|
||||
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||
} else {
|
||||
ditheredGray = gray / 85;
|
||||
if (ditheredGray > 3) ditheredGray = 3;
|
||||
}
|
||||
pw.writePixel(outX, ditheredGray, ctx->alphaLineBuffer != nullptr);
|
||||
if (caching) cw.writePixel(outX, ditheredGray);
|
||||
}
|
||||
pw.writePixel(outX, ditheredGray);
|
||||
if (caching) cw.writePixel(outX, ditheredGray);
|
||||
}
|
||||
|
||||
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||
error += srcWidth;
|
||||
// Bresenham-style stepping: advance srcX based on ratio visibleWidth/dstWidth
|
||||
error += ctx->visibleWidth;
|
||||
while (error >= dstWidth) {
|
||||
error -= dstWidth;
|
||||
srcX++;
|
||||
@@ -340,6 +356,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
}
|
||||
|
||||
PngContext ctx;
|
||||
ctx.decoder = png.get();
|
||||
ctx.renderer = &renderer;
|
||||
ctx.config = &config;
|
||||
ctx.screenWidth = renderer.getScreenWidth();
|
||||
@@ -360,28 +377,36 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
// Calculate output dimensions
|
||||
ctx.srcWidth = png->getWidth();
|
||||
ctx.srcHeight = png->getHeight();
|
||||
ctx.cropLeft = static_cast<int>(ctx.srcWidth * std::clamp(config.sourceCropX, 0.0f, 0.99f) / 2.0f);
|
||||
ctx.cropTop = static_cast<int>(ctx.srcHeight * std::clamp(config.sourceCropY, 0.0f, 0.99f) / 2.0f);
|
||||
ctx.visibleWidth = ctx.srcWidth - 2 * ctx.cropLeft;
|
||||
ctx.visibleHeight = ctx.srcHeight - 2 * ctx.cropTop;
|
||||
if (ctx.visibleWidth <= 0 || ctx.visibleHeight <= 0) {
|
||||
LOG_ERR("PNG", "PNG crop leaves no visible pixels");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
||||
ctx.dstWidth = config.maxWidth;
|
||||
ctx.dstHeight = config.maxHeight;
|
||||
ctx.scale = (float)ctx.dstWidth / ctx.srcWidth;
|
||||
ctx.scale = (float)ctx.dstWidth / ctx.visibleWidth;
|
||||
} else {
|
||||
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||
float scaleX = (float)config.maxWidth / ctx.srcWidth;
|
||||
float scaleY = (float)config.maxHeight / ctx.srcHeight;
|
||||
float scaleX = (float)config.maxWidth / ctx.visibleWidth;
|
||||
float scaleY = (float)config.maxHeight / ctx.visibleHeight;
|
||||
ctx.scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
if (ctx.scale > 1.0f) ctx.scale = 1.0f; // Don't upscale
|
||||
|
||||
ctx.dstWidth = (int)(ctx.srcWidth * ctx.scale);
|
||||
ctx.dstHeight = (int)(ctx.srcHeight * ctx.scale);
|
||||
ctx.dstWidth = (int)(ctx.visibleWidth * ctx.scale);
|
||||
ctx.dstHeight = (int)(ctx.visibleHeight * ctx.scale);
|
||||
}
|
||||
ctx.lastDstY = -1; // Reset row tracking
|
||||
|
||||
const int pixelType = png->getPixelType();
|
||||
const int bitsPerSample = png->getBpp();
|
||||
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth,
|
||||
ctx.dstHeight, ctx.scale, pixelType, bitsPerSample);
|
||||
LOG_DBG("PNG", "PNG %dx%d (visible %dx%d) -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight,
|
||||
ctx.visibleWidth, ctx.visibleHeight, ctx.dstWidth, ctx.dstHeight, ctx.scale, pixelType, bitsPerSample);
|
||||
|
||||
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType, bitsPerSample);
|
||||
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
|
||||
@@ -410,12 +435,18 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
return false;
|
||||
}
|
||||
|
||||
auto grayLineBuffer = makeUniqueNoThrow<uint8_t[]>(grayBufSize);
|
||||
if (!grayLineBuffer) {
|
||||
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
||||
// tRNS chunks are parsed during decode(), so hasAlpha() is not reliable yet
|
||||
// for color-key and indexed transparency. Reserve the alpha line whenever
|
||||
// the caller requests preservation; opaque pixels simply store alpha 255.
|
||||
const bool retainAlpha = config.preserveAlpha;
|
||||
const size_t lineBufferBytes = grayBufSize * (retainAlpha ? 2u : 1u);
|
||||
auto lineBuffers = makeUniqueNoThrow<uint8_t[]>(lineBufferBytes);
|
||||
if (!lineBuffers) {
|
||||
LOG_ERR("PNG", "Failed to allocate PNG line buffers (%u bytes)", static_cast<unsigned>(lineBufferBytes));
|
||||
return false;
|
||||
}
|
||||
ctx.grayLineBuffer = grayLineBuffer.get();
|
||||
ctx.grayLineBuffer = lineBuffers.get();
|
||||
ctx.alphaLineBuffer = retainAlpha ? ctx.grayLineBuffer + grayBufSize : nullptr;
|
||||
|
||||
// Stream the pixel cache to disk. PNGdec delivers source scanlines top to
|
||||
// bottom and we emit at most one (downscaled) output row per callback, so the
|
||||
@@ -423,7 +454,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
// unlike the old full-image buffer it neither competes with the ~44KB decoder
|
||||
// nor forces larger images to skip caching - which previously meant a full
|
||||
// re-decode on every one of an image page's ~14 render passes.
|
||||
ctx.caching = !config.cachePath.empty();
|
||||
ctx.caching = !config.preserveAlpha && !config.cachePath.empty();
|
||||
if (ctx.caching) {
|
||||
if (!ctx.cache.begin(config.cachePath, ctx.dstWidth, ctx.dstHeight, config.x, config.y, 1)) {
|
||||
LOG_ERR("PNG", "Failed to start cache stream, continuing without caching");
|
||||
@@ -436,6 +467,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
unsigned long decodeTime = millis() - decodeStart;
|
||||
|
||||
ctx.grayLineBuffer = nullptr;
|
||||
ctx.alphaLineBuffer = nullptr;
|
||||
|
||||
if (rc != PNG_SUCCESS) {
|
||||
LOG_ERR("PNG", "Decode failed: %d", rc);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "SleepActivity.h"
|
||||
|
||||
#include <Epub.h>
|
||||
#include <Epub/converters/PngToFramebufferConverter.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalDisplay.h>
|
||||
@@ -8,6 +9,7 @@
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
#include <Memory.h>
|
||||
#include <PNGdec.h>
|
||||
#include <Txt.h>
|
||||
#include <Xtc.h>
|
||||
|
||||
@@ -29,7 +31,8 @@
|
||||
namespace {
|
||||
|
||||
// Kept separate from /sleep.bmp and /.sleep so alpha-overlay art does not mix with full-screen wallpapers.
|
||||
constexpr char TRANSPARENT_SLEEP_ROOT[] = "/sleep-overlay.bmp";
|
||||
constexpr char TRANSPARENT_SLEEP_ROOT_BMP[] = "/sleep-overlay.bmp";
|
||||
constexpr char TRANSPARENT_SLEEP_ROOT_PNG[] = "/sleep-overlay.png";
|
||||
constexpr char TRANSPARENT_SLEEP_DIR[] = "/.sleep-overlay";
|
||||
constexpr char TRANSPARENT_SLEEP_LEGACY_DIR[] = "/sleep-overlay";
|
||||
constexpr size_t MAX_SLEEP_FILE_NAME_LEN = 256;
|
||||
@@ -71,6 +74,50 @@ uint32_t readLE32(HalFile& file) {
|
||||
(static_cast<uint32_t>(b3) << 24);
|
||||
}
|
||||
|
||||
uint32_t readBE32(HalFile& file) {
|
||||
const int c0 = file.read();
|
||||
const int c1 = file.read();
|
||||
const int c2 = file.read();
|
||||
const int c3 = file.read();
|
||||
if (c0 < 0 || c1 < 0 || c2 < 0 || c3 < 0) return 0;
|
||||
return (static_cast<uint32_t>(c0) << 24) | (static_cast<uint32_t>(c1) << 16) | (static_cast<uint32_t>(c2) << 8) |
|
||||
static_cast<uint32_t>(c3);
|
||||
}
|
||||
|
||||
bool isValidPngHeader(HalFile& file) {
|
||||
static constexpr uint8_t PNG_SIGNATURE[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
|
||||
static constexpr uint32_t MAX_SOURCE_PIXELS = 2048u * 1536u;
|
||||
uint8_t signature[8];
|
||||
if (!file.seek(0) || file.read(signature, sizeof(signature)) != static_cast<int>(sizeof(signature)) ||
|
||||
!std::equal(std::begin(signature), std::end(signature), std::begin(PNG_SIGNATURE))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t ihdrLength = readBE32(file);
|
||||
char chunkType[4];
|
||||
if (file.read(reinterpret_cast<uint8_t*>(chunkType), sizeof(chunkType)) != static_cast<int>(sizeof(chunkType)) ||
|
||||
ihdrLength != 13 || !std::equal(std::begin(chunkType), std::end(chunkType), "IHDR")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t width = readBE32(file);
|
||||
const uint32_t height = readBE32(file);
|
||||
const int bitDepth = file.read();
|
||||
const int colorType = file.read();
|
||||
const int compression = file.read();
|
||||
const int filter = file.read();
|
||||
const int interlace = file.read();
|
||||
|
||||
const bool supportedBitDepth =
|
||||
bitDepth == 8 || ((colorType == PNG_PIXEL_GRAYSCALE || colorType == PNG_PIXEL_INDEXED) &&
|
||||
(bitDepth == 1 || bitDepth == 2 || bitDepth == 4));
|
||||
const bool supportedColorType = colorType == PNG_PIXEL_GRAYSCALE || colorType == PNG_PIXEL_TRUECOLOR ||
|
||||
colorType == PNG_PIXEL_INDEXED || colorType == PNG_PIXEL_GRAY_ALPHA ||
|
||||
colorType == PNG_PIXEL_TRUECOLOR_ALPHA;
|
||||
return width > 0 && height > 0 && width <= 2048 && height <= 3072 && width * height <= MAX_SOURCE_PIXELS &&
|
||||
supportedBitDepth && supportedColorType && compression == 0 && filter == 0 && interlace == 0;
|
||||
}
|
||||
|
||||
BitmapPlacement calculateBitmapPlacement(const int bitmapWidth, const int bitmapHeight, const GfxRenderer& renderer) {
|
||||
BitmapPlacement placement;
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
@@ -335,20 +382,26 @@ void pushRecentSleepIndex(const SleepRecentKind recentKind, const uint16_t idx)
|
||||
}
|
||||
}
|
||||
|
||||
bool findNextValidSleepBmp(HalFile& dir, char* name) {
|
||||
bool findNextValidSleepImage(HalFile& dir, const SleepRecentKind recentKind, char* name) {
|
||||
for (auto dirFile = dir.openNextFile(); dirFile; dirFile = dir.openNextFile()) {
|
||||
if (dirFile.isDirectory()) continue;
|
||||
|
||||
dirFile.getName(name, MAX_SLEEP_FILE_NAME_LEN);
|
||||
if (name[0] == '\0' || name[0] == '.') continue;
|
||||
|
||||
if (!FsHelpers::hasBmpExtension(name)) {
|
||||
const bool isBmp = FsHelpers::hasBmpExtension(name);
|
||||
const bool isPng = recentKind == SleepRecentKind::Overlay && FsHelpers::hasPngExtension(std::string_view{name});
|
||||
if (!isBmp && !isPng) {
|
||||
LOG_DBG("SLP", "Skipping unsupported sleep image: %s", name);
|
||||
continue;
|
||||
}
|
||||
|
||||
Bitmap bitmap(dirFile);
|
||||
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
|
||||
const bool isValid = isBmp ? [&dirFile]() {
|
||||
Bitmap bitmap(dirFile);
|
||||
return bitmap.parseHeaders() == BmpReaderError::Ok;
|
||||
}()
|
||||
: isValidPngHeader(dirFile);
|
||||
if (!isValid) {
|
||||
LOG_DBG("SLP", "Skipping invalid sleep image: %s", name);
|
||||
continue;
|
||||
}
|
||||
@@ -368,7 +421,7 @@ bool selectRandomSleepFile(const char* dirPath, const SleepRecentKind recentKind
|
||||
}
|
||||
|
||||
uint16_t fileCount = 0;
|
||||
while (fileCount < UINT16_MAX && findNextValidSleepBmp(dir, name.get())) ++fileCount;
|
||||
while (fileCount < UINT16_MAX && findNextValidSleepImage(dir, recentKind, name.get())) ++fileCount;
|
||||
if (fileCount == 0) return false;
|
||||
|
||||
// Pick a random wallpaper, excluding recently shown ones.
|
||||
@@ -383,7 +436,7 @@ bool selectRandomSleepFile(const char* dirPath, const SleepRecentKind recentKind
|
||||
|
||||
dir.rewindDirectory();
|
||||
for (uint16_t index = 0; index <= randomFileIndex; ++index) {
|
||||
if (!findNextValidSleepBmp(dir, name.get())) return false;
|
||||
if (!findNextValidSleepImage(dir, recentKind, name.get())) return false;
|
||||
}
|
||||
|
||||
selectedPath.reserve(strlen(dirPath) + 1 + strlen(name.get()));
|
||||
@@ -395,6 +448,34 @@ bool selectRandomSleepFile(const char* dirPath, const SleepRecentKind recentKind
|
||||
return true;
|
||||
}
|
||||
|
||||
bool drawSleepPopupPreservingFrame(GfxRenderer& renderer) {
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const int frameThickness = metrics.popupFrameThickness;
|
||||
const int popupY = static_cast<int>(renderer.getScreenHeight() * metrics.popupTopOffsetRatio);
|
||||
const int popupHeight = renderer.getLineHeight(UI_12_FONT_ID) + metrics.popupMarginY * 2;
|
||||
const int bandTop = std::max(0, popupY - frameThickness);
|
||||
const int bandBottom = std::min(renderer.getScreenHeight(), popupY + popupHeight + frameThickness);
|
||||
const int bandHeight = bandBottom - bandTop;
|
||||
const size_t bandBytes = renderer.getRegionByteSize(0, bandTop, renderer.getScreenWidth(), bandHeight);
|
||||
|
||||
auto savedBand = makeUniqueNoThrow<uint8_t[]>(bandBytes);
|
||||
if (!savedBand) {
|
||||
LOG_ERR("SLP", "OOM: sleep popup background (%u bytes)", static_cast<unsigned>(bandBytes));
|
||||
return false;
|
||||
}
|
||||
if (!renderer.copyRegionToBuffer(0, bandTop, renderer.getScreenWidth(), bandHeight, savedBand.get(), bandBytes)) {
|
||||
LOG_ERR("SLP", "Failed to save sleep popup background");
|
||||
return false;
|
||||
}
|
||||
|
||||
GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP));
|
||||
if (!renderer.copyBufferToRegion(0, bandTop, renderer.getScreenWidth(), bandHeight, savedBand.get(), bandBytes)) {
|
||||
LOG_ERR("SLP", "Failed to restore sleep popup background");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SleepActivity::onEnter() {
|
||||
@@ -421,6 +502,10 @@ void SleepActivity::onEnter() {
|
||||
// output-level inversion first so the retained content keeps its visible
|
||||
// polarity after the display driver returns to normal.
|
||||
if (frameWasInverted) renderer.invertScreen();
|
||||
if (APP_STATE.lastSleepFromReader) {
|
||||
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||
}
|
||||
drawSleepPopupPreservingFrame(renderer);
|
||||
if (APP_STATE.lastSleepFromReader) {
|
||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
}
|
||||
@@ -585,26 +670,68 @@ bool SleepActivity::renderSleepOverlayFile(HalFile& file, const char* pathForLog
|
||||
return true;
|
||||
}
|
||||
|
||||
void SleepActivity::renderTransparentCustomSleepScreen() const {
|
||||
{
|
||||
HalFile rootFile;
|
||||
if (Storage.openFileForRead("SLP", TRANSPARENT_SLEEP_ROOT, rootFile)) {
|
||||
if (renderSleepOverlayFile(rootFile, TRANSPARENT_SLEEP_ROOT)) return;
|
||||
}
|
||||
bool SleepActivity::renderTransparentOverlayPng(const std::string& path) const {
|
||||
ImageDimensions dimensions;
|
||||
if (!PngToFramebufferConverter::getDimensionsStatic(path, dimensions)) return false;
|
||||
|
||||
const auto placement = calculateBitmapPlacement(dimensions.width, dimensions.height, renderer);
|
||||
RenderConfig config;
|
||||
config.x = placement.x;
|
||||
config.y = placement.y;
|
||||
config.maxWidth = renderer.getScreenWidth();
|
||||
config.maxHeight = renderer.getScreenHeight();
|
||||
config.useDithering = false;
|
||||
config.sourceCropX = placement.cropX;
|
||||
config.sourceCropY = placement.cropY;
|
||||
config.useExactDimensions = placement.cropX > 0.0f || placement.cropY > 0.0f;
|
||||
config.preserveAlpha = true;
|
||||
|
||||
PngToFramebufferConverter converter;
|
||||
LOG_DBG("SLP", "Rendering transparent PNG overlay: %s (%dx%d)", path.c_str(), dimensions.width, dimensions.height);
|
||||
|
||||
if (!converter.decodeToFramebuffer(path, renderer, config)) return false;
|
||||
renderer.displayGrayscaleBase(HalDisplay::HALF_REFRESH);
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
if (!converter.decodeToFramebuffer(path, renderer, config)) {
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
return true;
|
||||
}
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
if (!converter.decodeToFramebuffer(path, renderer, config)) {
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
return true;
|
||||
}
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
|
||||
renderer.displayGrayBuffer();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SleepActivity::renderSleepOverlayPath(const std::string& path) const {
|
||||
if (FsHelpers::hasPngExtension(path)) {
|
||||
return Storage.exists(path.c_str()) && renderTransparentOverlayPng(path);
|
||||
}
|
||||
|
||||
HalFile file;
|
||||
return Storage.openFileForRead("SLP", path, file) && renderSleepOverlayFile(file, path.c_str());
|
||||
}
|
||||
|
||||
void SleepActivity::renderTransparentCustomSleepScreen() const {
|
||||
if (renderSleepOverlayPath(TRANSPARENT_SLEEP_ROOT_BMP)) return;
|
||||
if (renderSleepOverlayPath(TRANSPARENT_SLEEP_ROOT_PNG)) return;
|
||||
|
||||
std::string selectedPath;
|
||||
if (!selectRandomSleepFile(TRANSPARENT_SLEEP_DIR, SleepRecentKind::Overlay, selectedPath)) {
|
||||
selectRandomSleepFile(TRANSPARENT_SLEEP_LEGACY_DIR, SleepRecentKind::Overlay, selectedPath);
|
||||
}
|
||||
|
||||
if (!selectedPath.empty()) {
|
||||
HalFile overlayFile;
|
||||
if (Storage.openFileForRead("SLP", selectedPath, overlayFile) &&
|
||||
renderSleepOverlayFile(overlayFile, selectedPath.c_str())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!selectedPath.empty() && renderSleepOverlayPath(selectedPath)) return;
|
||||
|
||||
LOG_ERR("SLP", "No valid transparent sleep overlay found");
|
||||
renderDefaultSleepScreen();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
#include "activities/Activity.h"
|
||||
|
||||
class Bitmap;
|
||||
@@ -16,6 +18,8 @@ class SleepActivity final : public Activity {
|
||||
void renderCoverSleepScreen() const;
|
||||
void renderBitmapSleepScreen(const Bitmap& bitmap, bool preserveBackground = false) const;
|
||||
bool renderSleepOverlayFile(HalFile& file, const char* pathForLog) const;
|
||||
bool renderTransparentOverlayPng(const std::string& path) const;
|
||||
bool renderSleepOverlayPath(const std::string& path) const;
|
||||
void renderLastScreenSleepScreen() const;
|
||||
void renderTransparentCustomSleepScreen() const;
|
||||
void renderBlankSleepScreen() const;
|
||||
|
||||
@@ -69,7 +69,7 @@ void FileBrowserActivity::loadFiles() {
|
||||
}
|
||||
} else if (FsHelpers::hasEpubExtension(filename) || FsHelpers::hasXtcExtension(filename) ||
|
||||
FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename) ||
|
||||
FsHelpers::hasBmpExtension(filename)) {
|
||||
FsHelpers::hasBmpExtension(filename) || FsHelpers::hasPngExtension(filename)) {
|
||||
files.emplace_back(filename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ bool ReaderActivity::isTxtFile(const std::string& path) {
|
||||
FsHelpers::hasMarkdownExtension(path); // Treat .md as txt files (until we have a markdown reader)
|
||||
}
|
||||
|
||||
bool ReaderActivity::isBmpFile(const std::string& path) { return FsHelpers::hasBmpExtension(path); }
|
||||
bool ReaderActivity::isImageFile(const std::string& path) {
|
||||
return FsHelpers::hasBmpExtension(path) || FsHelpers::hasPngExtension(path);
|
||||
}
|
||||
|
||||
int ReaderActivity::initialRefreshCountdown() const {
|
||||
if (!allowFastInitialRefresh) return 0;
|
||||
@@ -152,7 +154,7 @@ void ReaderActivity::onEnter() {
|
||||
sdFontSystem.ensureLoaded(renderer);
|
||||
|
||||
currentBookPath = initialBookPath;
|
||||
if (isBmpFile(initialBookPath)) {
|
||||
if (isImageFile(initialBookPath)) {
|
||||
onGoToBmpViewer(initialBookPath);
|
||||
} else if (isXtcFile(initialBookPath)) {
|
||||
auto xtc = loadXtc(initialBookPath);
|
||||
|
||||
@@ -18,7 +18,7 @@ class ReaderActivity final : public Activity {
|
||||
static std::unique_ptr<Txt> loadTxt(const std::string& path);
|
||||
static bool isXtcFile(const std::string& path);
|
||||
static bool isTxtFile(const std::string& path);
|
||||
static bool isBmpFile(const std::string& path);
|
||||
static bool isImageFile(const std::string& path);
|
||||
|
||||
void goToLibrary(const std::string& fromBookPath = "");
|
||||
void onGoToEpubReader(std::unique_ptr<Epub> epub);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "BmpViewerActivity.h"
|
||||
|
||||
#include <Bitmap.h>
|
||||
#include <Epub/converters/PngToFramebufferConverter.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
@@ -16,6 +17,7 @@
|
||||
namespace {
|
||||
constexpr char CUSTOM_SLEEP_ROOT_BMP[] = "/sleep.bmp";
|
||||
constexpr char TRANSPARENT_SLEEP_ROOT_BMP[] = "/sleep-overlay.bmp";
|
||||
constexpr char TRANSPARENT_SLEEP_ROOT_PNG[] = "/sleep-overlay.png";
|
||||
constexpr size_t COPY_BUFFER_SIZE = 2048;
|
||||
} // namespace
|
||||
|
||||
@@ -44,7 +46,7 @@ void BmpViewerActivity::loadSiblingImages() {
|
||||
file.getName(name, sizeof(name));
|
||||
if (name[0] != '.') {
|
||||
std::string fname(name);
|
||||
if (FsHelpers::hasBmpExtension(fname)) {
|
||||
if (FsHelpers::hasBmpExtension(fname) || FsHelpers::hasPngExtension(fname)) {
|
||||
siblingImages.push_back(fname);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +65,28 @@ void BmpViewerActivity::loadSiblingImages() {
|
||||
}
|
||||
}
|
||||
|
||||
bool BmpViewerActivity::canSetSleepCover() const {
|
||||
return FsHelpers::hasBmpExtension(filePath) ||
|
||||
(SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::TRANSPARENT_CUSTOM &&
|
||||
FsHelpers::hasPngExtension(filePath));
|
||||
}
|
||||
|
||||
bool BmpViewerActivity::renderPng() {
|
||||
ImageDimensions dimensions;
|
||||
if (!PngToFramebufferConverter::getDimensionsStatic(filePath, dimensions)) return false;
|
||||
if (dimensions.width <= 0 || dimensions.height <= 0) return false;
|
||||
|
||||
const float scale = std::min(static_cast<float>(renderer.getScreenWidth()) / dimensions.width,
|
||||
static_cast<float>(renderer.getScreenHeight()) / dimensions.height);
|
||||
const int width = std::min(renderer.getScreenWidth(), static_cast<int>(dimensions.width * std::min(scale, 1.0f)));
|
||||
const int height = std::min(renderer.getScreenHeight(), static_cast<int>(dimensions.height * std::min(scale, 1.0f)));
|
||||
RenderConfig config{(renderer.getScreenWidth() - width) / 2, (renderer.getScreenHeight() - height) / 2, width,
|
||||
height};
|
||||
|
||||
PngToFramebufferConverter converter;
|
||||
return converter.decodeToFramebuffer(filePath, renderer, config);
|
||||
}
|
||||
|
||||
void BmpViewerActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
@@ -70,13 +94,30 @@ void BmpViewerActivity::onEnter() {
|
||||
loadSiblingImages();
|
||||
}
|
||||
|
||||
HalFile file;
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
Rect popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
|
||||
GUI.fillPopupProgress(renderer, popupRect, 20); // Initial 20% progress
|
||||
// 1. Open the file
|
||||
if (FsHelpers::hasPngExtension(filePath)) {
|
||||
renderer.clearScreen();
|
||||
const bool hasPrevious = siblingImages.size() > 1 && currentImageIndex > 0;
|
||||
const bool hasNext = siblingImages.size() > 1 && currentImageIndex != -1 &&
|
||||
currentImageIndex < static_cast<int>(siblingImages.size()) - 1;
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), canSetSleepCover() ? tr(STR_SET_SLEEP_COVER) : "",
|
||||
hasPrevious ? "<" : "", hasNext ? ">" : "");
|
||||
if (renderPng()) {
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
||||
} else {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_FILE_OPEN_FAILED));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, "", "", "");
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
HalFile file;
|
||||
// 1. Open the BMP file
|
||||
if (Storage.openFileForRead("BMP", filePath, file)) {
|
||||
Bitmap bitmap(file, true);
|
||||
|
||||
@@ -108,8 +149,8 @@ void BmpViewerActivity::onEnter() {
|
||||
bool hasNext = (siblingImages.size() > 1 && currentImageIndex != -1 &&
|
||||
currentImageIndex < static_cast<int>(siblingImages.size()) - 1);
|
||||
|
||||
const auto labels =
|
||||
mappedInput.mapLabels(tr(STR_BACK), tr(STR_SET_SLEEP_COVER), (hasPrevious ? "<" : ""), (hasNext ? ">" : ""));
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), canSetSleepCover() ? tr(STR_SET_SLEEP_COVER) : "",
|
||||
(hasPrevious ? "<" : ""), (hasNext ? ">" : ""));
|
||||
|
||||
GUI.fillPopupProgress(renderer, popupRect, 50);
|
||||
|
||||
@@ -154,7 +195,11 @@ void BmpViewerActivity::doSetSleepCover() {
|
||||
GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
|
||||
|
||||
const bool transparentMode = SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::TRANSPARENT_CUSTOM;
|
||||
const char* destination = transparentMode ? TRANSPARENT_SLEEP_ROOT_BMP : CUSTOM_SLEEP_ROOT_BMP;
|
||||
if (!canSetSleepCover()) return;
|
||||
|
||||
const char* destination =
|
||||
transparentMode ? (FsHelpers::hasPngExtension(filePath) ? TRANSPARENT_SLEEP_ROOT_PNG : TRANSPARENT_SLEEP_ROOT_BMP)
|
||||
: CUSTOM_SLEEP_ROOT_BMP;
|
||||
bool success = filePath == destination;
|
||||
|
||||
if (!success) {
|
||||
@@ -226,7 +271,7 @@ void BmpViewerActivity::loop() {
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
doSetSleepCover();
|
||||
if (canSetSleepCover()) doSetSleepCover();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,10 @@ class BmpViewerActivity final : public Activity {
|
||||
private:
|
||||
void loadSiblingImages();
|
||||
void doSetSleepCover();
|
||||
bool canSetSleepCover() const;
|
||||
bool renderPng();
|
||||
|
||||
std::string filePath;
|
||||
std::vector<std::string> siblingImages;
|
||||
int currentImageIndex = -1;
|
||||
};
|
||||
};
|
||||
@@ -139,7 +139,7 @@ UIIcon UITheme::getFileIcon(const std::string& filename) {
|
||||
if (FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename)) {
|
||||
return Text;
|
||||
}
|
||||
if (FsHelpers::hasBmpExtension(filename)) {
|
||||
if (FsHelpers::hasBmpExtension(filename) || FsHelpers::hasPngExtension(filename)) {
|
||||
return Image;
|
||||
}
|
||||
return File;
|
||||
|
||||
Reference in new issue
Block a user