## Summary * What is the goal of this PR? - Allow users to create custom sleep screen images with standard tools (ImageMagick, GIMP, etc.) that render cleanly on the e-ink display without dithering artifacts. Previously, avoiding dithering required non-standard 2-bit BMPs that no standard image editor can produce. ( see issue #931 ) * What changes are included? - Add 4-bit BMP format support to Bitmap.cpp (standard format, widely supported by image tools) - Auto-detect "native palette" images: if a BMP has ≤4 palette entries and all luminances map within ±21 of the display's native gray levels (0, 85, 170, 255), skip dithering entirely and direct-map pixels - Clarify pixel processing strategy with three distinct paths: error-diffusion dithering, simple quantization, or direct mapping - Add scripts/generate_test_bmps.py for generating test images across all supported BMP formats ## Additional Context * The e-ink display has 4 native gray levels. When a BMP already uses exactly those levels, dithering adds noise to what should be clean output. The native palette detection uses a ±21 tolerance (~10%) to handle slight rounding from color space conversions in image tools. Users can now create a 4-color grayscale BMP with (imagemagic example): ``` convert input.png -colorspace Gray -colors 4 -depth ``` --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _** YES**_
72 lines
1.7 KiB
C++
72 lines
1.7 KiB
C++
#pragma once
|
|
|
|
#include <HalStorage.h>
|
|
|
|
#include <cstdint>
|
|
|
|
#include "BitmapHelpers.h"
|
|
|
|
enum class BmpReaderError : uint8_t {
|
|
Ok = 0,
|
|
FileInvalid,
|
|
SeekStartFailed,
|
|
|
|
NotBMP,
|
|
DIBTooSmall,
|
|
|
|
BadPlanes,
|
|
UnsupportedBpp,
|
|
UnsupportedCompression,
|
|
|
|
BadDimensions,
|
|
ImageTooLarge,
|
|
PaletteTooLarge,
|
|
|
|
SeekPixelDataFailed,
|
|
BufferTooSmall,
|
|
OomRowBuffer,
|
|
ShortReadRow,
|
|
};
|
|
|
|
class Bitmap {
|
|
public:
|
|
static const char* errorToString(BmpReaderError err);
|
|
|
|
explicit Bitmap(FsFile& file, bool dithering = false) : file(file), dithering(dithering) {}
|
|
~Bitmap();
|
|
BmpReaderError parseHeaders();
|
|
BmpReaderError readNextRow(uint8_t* data, uint8_t* rowBuffer) 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; }
|
|
int getRowBytes() const { return rowBytes; }
|
|
bool is1Bit() const { return bpp == 1; }
|
|
uint16_t getBpp() const { return bpp; }
|
|
|
|
private:
|
|
static uint16_t readLE16(FsFile& f);
|
|
static uint32_t readLE32(FsFile& f);
|
|
|
|
FsFile& file;
|
|
bool dithering = false;
|
|
int width = 0;
|
|
int height = 0;
|
|
bool topDown = false;
|
|
uint32_t bfOffBits = 0;
|
|
uint16_t bpp = 0;
|
|
uint32_t colorsUsed = 0;
|
|
bool nativePalette = false; // true if all palette entries map to native gray levels
|
|
int rowBytes = 0;
|
|
uint8_t paletteLum[256] = {};
|
|
|
|
// Dithering state (mutable for const methods)
|
|
mutable int16_t* errorCurRow = nullptr;
|
|
mutable int16_t* errorNextRow = nullptr;
|
|
mutable int prevRowY = -1; // Track row progression for error propagation
|
|
|
|
mutable AtkinsonDitherer* atkinsonDitherer = nullptr;
|
|
mutable FloydSteinbergDitherer* fsDitherer = nullptr;
|
|
};
|