sleep screen filled with cover image's perimeter dominant color prior to being drawn

This commit is contained in:
cottongin
2026-01-24 03:29:34 -05:00
parent 1e20d30875
commit 2f21f55512
4 changed files with 209 additions and 14 deletions

View File

@@ -12,6 +12,13 @@
#include "images/CrossLarge.h"
#include "util/StringUtils.h"
namespace {
// Perimeter cache file format:
// - 4 bytes: uint32_t file size (for cache invalidation)
// - 1 byte: result (0 = white perimeter, 1 = black perimeter)
constexpr size_t PERIM_CACHE_SIZE = 5;
} // namespace
void SleepActivity::onEnter() {
Activity::onEnter();
renderPopup("Entering Sleep...");
@@ -88,14 +95,14 @@ void SleepActivity::renderCustomSleepScreen() const {
}
APP_STATE.lastSleepImage = randomFileIndex;
APP_STATE.saveToFile();
const auto filename = "/sleep/" + files[randomFileIndex];
const auto bmpPath = "/sleep/" + files[randomFileIndex];
FsFile file;
if (SdMan.openFileForRead("SLP", filename, file)) {
if (SdMan.openFileForRead("SLP", bmpPath, file)) {
Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str());
delay(100);
Bitmap bitmap(file, true);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
renderBitmapSleepScreen(bitmap);
renderBitmapSleepScreen(bitmap, bmpPath);
dir.close();
return;
}
@@ -107,11 +114,12 @@ void SleepActivity::renderCustomSleepScreen() const {
// Look for sleep.bmp on the root of the sd card to determine if we should
// render a custom sleep screen instead of the default.
FsFile file;
if (SdMan.openFileForRead("SLP", "/sleep.bmp", file)) {
const std::string rootSleepPath = "/sleep.bmp";
if (SdMan.openFileForRead("SLP", rootSleepPath, file)) {
Bitmap bitmap(file, true);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis());
renderBitmapSleepScreen(bitmap);
renderBitmapSleepScreen(bitmap, rootSleepPath);
return;
}
}
@@ -136,13 +144,15 @@ void SleepActivity::renderDefaultSleepScreen() const {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
}
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& bmpPath) const {
int x, y;
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
float cropX = 0, cropY = 0;
int drawWidth = pageWidth;
int drawHeight = pageHeight;
int fillWidth = pageWidth; // Actual area the image will occupy
int fillHeight = pageHeight;
Serial.printf("[%lu] [SLP] bitmap %d x %d, screen %d x %d\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
pageWidth, pageHeight);
@@ -160,6 +170,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
// Don't constrain to screen dimensions - drawBitmap will clip
drawWidth = 0;
drawHeight = 0;
fillWidth = bitmap.getWidth();
fillHeight = bitmap.getHeight();
Serial.printf("[%lu] [SLP] ACTUAL mode: centering at %d, %d\n", millis(), x, y);
} else if (coverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
// CROP mode: Scale to fill screen completely (may crop edges)
@@ -176,6 +188,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
// After cropping, the image should fill the screen exactly
x = 0;
y = 0;
fillWidth = pageWidth;
fillHeight = pageHeight;
Serial.printf("[%lu] [SLP] CROP mode: drawing at 0, 0 with crop %f, %f\n", millis(), cropX, cropY);
} else {
// FIT mode (default): Scale to fit entire image within screen (may have letterboxing)
@@ -188,18 +202,28 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
// Image is taller than screen ratio - fit to height
scale = static_cast<float>(pageHeight) / static_cast<float>(bitmap.getHeight());
}
const int scaledWidth = static_cast<int>(bitmap.getWidth() * scale);
const int scaledHeight = static_cast<int>(bitmap.getHeight() * scale);
fillWidth = static_cast<int>(bitmap.getWidth() * scale);
fillHeight = static_cast<int>(bitmap.getHeight() * scale);
// Center the scaled image
x = (pageWidth - scaledWidth) / 2;
y = (pageHeight - scaledHeight) / 2;
x = (pageWidth - fillWidth) / 2;
y = (pageHeight - fillHeight) / 2;
Serial.printf("[%lu] [SLP] FIT mode: scale %f, scaled size %d x %d, position %d, %d\n", millis(), scale,
scaledWidth, scaledHeight, x, y);
fillWidth, fillHeight, x, y);
}
// Detect perimeter color and clear to matching background
const bool isBlackPerimeter = getPerimeterIsBlack(bitmap, bmpPath);
const uint8_t clearColor = isBlackPerimeter ? 0x00 : 0xFF;
Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y);
renderer.clearScreen();
renderer.clearScreen(clearColor);
// If background is black, fill the image area with white first so white pixels render correctly
if (isBlackPerimeter) {
renderer.fillRect(x, y, fillWidth, fillHeight, false); // false = white
}
renderer.drawBitmap(bitmap, x, y, drawWidth, drawHeight, cropX, cropY);
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
@@ -281,7 +305,7 @@ void SleepActivity::renderCoverSleepScreen() const {
if (SdMan.openFileForRead("SLP", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
renderBitmapSleepScreen(bitmap);
renderBitmapSleepScreen(bitmap, coverBmpPath);
return;
}
}
@@ -293,3 +317,80 @@ void SleepActivity::renderBlankSleepScreen() const {
renderer.clearScreen();
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
}
std::string SleepActivity::getPerimeterCachePath(const std::string& bmpPath) {
// Convert "/dir/file.bmp" to "/dir/.file.bmp.perim"
const size_t lastSlash = bmpPath.find_last_of('/');
if (lastSlash == std::string::npos) {
// No directory, just prepend dot
return "." + bmpPath + ".perim";
}
const std::string dir = bmpPath.substr(0, lastSlash + 1);
const std::string filename = bmpPath.substr(lastSlash + 1);
return dir + "." + filename + ".perim";
}
bool SleepActivity::getPerimeterIsBlack(const Bitmap& bitmap, const std::string& bmpPath) const {
const std::string cachePath = getPerimeterCachePath(bmpPath);
// Try to read from cache
FsFile cacheFile;
if (SdMan.openFileForRead("SLP", cachePath, cacheFile)) {
uint8_t cacheData[PERIM_CACHE_SIZE];
if (cacheFile.read(cacheData, PERIM_CACHE_SIZE) == PERIM_CACHE_SIZE) {
// Extract cached file size
const uint32_t cachedSize = static_cast<uint32_t>(cacheData[0]) |
(static_cast<uint32_t>(cacheData[1]) << 8) |
(static_cast<uint32_t>(cacheData[2]) << 16) |
(static_cast<uint32_t>(cacheData[3]) << 24);
// Get current BMP file size
FsFile bmpFile;
uint32_t currentSize = 0;
if (SdMan.openFileForRead("SLP", bmpPath, bmpFile)) {
currentSize = bmpFile.size();
bmpFile.close();
}
// Validate cache
if (cachedSize == currentSize && currentSize > 0) {
const bool result = cacheData[4] != 0;
Serial.printf("[%lu] [SLP] Perimeter cache hit for %s: %s\n", millis(), bmpPath.c_str(),
result ? "black" : "white");
cacheFile.close();
return result;
}
Serial.printf("[%lu] [SLP] Perimeter cache invalid (size mismatch: %lu vs %lu)\n", millis(),
static_cast<unsigned long>(cachedSize), static_cast<unsigned long>(currentSize));
}
cacheFile.close();
}
// Cache miss - calculate perimeter
Serial.printf("[%lu] [SLP] Calculating perimeter for %s\n", millis(), bmpPath.c_str());
const bool isBlack = bitmap.detectPerimeterIsBlack();
Serial.printf("[%lu] [SLP] Perimeter detected: %s\n", millis(), isBlack ? "black" : "white");
// Get BMP file size for cache
FsFile bmpFile;
uint32_t fileSize = 0;
if (SdMan.openFileForRead("SLP", bmpPath, bmpFile)) {
fileSize = bmpFile.size();
bmpFile.close();
}
// Save to cache
if (fileSize > 0 && SdMan.openFileForWrite("SLP", cachePath, cacheFile)) {
uint8_t cacheData[PERIM_CACHE_SIZE];
cacheData[0] = fileSize & 0xFF;
cacheData[1] = (fileSize >> 8) & 0xFF;
cacheData[2] = (fileSize >> 16) & 0xFF;
cacheData[3] = (fileSize >> 24) & 0xFF;
cacheData[4] = isBlack ? 1 : 0;
cacheFile.write(cacheData, PERIM_CACHE_SIZE);
cacheFile.close();
Serial.printf("[%lu] [SLP] Saved perimeter cache to %s\n", millis(), cachePath.c_str());
}
return isBlack;
}

View File

@@ -1,6 +1,8 @@
#pragma once
#include "../Activity.h"
#include <string>
class Bitmap;
class SleepActivity final : public Activity {
@@ -14,6 +16,10 @@ class SleepActivity final : public Activity {
void renderDefaultSleepScreen() const;
void renderCustomSleepScreen() const;
void renderCoverSleepScreen() const;
void renderBitmapSleepScreen(const Bitmap& bitmap) const;
void renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& bmpPath) const;
void renderBlankSleepScreen() const;
// Perimeter detection caching helpers
static std::string getPerimeterCachePath(const std::string& bmpPath);
bool getPerimeterIsBlack(const Bitmap& bitmap, const std::string& bmpPath) const;
};