Feature/cover crop mode (#225)
Added a setting to select `fit` or `crop` for cover image on sleep screen. Might add a `expand` feature in the future that does not crop but rather fills the blank space with a mirror of the image. --------- Co-authored-by: Dave Allie <dave@daveallie.com>
This commit is contained in:
@@ -250,34 +250,29 @@ BmpReaderError Bitmap::parseHeaders() {
|
||||
delete[] errorNextRow;
|
||||
errorCurRow = new int16_t[width + 2](); // +2 for boundary handling
|
||||
errorNextRow = new int16_t[width + 2]();
|
||||
lastRowY = -1;
|
||||
prevRowY = -1;
|
||||
}
|
||||
|
||||
return BmpReaderError::Ok;
|
||||
}
|
||||
|
||||
// packed 2bpp output, 0 = black, 1 = dark gray, 2 = light gray, 3 = white
|
||||
BmpReaderError Bitmap::readRow(uint8_t* data, uint8_t* rowBuffer, int rowY) const {
|
||||
BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const {
|
||||
// Note: rowBuffer should be pre-allocated by the caller to size 'rowBytes'
|
||||
if (file.read(rowBuffer, rowBytes) != rowBytes) return BmpReaderError::ShortReadRow;
|
||||
|
||||
// Handle Floyd-Steinberg error buffer progression
|
||||
const bool useFS = USE_FLOYD_STEINBERG && errorCurRow && errorNextRow;
|
||||
if (useFS) {
|
||||
// Check if we need to advance to next row (or reset if jumping)
|
||||
if (rowY != lastRowY + 1 && rowY != 0) {
|
||||
// Non-sequential row access - reset error buffers
|
||||
memset(errorCurRow, 0, (width + 2) * sizeof(int16_t));
|
||||
memset(errorNextRow, 0, (width + 2) * sizeof(int16_t));
|
||||
} else if (rowY > 0) {
|
||||
if (prevRowY != -1) {
|
||||
// Sequential access - swap buffers
|
||||
int16_t* temp = errorCurRow;
|
||||
errorCurRow = errorNextRow;
|
||||
errorNextRow = temp;
|
||||
memset(errorNextRow, 0, (width + 2) * sizeof(int16_t));
|
||||
}
|
||||
lastRowY = rowY;
|
||||
}
|
||||
prevRowY += 1;
|
||||
|
||||
uint8_t* outPtr = data;
|
||||
uint8_t currentOutByte = 0;
|
||||
@@ -292,7 +287,7 @@ BmpReaderError Bitmap::readRow(uint8_t* data, uint8_t* rowBuffer, int rowY) cons
|
||||
color = quantizeFloydSteinberg(lum, currentX, width, errorCurRow, errorNextRow, false);
|
||||
} else {
|
||||
// Simple quantization or noise dithering
|
||||
color = quantize(lum, currentX, rowY);
|
||||
color = quantize(lum, currentX, prevRowY);
|
||||
}
|
||||
currentOutByte |= (color << bitShift);
|
||||
if (bitShift == 0) {
|
||||
@@ -365,7 +360,7 @@ BmpReaderError Bitmap::rewindToData() const {
|
||||
if (USE_FLOYD_STEINBERG && errorCurRow && errorNextRow) {
|
||||
memset(errorCurRow, 0, (width + 2) * sizeof(int16_t));
|
||||
memset(errorNextRow, 0, (width + 2) * sizeof(int16_t));
|
||||
lastRowY = -1;
|
||||
prevRowY = -1;
|
||||
}
|
||||
|
||||
return BmpReaderError::Ok;
|
||||
|
||||
@@ -31,7 +31,7 @@ class Bitmap {
|
||||
explicit Bitmap(FsFile& file) : file(file) {}
|
||||
~Bitmap();
|
||||
BmpReaderError parseHeaders();
|
||||
BmpReaderError readRow(uint8_t* data, uint8_t* rowBuffer, int rowY) const;
|
||||
BmpReaderError readNextRow(uint8_t* data, uint8_t* rowBuffer) const;
|
||||
BmpReaderError rewindToData() const;
|
||||
int getWidth() const { return width; }
|
||||
int getHeight() const { return height; }
|
||||
@@ -55,5 +55,5 @@ class Bitmap {
|
||||
// Floyd-Steinberg dithering state (mutable for const methods)
|
||||
mutable int16_t* errorCurRow = nullptr;
|
||||
mutable int16_t* errorNextRow = nullptr;
|
||||
mutable int lastRowY = -1; // Track row progression for error propagation
|
||||
mutable int prevRowY = -1; // Track row progression for error propagation
|
||||
};
|
||||
|
||||
@@ -152,18 +152,24 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
|
||||
einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth,
|
||||
const int maxHeight) const {
|
||||
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
|
||||
const float cropX, const float cropY) const {
|
||||
float scale = 1.0f;
|
||||
bool isScaled = false;
|
||||
if (maxWidth > 0 && bitmap.getWidth() > maxWidth) {
|
||||
scale = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
||||
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
|
||||
int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f);
|
||||
Serial.printf("[%lu] [GFX] Cropping %dx%d by %dx%d pix, is %s\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
|
||||
cropPixX, cropPixY, bitmap.isTopDown() ? "top-down" : "bottom-up");
|
||||
|
||||
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
||||
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
||||
isScaled = true;
|
||||
}
|
||||
if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
|
||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight()));
|
||||
if (maxHeight > 0 && (1.0f - cropY) * bitmap.getHeight() > maxHeight) {
|
||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
|
||||
isScaled = true;
|
||||
}
|
||||
Serial.printf("[%lu] [GFX] Scaling by %f - %s\n", millis(), scale, isScaled ? "scaled" : "not scaled");
|
||||
|
||||
// Calculate output row size (2 bits per pixel, packed into bytes)
|
||||
// IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide
|
||||
@@ -178,29 +184,36 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
return;
|
||||
}
|
||||
|
||||
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
||||
for (int bmpY = 0; bmpY < (bitmap.getHeight() - cropPixY); bmpY++) {
|
||||
// The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative).
|
||||
// Screen's (0, 0) is the top-left corner.
|
||||
int screenY = y + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY);
|
||||
int screenY = -cropPixY + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY);
|
||||
if (isScaled) {
|
||||
screenY = std::floor(screenY * scale);
|
||||
}
|
||||
screenY += y; // the offset should not be scaled
|
||||
if (screenY >= getScreenHeight()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (bitmap.readRow(outputRow, rowBytes, bmpY) != BmpReaderError::Ok) {
|
||||
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
||||
Serial.printf("[%lu] [GFX] Failed to read row %d from bitmap\n", millis(), bmpY);
|
||||
free(outputRow);
|
||||
free(rowBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) {
|
||||
int screenX = x + bmpX;
|
||||
if (bmpY < cropPixY) {
|
||||
// Skip the row if it's outside the crop area
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
||||
int screenX = bmpX - cropPixX;
|
||||
if (isScaled) {
|
||||
screenX = std::floor(screenX * scale);
|
||||
}
|
||||
screenX += x; // the offset should not be scaled
|
||||
if (screenX >= getScreenWidth()) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,8 @@ class GfxRenderer {
|
||||
void drawRect(int x, int y, int width, int height, bool state = true) const;
|
||||
void fillRect(int x, int y, int width, int height, bool state = true) const;
|
||||
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
||||
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
||||
float cropY = 0) const;
|
||||
|
||||
// Text
|
||||
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
|
||||
Reference in New Issue
Block a user