|
|
|
|
@@ -1,6 +1,5 @@
|
|
|
|
|
#include "SleepActivity.h"
|
|
|
|
|
|
|
|
|
|
#include <BitmapHelpers.h>
|
|
|
|
|
#include <Epub.h>
|
|
|
|
|
#include <GfxRenderer.h>
|
|
|
|
|
#include <HalStorage.h>
|
|
|
|
|
@@ -9,6 +8,7 @@
|
|
|
|
|
#include <Xtc.h>
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
|
|
|
|
|
#include "CrossPointSettings.h"
|
|
|
|
|
#include "CrossPointState.h"
|
|
|
|
|
@@ -19,36 +19,62 @@
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
// Number of source pixels along the image edge to average for the gradient color
|
|
|
|
|
// Number of source pixels along the image edge to average for the dominant color
|
|
|
|
|
constexpr int EDGE_SAMPLE_DEPTH = 20;
|
|
|
|
|
|
|
|
|
|
// Map a 2-bit quantized pixel value to an 8-bit grayscale value
|
|
|
|
|
constexpr uint8_t val2bitToGray(uint8_t val2bit) { return val2bit * 85; }
|
|
|
|
|
|
|
|
|
|
// Edge gradient data produced by sampleBitmapEdges and consumed by drawLetterboxGradients.
|
|
|
|
|
// edgeA is the "first" edge (top or left), edgeB is the "second" edge (bottom or right).
|
|
|
|
|
struct LetterboxGradientData {
|
|
|
|
|
uint8_t* edgeA = nullptr;
|
|
|
|
|
uint8_t* edgeB = nullptr;
|
|
|
|
|
int edgeCount = 0;
|
|
|
|
|
// Letterbox fill data: one average gray value per edge (top/bottom or left/right).
|
|
|
|
|
struct LetterboxFillData {
|
|
|
|
|
uint8_t avgA = 128; // average gray of edge A (top or left)
|
|
|
|
|
uint8_t avgB = 128; // average gray of edge B (bottom or right)
|
|
|
|
|
int letterboxA = 0; // pixel size of the first letterbox area (top or left)
|
|
|
|
|
int letterboxB = 0; // pixel size of the second letterbox area (bottom or right)
|
|
|
|
|
bool horizontal = false; // true = top/bottom letterbox, false = left/right
|
|
|
|
|
|
|
|
|
|
void free() {
|
|
|
|
|
::free(edgeA);
|
|
|
|
|
::free(edgeB);
|
|
|
|
|
edgeA = nullptr;
|
|
|
|
|
edgeB = nullptr;
|
|
|
|
|
}
|
|
|
|
|
bool valid = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Binary cache version for edge data files
|
|
|
|
|
constexpr uint8_t EDGE_CACHE_VERSION = 1;
|
|
|
|
|
// Snap an 8-bit gray value to the nearest of the 4 e-ink levels: 0, 85, 170, 255.
|
|
|
|
|
uint8_t snapToEinkLevel(uint8_t gray) {
|
|
|
|
|
// Thresholds at midpoints: 42, 127, 212
|
|
|
|
|
if (gray < 43) return 0;
|
|
|
|
|
if (gray < 128) return 85;
|
|
|
|
|
if (gray < 213) return 170;
|
|
|
|
|
return 255;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load cached edge data from a binary file. Returns true if the cache was valid and loaded successfully.
|
|
|
|
|
// Validates cache version and screen dimensions to detect stale data.
|
|
|
|
|
bool loadEdgeCache(const std::string& path, int screenWidth, int screenHeight, LetterboxGradientData& data) {
|
|
|
|
|
// 4x4 Bayer ordered dithering matrix, values 0-255.
|
|
|
|
|
// Produces a structured halftone pattern for 4-level quantization.
|
|
|
|
|
// clang-format off
|
|
|
|
|
constexpr uint8_t BAYER_4X4[4][4] = {
|
|
|
|
|
{ 0, 128, 32, 160},
|
|
|
|
|
{192, 64, 224, 96},
|
|
|
|
|
{ 48, 176, 16, 144},
|
|
|
|
|
{240, 112, 208, 80}
|
|
|
|
|
};
|
|
|
|
|
// clang-format on
|
|
|
|
|
|
|
|
|
|
// Ordered (Bayer) dithering for 4-level e-ink display.
|
|
|
|
|
// Maps an 8-bit gray value to a 2-bit level (0-3) using the Bayer matrix
|
|
|
|
|
// to produce a structured, repeating halftone pattern.
|
|
|
|
|
uint8_t quantizeBayerDither(int gray, int x, int y) {
|
|
|
|
|
const int threshold = BAYER_4X4[y & 3][x & 3];
|
|
|
|
|
const int scaled = gray * 3;
|
|
|
|
|
if (scaled < 255) {
|
|
|
|
|
return (scaled + threshold >= 255) ? 1 : 0;
|
|
|
|
|
} else if (scaled < 510) {
|
|
|
|
|
return ((scaled - 255) + threshold >= 255) ? 2 : 1;
|
|
|
|
|
} else {
|
|
|
|
|
return ((scaled - 510) + threshold >= 255) ? 3 : 2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Edge average cache ---
|
|
|
|
|
// Caches the computed edge averages alongside the cover BMP so we don't rescan on every sleep.
|
|
|
|
|
constexpr uint8_t EDGE_CACHE_VERSION = 2;
|
|
|
|
|
|
|
|
|
|
bool loadEdgeCache(const std::string& path, int screenWidth, int screenHeight, LetterboxFillData& data) {
|
|
|
|
|
FsFile file;
|
|
|
|
|
if (!Storage.openFileForRead("SLP", path, file)) return false;
|
|
|
|
|
|
|
|
|
|
@@ -71,9 +97,8 @@ bool loadEdgeCache(const std::string& path, int screenWidth, int screenHeight, L
|
|
|
|
|
serialization::readPod(file, horizontal);
|
|
|
|
|
data.horizontal = (horizontal != 0);
|
|
|
|
|
|
|
|
|
|
uint16_t edgeCount;
|
|
|
|
|
serialization::readPod(file, edgeCount);
|
|
|
|
|
data.edgeCount = edgeCount;
|
|
|
|
|
serialization::readPod(file, data.avgA);
|
|
|
|
|
serialization::readPod(file, data.avgB);
|
|
|
|
|
|
|
|
|
|
int16_t lbA, lbB;
|
|
|
|
|
serialization::readPod(file, lbA);
|
|
|
|
|
@@ -81,34 +106,15 @@ bool loadEdgeCache(const std::string& path, int screenWidth, int screenHeight, L
|
|
|
|
|
data.letterboxA = lbA;
|
|
|
|
|
data.letterboxB = lbB;
|
|
|
|
|
|
|
|
|
|
if (edgeCount == 0 || edgeCount > 2048) {
|
|
|
|
|
file.close();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data.edgeA = static_cast<uint8_t*>(malloc(edgeCount));
|
|
|
|
|
data.edgeB = static_cast<uint8_t*>(malloc(edgeCount));
|
|
|
|
|
if (!data.edgeA || !data.edgeB) {
|
|
|
|
|
data.free();
|
|
|
|
|
file.close();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (file.read(data.edgeA, edgeCount) != static_cast<int>(edgeCount) ||
|
|
|
|
|
file.read(data.edgeB, edgeCount) != static_cast<int>(edgeCount)) {
|
|
|
|
|
data.free();
|
|
|
|
|
file.close();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file.close();
|
|
|
|
|
Serial.printf("[%lu] [SLP] Loaded edge cache from %s (%d edges)\n", millis(), path.c_str(), edgeCount);
|
|
|
|
|
data.valid = true;
|
|
|
|
|
Serial.printf("[%lu] [SLP] Loaded edge cache from %s (avgA=%d, avgB=%d)\n", millis(), path.c_str(), data.avgA,
|
|
|
|
|
data.avgB);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save edge data to a binary cache file for reuse on subsequent sleep screens.
|
|
|
|
|
bool saveEdgeCache(const std::string& path, int screenWidth, int screenHeight, const LetterboxGradientData& data) {
|
|
|
|
|
if (!data.edgeA || !data.edgeB || data.edgeCount <= 0) return false;
|
|
|
|
|
bool saveEdgeCache(const std::string& path, int screenWidth, int screenHeight, const LetterboxFillData& data) {
|
|
|
|
|
if (!data.valid) return false;
|
|
|
|
|
|
|
|
|
|
FsFile file;
|
|
|
|
|
if (!Storage.openFileForWrite("SLP", path, file)) return false;
|
|
|
|
|
@@ -117,23 +123,22 @@ bool saveEdgeCache(const std::string& path, int screenWidth, int screenHeight, c
|
|
|
|
|
serialization::writePod(file, static_cast<uint16_t>(screenWidth));
|
|
|
|
|
serialization::writePod(file, static_cast<uint16_t>(screenHeight));
|
|
|
|
|
serialization::writePod(file, static_cast<uint8_t>(data.horizontal ? 1 : 0));
|
|
|
|
|
serialization::writePod(file, static_cast<uint16_t>(data.edgeCount));
|
|
|
|
|
serialization::writePod(file, data.avgA);
|
|
|
|
|
serialization::writePod(file, data.avgB);
|
|
|
|
|
serialization::writePod(file, static_cast<int16_t>(data.letterboxA));
|
|
|
|
|
serialization::writePod(file, static_cast<int16_t>(data.letterboxB));
|
|
|
|
|
file.write(data.edgeA, data.edgeCount);
|
|
|
|
|
file.write(data.edgeB, data.edgeCount);
|
|
|
|
|
file.close();
|
|
|
|
|
|
|
|
|
|
Serial.printf("[%lu] [SLP] Saved edge cache to %s (%d edges)\n", millis(), path.c_str(), data.edgeCount);
|
|
|
|
|
Serial.printf("[%lu] [SLP] Saved edge cache to %s\n", millis(), path.c_str());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read the bitmap once to sample the first/last EDGE_SAMPLE_DEPTH rows or columns.
|
|
|
|
|
// Returns edge color arrays in source pixel resolution. Caller must call data.free() when done.
|
|
|
|
|
// Read the bitmap once to compute a single average gray value for the top/bottom or left/right edges.
|
|
|
|
|
// Only computes running sums -- no per-pixel arrays, no malloc beyond row buffers.
|
|
|
|
|
// After sampling the bitmap is rewound via rewindToData().
|
|
|
|
|
LetterboxGradientData sampleBitmapEdges(const Bitmap& bitmap, int imgX, int imgY, int pageWidth, int pageHeight,
|
|
|
|
|
LetterboxFillData computeEdgeAverages(const Bitmap& bitmap, int imgX, int imgY, int pageWidth, int pageHeight,
|
|
|
|
|
float scale, float cropX, float cropY) {
|
|
|
|
|
LetterboxGradientData data;
|
|
|
|
|
LetterboxFillData data;
|
|
|
|
|
|
|
|
|
|
const int cropPixX = static_cast<int>(std::floor(bitmap.getWidth() * cropX / 2.0f));
|
|
|
|
|
const int cropPixY = static_cast<int>(std::floor(bitmap.getHeight() * cropY / 2.0f));
|
|
|
|
|
@@ -146,35 +151,22 @@ LetterboxGradientData sampleBitmapEdges(const Bitmap& bitmap, int imgX, int imgY
|
|
|
|
|
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
|
|
|
|
|
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
|
|
|
|
if (!outputRow || !rowBytes) {
|
|
|
|
|
::free(outputRow);
|
|
|
|
|
::free(rowBytes);
|
|
|
|
|
free(outputRow);
|
|
|
|
|
free(rowBytes);
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (imgY > 0) {
|
|
|
|
|
// Top/bottom letterboxing -- sample per-column averages of first/last N rows
|
|
|
|
|
// Top/bottom letterboxing -- compute overall average of first/last EDGE_SAMPLE_DEPTH rows
|
|
|
|
|
data.horizontal = true;
|
|
|
|
|
data.edgeCount = visibleWidth;
|
|
|
|
|
const int scaledHeight = static_cast<int>(std::round(static_cast<float>(visibleHeight) * scale));
|
|
|
|
|
data.letterboxA = imgY;
|
|
|
|
|
data.letterboxB = pageHeight - imgY - scaledHeight;
|
|
|
|
|
if (data.letterboxB < 0) data.letterboxB = 0;
|
|
|
|
|
|
|
|
|
|
const int sampleRows = std::min(EDGE_SAMPLE_DEPTH, visibleHeight);
|
|
|
|
|
|
|
|
|
|
auto* accumTop = static_cast<uint32_t*>(calloc(visibleWidth, sizeof(uint32_t)));
|
|
|
|
|
auto* accumBot = static_cast<uint32_t*>(calloc(visibleWidth, sizeof(uint32_t)));
|
|
|
|
|
data.edgeA = static_cast<uint8_t*>(malloc(visibleWidth));
|
|
|
|
|
data.edgeB = static_cast<uint8_t*>(malloc(visibleWidth));
|
|
|
|
|
|
|
|
|
|
if (!accumTop || !accumBot || !data.edgeA || !data.edgeB) {
|
|
|
|
|
::free(accumTop);
|
|
|
|
|
::free(accumBot);
|
|
|
|
|
data.free();
|
|
|
|
|
::free(outputRow);
|
|
|
|
|
::free(rowBytes);
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
uint64_t sumTop = 0, sumBot = 0;
|
|
|
|
|
int countTop = 0, countBot = 0;
|
|
|
|
|
|
|
|
|
|
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
|
|
|
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) break;
|
|
|
|
|
@@ -187,188 +179,106 @@ LetterboxGradientData sampleBitmapEdges(const Bitmap& bitmap, int imgX, int imgY
|
|
|
|
|
if (!inTop && !inBot) continue;
|
|
|
|
|
|
|
|
|
|
for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
|
|
|
|
const int outX = bmpX - cropPixX;
|
|
|
|
|
const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3;
|
|
|
|
|
const uint8_t gray = val2bitToGray(val);
|
|
|
|
|
if (inTop) accumTop[outX] += gray;
|
|
|
|
|
if (inBot) accumBot[outX] += gray;
|
|
|
|
|
if (inTop) {
|
|
|
|
|
sumTop += gray;
|
|
|
|
|
countTop++;
|
|
|
|
|
}
|
|
|
|
|
if (inBot) {
|
|
|
|
|
sumBot += gray;
|
|
|
|
|
countBot++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < visibleWidth; i++) {
|
|
|
|
|
data.edgeA[i] = static_cast<uint8_t>(accumTop[i] / sampleRows);
|
|
|
|
|
data.edgeB[i] = static_cast<uint8_t>(accumBot[i] / sampleRows);
|
|
|
|
|
}
|
|
|
|
|
::free(accumTop);
|
|
|
|
|
::free(accumBot);
|
|
|
|
|
data.avgA = countTop > 0 ? static_cast<uint8_t>(sumTop / countTop) : 128;
|
|
|
|
|
data.avgB = countBot > 0 ? static_cast<uint8_t>(sumBot / countBot) : 128;
|
|
|
|
|
data.valid = true;
|
|
|
|
|
|
|
|
|
|
} else if (imgX > 0) {
|
|
|
|
|
// Left/right letterboxing -- sample per-row averages of first/last N columns
|
|
|
|
|
// Left/right letterboxing -- compute overall average of first/last EDGE_SAMPLE_DEPTH columns
|
|
|
|
|
data.horizontal = false;
|
|
|
|
|
data.edgeCount = visibleHeight;
|
|
|
|
|
const int scaledWidth = static_cast<int>(std::round(static_cast<float>(visibleWidth) * scale));
|
|
|
|
|
data.letterboxA = imgX;
|
|
|
|
|
data.letterboxB = pageWidth - imgX - scaledWidth;
|
|
|
|
|
if (data.letterboxB < 0) data.letterboxB = 0;
|
|
|
|
|
|
|
|
|
|
const int sampleCols = std::min(EDGE_SAMPLE_DEPTH, visibleWidth);
|
|
|
|
|
|
|
|
|
|
auto* accumLeft = static_cast<uint32_t*>(calloc(visibleHeight, sizeof(uint32_t)));
|
|
|
|
|
auto* accumRight = static_cast<uint32_t*>(calloc(visibleHeight, sizeof(uint32_t)));
|
|
|
|
|
data.edgeA = static_cast<uint8_t*>(malloc(visibleHeight));
|
|
|
|
|
data.edgeB = static_cast<uint8_t*>(malloc(visibleHeight));
|
|
|
|
|
|
|
|
|
|
if (!accumLeft || !accumRight || !data.edgeA || !data.edgeB) {
|
|
|
|
|
::free(accumLeft);
|
|
|
|
|
::free(accumRight);
|
|
|
|
|
data.free();
|
|
|
|
|
::free(outputRow);
|
|
|
|
|
::free(rowBytes);
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
uint64_t sumLeft = 0, sumRight = 0;
|
|
|
|
|
int countLeft = 0, countRight = 0;
|
|
|
|
|
|
|
|
|
|
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
|
|
|
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) break;
|
|
|
|
|
const int logicalY = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
|
|
|
|
|
if (logicalY < cropPixY || logicalY >= bitmap.getHeight() - cropPixY) continue;
|
|
|
|
|
const int outY = logicalY - cropPixY;
|
|
|
|
|
|
|
|
|
|
// Sample left edge columns
|
|
|
|
|
for (int bmpX = cropPixX; bmpX < cropPixX + sampleCols; bmpX++) {
|
|
|
|
|
const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3;
|
|
|
|
|
accumLeft[outY] += val2bitToGray(val);
|
|
|
|
|
sumLeft += val2bitToGray(val);
|
|
|
|
|
countLeft++;
|
|
|
|
|
}
|
|
|
|
|
// Sample right edge columns
|
|
|
|
|
for (int bmpX = bitmap.getWidth() - cropPixX - sampleCols; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
|
|
|
|
const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3;
|
|
|
|
|
accumRight[outY] += val2bitToGray(val);
|
|
|
|
|
sumRight += val2bitToGray(val);
|
|
|
|
|
countRight++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < visibleHeight; i++) {
|
|
|
|
|
data.edgeA[i] = static_cast<uint8_t>(accumLeft[i] / sampleCols);
|
|
|
|
|
data.edgeB[i] = static_cast<uint8_t>(accumRight[i] / sampleCols);
|
|
|
|
|
}
|
|
|
|
|
::free(accumLeft);
|
|
|
|
|
::free(accumRight);
|
|
|
|
|
data.avgA = countLeft > 0 ? static_cast<uint8_t>(sumLeft / countLeft) : 128;
|
|
|
|
|
data.avgB = countRight > 0 ? static_cast<uint8_t>(sumRight / countRight) : 128;
|
|
|
|
|
data.valid = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::free(outputRow);
|
|
|
|
|
::free(rowBytes);
|
|
|
|
|
free(outputRow);
|
|
|
|
|
free(rowBytes);
|
|
|
|
|
bitmap.rewindToData();
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw dithered fills in the letterbox areas using the sampled edge colors.
|
|
|
|
|
// fillMode selects the fill algorithm: SOLID (single dominant shade), BLENDED (per-pixel edge color),
|
|
|
|
|
// or GRADIENT (per-pixel edge color interpolated toward targetColor).
|
|
|
|
|
// targetColor is the color the gradient fades toward (255=white, 0=black); only used in GRADIENT mode.
|
|
|
|
|
// Draw letterbox fill in the areas around the cover image.
|
|
|
|
|
// DITHERED: fills with the edge average using Bayer ordered dithering to approximate the color.
|
|
|
|
|
// SOLID: snaps edge average to nearest e-ink level (0/85/170/255) for a clean uniform fill.
|
|
|
|
|
// Must be called once per render pass (BW, GRAYSCALE_LSB, GRAYSCALE_MSB).
|
|
|
|
|
void drawLetterboxFill(GfxRenderer& renderer, const LetterboxGradientData& data, float scale, uint8_t fillMode,
|
|
|
|
|
int targetColor) {
|
|
|
|
|
if (!data.edgeA || !data.edgeB || data.edgeCount <= 0) return;
|
|
|
|
|
void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uint8_t fillMode) {
|
|
|
|
|
if (!data.valid) return;
|
|
|
|
|
|
|
|
|
|
const bool isSolid = (fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_SOLID);
|
|
|
|
|
const bool isGradient = (fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_GRADIENT);
|
|
|
|
|
|
|
|
|
|
// For SOLID mode, compute the dominant (average) shade for each edge once
|
|
|
|
|
uint8_t solidColorA = 0, solidColorB = 0;
|
|
|
|
|
if (isSolid) {
|
|
|
|
|
uint32_t sumA = 0, sumB = 0;
|
|
|
|
|
for (int i = 0; i < data.edgeCount; i++) {
|
|
|
|
|
sumA += data.edgeA[i];
|
|
|
|
|
sumB += data.edgeB[i];
|
|
|
|
|
}
|
|
|
|
|
solidColorA = static_cast<uint8_t>(sumA / data.edgeCount);
|
|
|
|
|
solidColorB = static_cast<uint8_t>(sumB / data.edgeCount);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper: compute gray value for a pixel given the edge color and interpolation factor t (0..1)
|
|
|
|
|
// GRADIENT interpolates from edgeColor toward targetColor; SOLID and BLENDED return edgeColor directly.
|
|
|
|
|
auto computeGray = [&](int edgeColor, float t) -> int {
|
|
|
|
|
if (isGradient) return edgeColor + static_cast<int>(static_cast<float>(targetColor - edgeColor) * t);
|
|
|
|
|
return edgeColor;
|
|
|
|
|
};
|
|
|
|
|
// For SOLID: snap to nearest e-ink level then convert to 2-bit
|
|
|
|
|
// For DITHERED: use the raw average, Bayer dithering produces the 2-bit level per pixel
|
|
|
|
|
const uint8_t colorA = isSolid ? snapToEinkLevel(data.avgA) : data.avgA;
|
|
|
|
|
const uint8_t colorB = isSolid ? snapToEinkLevel(data.avgB) : data.avgB;
|
|
|
|
|
const uint8_t solidA = colorA / 85; // only used for SOLID
|
|
|
|
|
const uint8_t solidB = colorB / 85;
|
|
|
|
|
|
|
|
|
|
if (data.horizontal) {
|
|
|
|
|
// Top letterbox
|
|
|
|
|
if (data.letterboxA > 0) {
|
|
|
|
|
const int imgTopY = data.letterboxA;
|
|
|
|
|
for (int screenY = 0; screenY < imgTopY; screenY++) {
|
|
|
|
|
const float t = static_cast<float>(imgTopY - screenY) / static_cast<float>(imgTopY);
|
|
|
|
|
for (int screenX = 0; screenX < renderer.getScreenWidth(); screenX++) {
|
|
|
|
|
int edgeColor;
|
|
|
|
|
if (isSolid) {
|
|
|
|
|
edgeColor = solidColorA;
|
|
|
|
|
} else {
|
|
|
|
|
int srcCol = static_cast<int>(screenX / scale);
|
|
|
|
|
srcCol = std::max(0, std::min(srcCol, data.edgeCount - 1));
|
|
|
|
|
edgeColor = data.edgeA[srcCol];
|
|
|
|
|
for (int y = 0; y < data.letterboxA; y++)
|
|
|
|
|
for (int x = 0; x < renderer.getScreenWidth(); x++)
|
|
|
|
|
renderer.drawPixelGray(x, y, isSolid ? solidA : quantizeBayerDither(colorA, x, y));
|
|
|
|
|
}
|
|
|
|
|
const int gray = computeGray(edgeColor, t);
|
|
|
|
|
renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bottom letterbox
|
|
|
|
|
if (data.letterboxB > 0) {
|
|
|
|
|
const int imgBottomY = renderer.getScreenHeight() - data.letterboxB;
|
|
|
|
|
for (int screenY = imgBottomY; screenY < renderer.getScreenHeight(); screenY++) {
|
|
|
|
|
const float t = static_cast<float>(screenY - imgBottomY + 1) / static_cast<float>(data.letterboxB);
|
|
|
|
|
for (int screenX = 0; screenX < renderer.getScreenWidth(); screenX++) {
|
|
|
|
|
int edgeColor;
|
|
|
|
|
if (isSolid) {
|
|
|
|
|
edgeColor = solidColorB;
|
|
|
|
|
} else {
|
|
|
|
|
int srcCol = static_cast<int>(screenX / scale);
|
|
|
|
|
srcCol = std::max(0, std::min(srcCol, data.edgeCount - 1));
|
|
|
|
|
edgeColor = data.edgeB[srcCol];
|
|
|
|
|
}
|
|
|
|
|
const int gray = computeGray(edgeColor, t);
|
|
|
|
|
renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const int start = renderer.getScreenHeight() - data.letterboxB;
|
|
|
|
|
for (int y = start; y < renderer.getScreenHeight(); y++)
|
|
|
|
|
for (int x = 0; x < renderer.getScreenWidth(); x++)
|
|
|
|
|
renderer.drawPixelGray(x, y, isSolid ? solidB : quantizeBayerDither(colorB, x, y));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Left letterbox
|
|
|
|
|
if (data.letterboxA > 0) {
|
|
|
|
|
const int imgLeftX = data.letterboxA;
|
|
|
|
|
for (int screenX = 0; screenX < imgLeftX; screenX++) {
|
|
|
|
|
const float t = static_cast<float>(imgLeftX - screenX) / static_cast<float>(imgLeftX);
|
|
|
|
|
for (int screenY = 0; screenY < renderer.getScreenHeight(); screenY++) {
|
|
|
|
|
int edgeColor;
|
|
|
|
|
if (isSolid) {
|
|
|
|
|
edgeColor = solidColorA;
|
|
|
|
|
} else {
|
|
|
|
|
int srcRow = static_cast<int>(screenY / scale);
|
|
|
|
|
srcRow = std::max(0, std::min(srcRow, data.edgeCount - 1));
|
|
|
|
|
edgeColor = data.edgeA[srcRow];
|
|
|
|
|
for (int x = 0; x < data.letterboxA; x++)
|
|
|
|
|
for (int y = 0; y < renderer.getScreenHeight(); y++)
|
|
|
|
|
renderer.drawPixelGray(x, y, isSolid ? solidA : quantizeBayerDither(colorA, x, y));
|
|
|
|
|
}
|
|
|
|
|
const int gray = computeGray(edgeColor, t);
|
|
|
|
|
renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Right letterbox
|
|
|
|
|
if (data.letterboxB > 0) {
|
|
|
|
|
const int imgRightX = renderer.getScreenWidth() - data.letterboxB;
|
|
|
|
|
for (int screenX = imgRightX; screenX < renderer.getScreenWidth(); screenX++) {
|
|
|
|
|
const float t = static_cast<float>(screenX - imgRightX + 1) / static_cast<float>(data.letterboxB);
|
|
|
|
|
for (int screenY = 0; screenY < renderer.getScreenHeight(); screenY++) {
|
|
|
|
|
int edgeColor;
|
|
|
|
|
if (isSolid) {
|
|
|
|
|
edgeColor = solidColorB;
|
|
|
|
|
} else {
|
|
|
|
|
int srcRow = static_cast<int>(screenY / scale);
|
|
|
|
|
srcRow = std::max(0, std::min(srcRow, data.edgeCount - 1));
|
|
|
|
|
edgeColor = data.edgeB[srcRow];
|
|
|
|
|
}
|
|
|
|
|
const int gray = computeGray(edgeColor, t);
|
|
|
|
|
renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const int start = renderer.getScreenWidth() - data.letterboxB;
|
|
|
|
|
for (int x = start; x < renderer.getScreenWidth(); x++)
|
|
|
|
|
for (int y = 0; y < renderer.getScreenHeight(); y++)
|
|
|
|
|
renderer.drawPixelGray(x, y, isSolid ? solidB : quantizeBayerDither(colorB, x, y));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -530,28 +440,31 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
|
|
|
|
|
// Determine letterbox fill settings
|
|
|
|
|
const uint8_t fillMode = SETTINGS.sleepScreenLetterboxFill;
|
|
|
|
|
const bool wantFill = (fillMode != CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_NONE);
|
|
|
|
|
const int targetColor =
|
|
|
|
|
(SETTINGS.sleepScreenGradientDir == CrossPointSettings::SLEEP_SCREEN_GRADIENT_DIR::GRADIENT_TO_BLACK) ? 0 : 255;
|
|
|
|
|
|
|
|
|
|
static const char* fillModeNames[] = {"none", "solid", "blended", "gradient"};
|
|
|
|
|
const char* fillModeName = (fillMode < 4) ? fillModeNames[fillMode] : "unknown";
|
|
|
|
|
static const char* fillModeNames[] = {"dithered", "solid", "none"};
|
|
|
|
|
const char* fillModeName = (fillMode < 3) ? fillModeNames[fillMode] : "unknown";
|
|
|
|
|
|
|
|
|
|
// Load cached edge data or sample from bitmap (first pass over bitmap, then rewind)
|
|
|
|
|
LetterboxGradientData gradientData;
|
|
|
|
|
// Compute edge averages if letterbox fill is requested (try cache first)
|
|
|
|
|
LetterboxFillData fillData;
|
|
|
|
|
const bool hasLetterbox = (x > 0 || y > 0);
|
|
|
|
|
if (hasLetterbox && wantFill) {
|
|
|
|
|
bool cacheLoaded = false;
|
|
|
|
|
if (!edgeCachePath.empty()) {
|
|
|
|
|
cacheLoaded = loadEdgeCache(edgeCachePath, pageWidth, pageHeight, gradientData);
|
|
|
|
|
cacheLoaded = loadEdgeCache(edgeCachePath, pageWidth, pageHeight, fillData);
|
|
|
|
|
}
|
|
|
|
|
if (!cacheLoaded) {
|
|
|
|
|
Serial.printf("[%lu] [SLP] Letterbox detected (x=%d, y=%d), sampling edges for %s fill\n", millis(), x, y,
|
|
|
|
|
fillModeName);
|
|
|
|
|
gradientData = sampleBitmapEdges(bitmap, x, y, pageWidth, pageHeight, scale, cropX, cropY);
|
|
|
|
|
if (!edgeCachePath.empty() && gradientData.edgeA) {
|
|
|
|
|
saveEdgeCache(edgeCachePath, pageWidth, pageHeight, gradientData);
|
|
|
|
|
Serial.printf("[%lu] [SLP] Letterbox detected (x=%d, y=%d), computing edge averages for %s fill\n", millis(), x,
|
|
|
|
|
y, fillModeName);
|
|
|
|
|
fillData = computeEdgeAverages(bitmap, x, y, pageWidth, pageHeight, scale, cropX, cropY);
|
|
|
|
|
if (fillData.valid && !edgeCachePath.empty()) {
|
|
|
|
|
saveEdgeCache(edgeCachePath, pageWidth, pageHeight, fillData);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (fillData.valid) {
|
|
|
|
|
Serial.printf("[%lu] [SLP] Letterbox fill: %s, horizontal=%d, avgA=%d, avgB=%d, letterboxA=%d, letterboxB=%d\n",
|
|
|
|
|
millis(), fillModeName, fillData.horizontal, fillData.avgA, fillData.avgB, fillData.letterboxA,
|
|
|
|
|
fillData.letterboxB);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderer.clearScreen();
|
|
|
|
|
@@ -560,8 +473,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
|
|
|
|
|
SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER;
|
|
|
|
|
|
|
|
|
|
// Draw letterbox fill (BW pass)
|
|
|
|
|
if (gradientData.edgeA) {
|
|
|
|
|
drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor);
|
|
|
|
|
if (fillData.valid) {
|
|
|
|
|
drawLetterboxFill(renderer, fillData, fillMode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
|
|
|
|
@@ -576,8 +489,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
|
|
|
|
|
bitmap.rewindToData();
|
|
|
|
|
renderer.clearScreen(0x00);
|
|
|
|
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
|
|
|
|
if (gradientData.edgeA) {
|
|
|
|
|
drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor);
|
|
|
|
|
if (fillData.valid) {
|
|
|
|
|
drawLetterboxFill(renderer, fillData, fillMode);
|
|
|
|
|
}
|
|
|
|
|
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
|
|
|
|
renderer.copyGrayscaleLsbBuffers();
|
|
|
|
|
@@ -585,8 +498,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
|
|
|
|
|
bitmap.rewindToData();
|
|
|
|
|
renderer.clearScreen(0x00);
|
|
|
|
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
|
|
|
|
if (gradientData.edgeA) {
|
|
|
|
|
drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor);
|
|
|
|
|
if (fillData.valid) {
|
|
|
|
|
drawLetterboxFill(renderer, fillData, fillMode);
|
|
|
|
|
}
|
|
|
|
|
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
|
|
|
|
renderer.copyGrayscaleMsbBuffers();
|
|
|
|
|
@@ -594,8 +507,6 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
|
|
|
|
|
renderer.displayGrayBuffer();
|
|
|
|
|
renderer.setRenderMode(GfxRenderer::BW);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gradientData.free();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SleepActivity::renderCoverSleepScreen() const {
|
|
|
|
|
@@ -665,17 +576,17 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|
|
|
|
return (this->*renderNoCoverSleepScreen)();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Derive edge cache path from cover BMP path (e.g. cover.bmp -> cover_edges.bin)
|
|
|
|
|
std::string edgeCachePath;
|
|
|
|
|
if (coverBmpPath.size() > 4) {
|
|
|
|
|
edgeCachePath = coverBmpPath.substr(0, coverBmpPath.size() - 4) + "_edges.bin";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FsFile file;
|
|
|
|
|
if (Storage.openFileForRead("SLP", coverBmpPath, file)) {
|
|
|
|
|
Bitmap bitmap(file);
|
|
|
|
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
|
|
|
|
Serial.printf("[%lu] [SLP] Rendering sleep cover: %s\n", millis(), coverBmpPath.c_str());
|
|
|
|
|
// Derive edge cache path from cover BMP path (e.g. cover.bmp -> cover_edges.bin)
|
|
|
|
|
std::string edgeCachePath;
|
|
|
|
|
const auto dotPos = coverBmpPath.rfind(".bmp");
|
|
|
|
|
if (dotPos != std::string::npos) {
|
|
|
|
|
edgeCachePath = coverBmpPath.substr(0, dotPos) + "_edges.bin";
|
|
|
|
|
}
|
|
|
|
|
renderBitmapSleepScreen(bitmap, edgeCachePath);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|