Move BmpReader implementation to GfxRenderer/Bitmap and avoid internal buffer
This commit is contained in:
parent
c33ace673f
commit
f72d2c372c
@ -1,210 +0,0 @@
|
||||
#include "BmpReader.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
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;
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <FS.h>
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
200
lib/GfxRenderer/Bitmap.cpp
Normal file
200
lib/GfxRenderer/Bitmap.cpp
Normal file
@ -0,0 +1,200 @@
|
||||
#include "Bitmap.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
uint16_t Bitmap::readLE16(File& f) {
|
||||
const int c0 = f.read();
|
||||
const int c1 = f.read();
|
||||
const auto b0 = static_cast<uint8_t>(c0 < 0 ? 0 : c0);
|
||||
const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1);
|
||||
return static_cast<uint16_t>(b0) | (static_cast<uint16_t>(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<uint8_t>(c0 < 0 ? 0 : c0);
|
||||
const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1);
|
||||
const auto b2 = static_cast<uint8_t>(c2 < 0 ? 0 : c2);
|
||||
const auto b3 = static_cast<uint8_t>(c3 < 0 ? 0 : c3);
|
||||
|
||||
return static_cast<uint32_t>(b0) | (static_cast<uint32_t>(b1) << 8) | (static_cast<uint32_t>(b2) << 16) |
|
||||
(static_cast<uint32_t>(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<int32_t>(readLE32(file));
|
||||
const auto rawHeight = static_cast<int32_t>(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<uint8_t>(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<uint8_t>(b < 0 ? 0 : b);
|
||||
const auto gg = static_cast<uint8_t>(g < 0 ? 0 : g);
|
||||
const auto rr = static_cast<uint8_t>(r < 0 ? 0 : r);
|
||||
paletteLum[i] = static_cast<uint8_t>((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<uint8_t*>(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<uint8_t>((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;
|
||||
}
|
||||
50
lib/GfxRenderer/Bitmap.h
Normal file
50
lib/GfxRenderer/Bitmap.h
Normal file
@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <FS.h>
|
||||
|
||||
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] = {};
|
||||
};
|
||||
@ -2,8 +2,6 @@
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
#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<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
||||
isScaled = true;
|
||||
}
|
||||
if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
|
||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>(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<uint8_t*>(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); }
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
|
||||
#include <map>
|
||||
|
||||
#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;
|
||||
|
||||
@ -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<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
||||
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user