diff --git a/lib/BmpReader/BmpReader.cpp b/lib/BmpReader/BmpReader.cpp deleted file mode 100644 index f71dbda..0000000 --- a/lib/BmpReader/BmpReader.cpp +++ /dev/null @@ -1,210 +0,0 @@ -#include "BmpReader.h" - -#include -#include - -uint16_t BmpReader::readLE16(File& f) { - const int c0 = f.read(); - const int c1 = f.read(); - const uint8_t b0 = (uint8_t)(c0 < 0 ? 0 : c0); - const uint8_t b1 = (uint8_t)(c1 < 0 ? 0 : c1); - return (uint16_t)b0 | ((uint16_t)b1 << 8); -} - -uint32_t BmpReader::readLE32(File& f) { - const int c0 = f.read(); - const int c1 = f.read(); - const int c2 = f.read(); - const int c3 = f.read(); - - const uint8_t b0 = (uint8_t)(c0 < 0 ? 0 : c0); - const uint8_t b1 = (uint8_t)(c1 < 0 ? 0 : c1); - const uint8_t b2 = (uint8_t)(c2 < 0 ? 0 : c2); - const uint8_t b3 = (uint8_t)(c3 < 0 ? 0 : c3); - - return (uint32_t)b0 | ((uint32_t)b1 << 8) | ((uint32_t)b2 << 16) | ((uint32_t)b3 << 24); -} - -void BmpReader::freeMonoBitmap(MonoBitmap& bmp) { - if (bmp.data) { - free(bmp.data); - bmp.data = nullptr; - } - bmp.width = 0; - bmp.height = 0; - bmp.len = 0; -} - -const char* BmpReader::errorToString(BmpReaderError err) { - switch (err) { - case BmpReaderError::Ok: - return "Ok"; - case BmpReaderError::FileInvalid: - return "FileInvalid"; - case BmpReaderError::SeekStartFailed: - return "SeekStartFailed"; - case BmpReaderError::NotBMP: - return "NotBMP (missing 'BM')"; - case BmpReaderError::DIBTooSmall: - return "DIBTooSmall (<40 bytes)"; - case BmpReaderError::BadPlanes: - return "BadPlanes (!= 1)"; - case BmpReaderError::UnsupportedBpp: - return "UnsupportedBpp (expected 24, 32 or 1)"; - case BmpReaderError::UnsupportedCompression: - return "UnsupportedCompression (expected BI_RGB or BI_BITFIELDS for 32bpp)"; - case BmpReaderError::BadDimensions: - return "BadDimensions"; - case BmpReaderError::SeekPixelDataFailed: - return "SeekPixelDataFailed"; - case BmpReaderError::OomOutput: - return "OomOutput"; - case BmpReaderError::OomRowBuffer: - return "OomRowBuffer"; - case BmpReaderError::ShortReadRow: - return "ShortReadRow"; - } - return "Unknown"; -} - -BmpReaderError BmpReader::read(File& file, MonoBitmap& out, uint8_t threshold) { - freeMonoBitmap(out); - - if (!file) return BmpReaderError::FileInvalid; - if (!file.seek(0)) return BmpReaderError::SeekStartFailed; - - // --- BMP FILE HEADER --- - const uint16_t bfType = readLE16(file); - if (bfType != 0x4D42) return BmpReaderError::NotBMP; - - (void)readLE32(file); - (void)readLE16(file); - (void)readLE16(file); - const uint32_t bfOffBits = readLE32(file); - - // --- DIB HEADER --- - const uint32_t biSize = readLE32(file); - if (biSize < 40) return BmpReaderError::DIBTooSmall; - - const int32_t srcW = (int32_t)readLE32(file); - int32_t srcHRaw = (int32_t)readLE32(file); - const uint16_t planes = readLE16(file); - const uint16_t bpp = readLE16(file); - const uint32_t comp = readLE32(file); - const bool is24Bit = (bpp == 24); - const bool is32Bit = (bpp == 32); - const bool is8Bit = (bpp == 8); - const bool is1Bit = (bpp == 1); - - if (planes != 1) return BmpReaderError::BadPlanes; - if (!is24Bit && !is32Bit && !is8Bit && !is1Bit) return BmpReaderError::UnsupportedBpp; - // Allow BI_RGB (0) for all, and BI_BITFIELDS (3) for 32bpp which is common for BGRA masks. - if (!(comp == 0 || (is32Bit && comp == 3))) return BmpReaderError::UnsupportedCompression; - - (void)readLE32(file); // biSizeImage - (void)readLE32(file); // biXPelsPerMeter - (void)readLE32(file); // biYPelsPerMeter - const uint32_t clrUsed = readLE32(file); - (void)readLE32(file); // biClrImportant - - if (srcW <= 0) return BmpReaderError::BadDimensions; - - const bool topDown = (srcHRaw < 0); - const int32_t srcH = topDown ? -srcHRaw : srcHRaw; - if (srcH <= 0) return BmpReaderError::BadDimensions; - - // Output dimensions after 90° CCW rotation - out.width = (int)srcH; - out.height = (int)srcW; - - const size_t outBytesPerRow = (size_t)(out.width + 7) / 8; - out.len = outBytesPerRow * (size_t)out.height; - - out.data = (uint8_t*)malloc(out.len); - if (!out.data) return BmpReaderError::OomOutput; - memset(out.data, 0xFF, out.len); - - // Palette for 8-bit indexed images - uint8_t paletteLum[256]; - if (is8Bit) { - for (int i = 0; i < 256; i++) paletteLum[i] = (uint8_t)i; // default grayscale ramp - uint32_t paletteCount = (clrUsed == 0) ? 256u : clrUsed; - if (paletteCount > 256u) paletteCount = 256u; - for (uint32_t i = 0; i < paletteCount; i++) { - const int b = file.read(); - const int g = file.read(); - const int r = file.read(); - (void)file.read(); // reserved - const uint8_t bb = (uint8_t)(b < 0 ? 0 : b); - const uint8_t gg = (uint8_t)(g < 0 ? 0 : g); - const uint8_t rr = (uint8_t)(r < 0 ? 0 : r); - paletteLum[i] = (uint8_t)((77u * rr + 150u * gg + 29u * bb) >> 8); - } - } - - // Source row stride (padded to 4 bytes) - uint32_t bytesPerPixel = 0u; - if (is8Bit) { - bytesPerPixel = 1u; - } else if (is32Bit) { - bytesPerPixel = 4u; - } else if (is24Bit) { - bytesPerPixel = 3u; - } - const uint32_t srcBytesPerRow = - is1Bit ? ((uint32_t)srcW + 7u) / 8u : (uint32_t)srcW * bytesPerPixel; // bpp==1 ignores bytesPerPixel - const uint32_t srcRowStride = (srcBytesPerRow + 3u) & ~3u; - - if (!file.seek(bfOffBits)) { - freeMonoBitmap(out); - return BmpReaderError::SeekPixelDataFailed; - } - - uint8_t* rowBuf = (uint8_t*)malloc(srcRowStride); - if (!rowBuf) { - freeMonoBitmap(out); - return BmpReaderError::OomRowBuffer; - } - - for (int fileRow = 0; fileRow < (int)srcH; fileRow++) { - if (file.read(rowBuf, srcRowStride) != (int)srcRowStride) { - free(rowBuf); - freeMonoBitmap(out); - return BmpReaderError::ShortReadRow; - } - - const int srcY = topDown ? fileRow : ((int)srcH - 1 - fileRow); - - for (int srcX = 0; srcX < (int)srcW; srcX++) { - bool isBlack; - if (is1Bit) { - const uint8_t byte = rowBuf[srcX >> 3]; - const uint8_t mask = (uint8_t)(0x80u >> (srcX & 7)); - const bool bitSet = (byte & mask) != 0; - // In 1bpp BMPs, palette index 0 is conventionally black and index 1 is white. - isBlack = !bitSet; - } else if (is8Bit) { - const uint8_t idx = rowBuf[srcX]; - const uint8_t lum = paletteLum[idx]; - isBlack = (lum < threshold); - } else { - const uint8_t* px = &rowBuf[srcX * bytesPerPixel]; - const uint8_t b = px[0]; - const uint8_t g = px[1]; - const uint8_t r = px[2]; - - const uint8_t lum = (uint8_t)((77u * r + 150u * g + 29u * b) >> 8); - isBlack = (lum < threshold); - } - - // 90° counter-clockwise: (x,y) -> (y, w-1-x) - const int outX = srcY; - const int outY = (int)srcW - 1 - srcX; - - setMonoPixel(out.data, out.width, outX, outY, isBlack); - } - } - - free(rowBuf); - return BmpReaderError::Ok; -} diff --git a/lib/BmpReader/BmpReader.h b/lib/BmpReader/BmpReader.h deleted file mode 100644 index 24ec1d3..0000000 --- a/lib/BmpReader/BmpReader.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include -#include - -struct MonoBitmap { - int width = 0; - int height = 0; - size_t len = 0; // bytesPerRow * height - uint8_t* data = nullptr; // row-aligned, MSB-first, 1=white 0=black -}; - -enum class BmpReaderError : uint8_t { - Ok = 0, - FileInvalid, - SeekStartFailed, - - NotBMP, - DIBTooSmall, - - BadPlanes, - UnsupportedBpp, - UnsupportedCompression, - - BadDimensions, - - SeekPixelDataFailed, - OomOutput, - OomRowBuffer, - ShortReadRow, -}; - -class BmpReader { - public: - // Rotate 90° counter-clockwise: (w,h) -> (h,w) - // Used for converting portrait BMP (480x800) into landscape framebuffer (800x480) - // Supports 8-bit, 24-bit, 32-bit color and 1-bit monochrome BMPs. - static BmpReaderError read(File& file, MonoBitmap& out, uint8_t threshold = 160); - - static void freeMonoBitmap(MonoBitmap& bmp); - static const char* errorToString(BmpReaderError err); - - private: - static uint16_t readLE16(File& f); - static uint32_t readLE32(File& f); - - // Writes a single pixel into a row-aligned 1bpp buffer (MSB-first), 0=black, 1=white - static inline void setMonoPixel(uint8_t* buf, int w, int x, int y, bool isBlack) { - const size_t bytesPerRow = (size_t)(w + 7) / 8; - const size_t idx = (size_t)y * bytesPerRow + (size_t)(x >> 3); - const uint8_t mask = (uint8_t)(0x80u >> (x & 7)); - if (isBlack) - buf[idx] &= (uint8_t)~mask; - else - buf[idx] |= mask; - } -}; diff --git a/lib/GfxRenderer/Bitmap.cpp b/lib/GfxRenderer/Bitmap.cpp new file mode 100644 index 0000000..d9f39c7 --- /dev/null +++ b/lib/GfxRenderer/Bitmap.cpp @@ -0,0 +1,200 @@ +#include "Bitmap.h" + +#include +#include + +uint16_t Bitmap::readLE16(File& f) { + const int c0 = f.read(); + const int c1 = f.read(); + const auto b0 = static_cast(c0 < 0 ? 0 : c0); + const auto b1 = static_cast(c1 < 0 ? 0 : c1); + return static_cast(b0) | (static_cast(b1) << 8); +} + +uint32_t Bitmap::readLE32(File& f) { + const int c0 = f.read(); + const int c1 = f.read(); + const int c2 = f.read(); + const int c3 = f.read(); + + const auto b0 = static_cast(c0 < 0 ? 0 : c0); + const auto b1 = static_cast(c1 < 0 ? 0 : c1); + const auto b2 = static_cast(c2 < 0 ? 0 : c2); + const auto b3 = static_cast(c3 < 0 ? 0 : c3); + + return static_cast(b0) | (static_cast(b1) << 8) | (static_cast(b2) << 16) | + (static_cast(b3) << 24); +} + +const char* Bitmap::errorToString(BmpReaderError err) { + switch (err) { + case BmpReaderError::Ok: + return "Ok"; + case BmpReaderError::FileInvalid: + return "FileInvalid"; + case BmpReaderError::SeekStartFailed: + return "SeekStartFailed"; + case BmpReaderError::NotBMP: + return "NotBMP (missing 'BM')"; + case BmpReaderError::DIBTooSmall: + return "DIBTooSmall (<40 bytes)"; + case BmpReaderError::BadPlanes: + return "BadPlanes (!= 1)"; + case BmpReaderError::UnsupportedBpp: + return "UnsupportedBpp (expected 1, 2, 8, 24, or 32)"; + case BmpReaderError::UnsupportedCompression: + return "UnsupportedCompression (expected BI_RGB or BI_BITFIELDS for 32bpp)"; + case BmpReaderError::BadDimensions: + return "BadDimensions"; + case BmpReaderError::PaletteTooLarge: + return "PaletteTooLarge"; + + case BmpReaderError::SeekPixelDataFailed: + return "SeekPixelDataFailed"; + case BmpReaderError::BufferTooSmall: + return "BufferTooSmall"; + + case BmpReaderError::OomRowBuffer: + return "OomRowBuffer"; + case BmpReaderError::ShortReadRow: + return "ShortReadRow"; + } + return "Unknown"; +} + +BmpReaderError Bitmap::parseHeaders() { + if (!file) return BmpReaderError::FileInvalid; + if (!file.seek(0)) return BmpReaderError::SeekStartFailed; + + // --- BMP FILE HEADER --- + const uint16_t bfType = readLE16(file); + if (bfType != 0x4D42) return BmpReaderError::NotBMP; + + file.seek(8, SeekCur); + bfOffBits = readLE32(file); + + // --- DIB HEADER --- + const uint32_t biSize = readLE32(file); + if (biSize < 40) return BmpReaderError::DIBTooSmall; + + width = static_cast(readLE32(file)); + const auto rawHeight = static_cast(readLE32(file)); + topDown = rawHeight < 0; + height = topDown ? -rawHeight : rawHeight; + + const uint16_t planes = readLE16(file); + bpp = readLE16(file); + const uint32_t comp = readLE32(file); + const bool validBpp = bpp == 1 || bpp == 2 || bpp == 8 || bpp == 24 || bpp == 32; + + if (planes != 1) return BmpReaderError::BadPlanes; + if (!validBpp) return BmpReaderError::UnsupportedBpp; + // Allow BI_RGB (0) for all, and BI_BITFIELDS (3) for 32bpp which is common for BGRA masks. + if (!(comp == 0 || (bpp == 32 && comp == 3))) return BmpReaderError::UnsupportedCompression; + + file.seek(12, SeekCur); // biSizeImage, biXPelsPerMeter, biYPelsPerMeter + const uint32_t colorsUsed = readLE32(file); + if (colorsUsed > 256u) return BmpReaderError::PaletteTooLarge; + file.seek(4, SeekCur); // biClrImportant + + if (width <= 0 || height <= 0) return BmpReaderError::BadDimensions; + + // Palette for 8-bit indexed images + for (int i = 0; i < 256; i++) paletteLum[i] = static_cast(i); // default grayscale ramp + if (colorsUsed > 0) { + for (uint32_t i = 0; i < colorsUsed; i++) { + const int b = file.read(); + const int g = file.read(); + const int r = file.read(); + file.seek(1, SeekCur); // reserved + const auto bb = static_cast(b < 0 ? 0 : b); + const auto gg = static_cast(g < 0 ? 0 : g); + const auto rr = static_cast(r < 0 ? 0 : r); + paletteLum[i] = static_cast((77u * rr + 150u * gg + 29u * bb) >> 8); + } + } + + if (!file.seek(bfOffBits)) { + return BmpReaderError::SeekPixelDataFailed; + } + + return BmpReaderError::Ok; +} + +// packed 2bpp output, 0 = black, 1 = dark gray, 2 = light gray, 3 = white +BmpReaderError Bitmap::readRow(uint8_t* data, const size_t dataLen, size_t* read) const { + // Validate data is long enough to hold a row worth of data + const size_t outputBytes = (width + 3) / 4; + if (dataLen < outputBytes) { + *read = 0; + return BmpReaderError::BufferTooSmall; + } + + // setup data to be all black + memset(data, 0x00, outputBytes); + + const size_t rowBytes = (width * bpp + 31) / 32 * 4; + const auto rowBuffer = static_cast(malloc(rowBytes)); + if (!rowBuffer) { + *read = 0; + return BmpReaderError::OomRowBuffer; + } + + if (file.read(rowBuffer, rowBytes) != rowBytes) { + free(rowBuffer); + *read = 0; + return BmpReaderError::ShortReadRow; + } + + for (int bmpX = 0; bmpX < width; bmpX++) { + uint8_t lum; + if (bpp == 1) { + const uint8_t byte = rowBuffer[bmpX / 8]; + const uint8_t bit = 7 - (bmpX % 8); + const bool bitSet = (byte >> bit) & 0x01; + // In 1bpp BMPs, palette index 0 is conventionally black and index 1 is white. + lum = bitSet ? 0xFF : 0x00; + } else if (bpp == 2) { + const uint8_t byte = rowBuffer[bmpX / 4]; + const uint8_t twobit = 6 - ((bmpX * 2) % 8); + const uint8_t val = (byte >> twobit) & 0x3; + lum = paletteLum[val]; + } else if (bpp == 8) { + const uint8_t idx = rowBuffer[bmpX]; + lum = paletteLum[idx]; + } else { + // 24 / 32 + const uint8_t* px = &rowBuffer[bmpX * bpp / 8]; + const uint8_t b = px[0]; + const uint8_t g = px[1]; + const uint8_t r = px[2]; + + lum = static_cast((77u * r + 150u * g + 29u * b) >> 8); + } + + uint8_t color; + if (lum >= 192) { + color = 0x3; // white + } else if (lum >= 128) { + color = 0x2; // light gray + } else if (lum >= 64) { + color = 0x1; // dark gray + } else { + color = 0x0; // black + } + + data[bmpX / 4] |= (color << (6 - ((bmpX % 4) * 2))); + } + + free(rowBuffer); + *read = outputBytes; + return BmpReaderError::Ok; +} + +BmpReaderError Bitmap::rewindToData() const { + if (!file.seek(bfOffBits)) { + return BmpReaderError::SeekPixelDataFailed; + } + + return BmpReaderError::Ok; +} diff --git a/lib/GfxRenderer/Bitmap.h b/lib/GfxRenderer/Bitmap.h new file mode 100644 index 0000000..c6721fa --- /dev/null +++ b/lib/GfxRenderer/Bitmap.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +enum class BmpReaderError : uint8_t { + Ok = 0, + FileInvalid, + SeekStartFailed, + + NotBMP, + DIBTooSmall, + + BadPlanes, + UnsupportedBpp, + UnsupportedCompression, + + BadDimensions, + PaletteTooLarge, + + SeekPixelDataFailed, + BufferTooSmall, + OomRowBuffer, + ShortReadRow, +}; + +class Bitmap { + public: + static const char* errorToString(BmpReaderError err); + + explicit Bitmap(File& file) : file(file) {}; + BmpReaderError parseHeaders(); + BmpReaderError readRow(uint8_t* data, size_t dataLen, size_t* read) const; + BmpReaderError rewindToData() const; + int getWidth() const { return width; } + int getHeight() const { return height; } + bool isTopDown() const { return topDown; } + bool hasGreyscale() const { return bpp > 1; } + + private: + static uint16_t readLE16(File& f); + static uint32_t readLE32(File& f); + + File& file; + int width = 0; + int height = 0; + bool topDown = false; + uint32_t bfOffBits = 0; + uint16_t bpp = 0; + uint8_t paletteLum[256] = {}; +}; diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 4a4ebee..19c8913 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -2,8 +2,6 @@ #include -#include "BmpReader.h" - void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); } void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { @@ -121,35 +119,67 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co einkDisplay.drawImage(bitmap, y, x, height, width); } -bool GfxRenderer::drawFullScreenBmp(File& file) { - if (!file) { - Serial.printf("[%lu] [GFX] drawFullScreenBmp: invalid file\n", millis()); - return false; +void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, + const int maxHeight) const { + float scale = 1.0f; + bool isScaled = false; + if (maxWidth > 0 && bitmap.getWidth() > maxWidth) { + scale = static_cast(maxWidth) / static_cast(bitmap.getWidth()); + isScaled = true; + } + if (maxHeight > 0 && bitmap.getHeight() > maxHeight) { + scale = std::min(scale, static_cast(maxHeight) / static_cast(bitmap.getHeight())); + isScaled = true; } - file.seek(0); // Ensure we're at the start of the file + const uint8_t outputRowSize = (bitmap.getWidth() + 3) / 4; + auto* outputRow = static_cast(malloc(outputRowSize)); - MonoBitmap bmp; - auto err = BmpReader::read(file, bmp); + for (int bmpY = 0; bmpY < bitmap.getHeight(); 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); + if (isScaled) { + screenY = std::floor(screenY * scale); + } + if (screenY >= getScreenHeight()) { + break; + } - if (err != BmpReaderError::Ok) { - Serial.printf("[%lu] [GFX] BMP convert failed: %s\n", millis(), BmpReader::errorToString(err)); - return false; + size_t readBytes; + if (bitmap.readRow(outputRow, outputRowSize, &readBytes) != BmpReaderError::Ok) { + free(outputRow); + return; + } + + if (readBytes != outputRowSize) { + Serial.printf("[%lu] [GFX] Failed to read BMP row data, got: %d, expected: %d\n", millis(), readBytes, + outputRowSize); + break; + } + + for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) { + int screenX = x + bmpX; + if (isScaled) { + screenX = std::floor(screenX * scale); + } + if (screenX >= getScreenWidth()) { + break; + } + + const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3; + + if (renderMode == BW && val < 3) { + drawPixel(screenX, screenY); + } else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) { + drawPixel(screenX, screenY, false); + } else if (renderMode == GRAYSCALE_LSB && val == 1) { + drawPixel(screenX, screenY, false); + } + } } - // Hard requirement: must match panel exactly - if (bmp.width != EInkDisplay::DISPLAY_WIDTH || bmp.height != EInkDisplay::DISPLAY_HEIGHT) { - Serial.printf("[%lu] [GFX] drawFullScreenBmp: rotated BMP size %dx%d does not match panel %dx%d\n", millis(), - bmp.width, bmp.height, EInkDisplay::DISPLAY_WIDTH, EInkDisplay::DISPLAY_HEIGHT); - BmpReader::freeMonoBitmap(bmp); - return false; - } - - // Full-screen blit - einkDisplay.drawImage(bmp.data, 0, 0, bmp.width, bmp.height); - - BmpReader::freeMonoBitmap(bmp); - return true; + free(outputRow); } void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 570941b..838e018 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -6,6 +6,8 @@ #include +#include "Bitmap.h" + class GfxRenderer { public: enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; @@ -46,7 +48,7 @@ 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; - bool drawFullScreenBmp(File& file); + void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const; // Text int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index dea1689..417a662 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -12,14 +12,17 @@ void SleepActivity::onEnter() { // render a custom sleep screen instead of the default. auto file = SD.open("/sleep.bmp"); if (file) { - renderCustomSleepScreen(file); - return; + Bitmap bitmap(file); + if (bitmap.parseHeaders() == BmpReaderError::Ok) { + renderCustomSleepScreen(bitmap); + return; + } } renderDefaultSleepScreen(); } -void SleepActivity::renderDefaultSleepScreen() { +void SleepActivity::renderDefaultSleepScreen() const { const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageHeight = GfxRenderer::getScreenHeight(); @@ -36,14 +39,49 @@ void SleepActivity::renderDefaultSleepScreen() { renderer.displayBuffer(EInkDisplay::HALF_REFRESH); } -void SleepActivity::renderCustomSleepScreen(File& file) { - renderer.clearScreen(); - bool didImageDrawSuccessfully = renderer.drawFullScreenBmp(file); +void SleepActivity::renderCustomSleepScreen(const Bitmap& bitmap) const { + int x, y; + const auto pageWidth = GfxRenderer::getScreenWidth(); + const auto pageHeight = GfxRenderer::getScreenHeight(); - if (!didImageDrawSuccessfully) { - renderer.clearScreen(); - renderer.drawCenteredText(UI_FONT_ID, GfxRenderer::getScreenHeight() / 2, "BAD CUSTOM SLEEP SCREEN", true, BOLD); + if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) { + // image will scale, make sure placement is right + const float ratio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); + const float screenRatio = static_cast(pageWidth) / static_cast(pageHeight); + + if (ratio > screenRatio) { + // image wider than viewport ratio, scaled down image needs to be centered vertically + x = 0; + y = (pageHeight - pageWidth / ratio) / 2; + } else { + // image taller than viewport ratio, scaled down image needs to be centered horizontally + x = (pageWidth - pageHeight * ratio) / 2; + y = 0; + } + } else { + // center the image + x = (pageWidth - bitmap.getWidth()) / 2; + y = (pageHeight - bitmap.getHeight()) / 2; } + renderer.clearScreen(); + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight); renderer.displayBuffer(EInkDisplay::HALF_REFRESH); -} \ No newline at end of file + + if (bitmap.hasGreyscale()) { + bitmap.rewindToData(); + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight); + renderer.copyGrayscaleLsbBuffers(); + + bitmap.rewindToData(); + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight); + renderer.copyGrayscaleMsbBuffers(); + + renderer.displayGrayBuffer(); + renderer.setRenderMode(GfxRenderer::BW); + } +} diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 4665dd6..9d4a7c4 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -1,6 +1,7 @@ #pragma once #include "../Activity.h" -#include "SD.h" + +class Bitmap; class SleepActivity final : public Activity { public: @@ -8,6 +9,6 @@ class SleepActivity final : public Activity { void onEnter() override; private: - void renderDefaultSleepScreen(); - void renderCustomSleepScreen(File& file); + void renderDefaultSleepScreen() const; + void renderCustomSleepScreen(const Bitmap& bitmap) const; };