diff --git a/lib/GfxRenderer/Bitmap.cpp b/lib/GfxRenderer/Bitmap.cpp index 8cc8a5f..1a3b440 100644 --- a/lib/GfxRenderer/Bitmap.cpp +++ b/lib/GfxRenderer/Bitmap.cpp @@ -8,119 +8,15 @@ // ============================================================================ // Note: For cover images, dithering is done in JpegToBmpConverter.cpp // This file handles BMP reading - use simple quantization to avoid double-dithering -constexpr bool USE_FLOYD_STEINBERG = false; // Disabled - dithering done at JPEG conversion -constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering -// Brightness adjustments: -constexpr bool USE_BRIGHTNESS = false; // true: apply brightness/gamma adjustments -constexpr int BRIGHTNESS_BOOST = 20; // Brightness offset (0-50), only if USE_BRIGHTNESS=true -constexpr bool GAMMA_CORRECTION = false; // Gamma curve, only if USE_BRIGHTNESS=true +constexpr bool USE_ATKINSON = true; // Use Atkinson dithering instead of Floyd-Steinberg // ============================================================================ -// Integer approximation of gamma correction (brightens midtones) -static inline int applyGamma(int gray) { - if (!GAMMA_CORRECTION) return gray; - const int product = gray * 255; - int x = gray; - if (x > 0) { - x = (x + product / x) >> 1; - x = (x + product / x) >> 1; - } - return x > 255 ? 255 : x; -} - -// Simple quantization without dithering - just divide into 4 levels -static inline uint8_t quantizeSimple(int gray) { - if (USE_BRIGHTNESS) { - gray += BRIGHTNESS_BOOST; - if (gray > 255) gray = 255; - gray = applyGamma(gray); - } - return static_cast(gray >> 6); -} - -// Hash-based noise dithering - survives downsampling without moiré artifacts -static inline uint8_t quantizeNoise(int gray, int x, int y) { - if (USE_BRIGHTNESS) { - gray += BRIGHTNESS_BOOST; - if (gray > 255) gray = 255; - gray = applyGamma(gray); - } - - uint32_t hash = static_cast(x) * 374761393u + static_cast(y) * 668265263u; - hash = (hash ^ (hash >> 13)) * 1274126177u; - const int threshold = static_cast(hash >> 24); - - 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; - } -} - -// Main quantization function -static inline uint8_t quantize(int gray, int x, int y) { - if (USE_NOISE_DITHERING) { - return quantizeNoise(gray, x, y); - } else { - return quantizeSimple(gray); - } -} - -// Floyd-Steinberg quantization with error diffusion and serpentine scanning -// Returns 2-bit value (0-3) and updates error buffers -static inline uint8_t quantizeFloydSteinberg(int gray, int x, int width, int16_t* errorCurRow, int16_t* errorNextRow, - bool reverseDir) { - // Add accumulated error to this pixel - int adjusted = gray + errorCurRow[x + 1]; - - // Clamp to valid range - if (adjusted < 0) adjusted = 0; - if (adjusted > 255) adjusted = 255; - - // Quantize to 4 levels (0, 85, 170, 255) - uint8_t quantized; - int quantizedValue; - if (adjusted < 43) { - quantized = 0; - quantizedValue = 0; - } else if (adjusted < 128) { - quantized = 1; - quantizedValue = 85; - } else if (adjusted < 213) { - quantized = 2; - quantizedValue = 170; - } else { - quantized = 3; - quantizedValue = 255; - } - - // Calculate error - int error = adjusted - quantizedValue; - - // Distribute error to neighbors (serpentine: direction-aware) - if (!reverseDir) { - // Left to right - errorCurRow[x + 2] += (error * 7) >> 4; // Right: 7/16 - errorNextRow[x] += (error * 3) >> 4; // Bottom-left: 3/16 - errorNextRow[x + 1] += (error * 5) >> 4; // Bottom: 5/16 - errorNextRow[x + 2] += (error) >> 4; // Bottom-right: 1/16 - } else { - // Right to left (mirrored) - errorCurRow[x] += (error * 7) >> 4; // Left: 7/16 - errorNextRow[x + 2] += (error * 3) >> 4; // Bottom-right: 3/16 - errorNextRow[x + 1] += (error * 5) >> 4; // Bottom: 5/16 - errorNextRow[x] += (error) >> 4; // Bottom-left: 1/16 - } - - return quantized; -} - Bitmap::~Bitmap() { delete[] errorCurRow; delete[] errorNextRow; + + delete atkinsonDitherer; + delete fsDitherer; } uint16_t Bitmap::readLE16(FsFile& f) { @@ -244,13 +140,14 @@ BmpReaderError Bitmap::parseHeaders() { return BmpReaderError::SeekPixelDataFailed; } - // Allocate Floyd-Steinberg error buffers if enabled - if (USE_FLOYD_STEINBERG) { - delete[] errorCurRow; - delete[] errorNextRow; - errorCurRow = new int16_t[width + 2](); // +2 for boundary handling - errorNextRow = new int16_t[width + 2](); - prevRowY = -1; + // Create ditherer if enabled (only for 2-bit output) + // Use OUTPUT dimensions for dithering (after prescaling) + if (bpp > 2 && dithering) { + if (USE_ATKINSON) { + atkinsonDitherer = new AtkinsonDitherer(width); + } else { + fsDitherer = new FloydSteinbergDitherer(width); + } } return BmpReaderError::Ok; @@ -261,17 +158,6 @@ 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) { - if (prevRowY != -1) { - // Sequential access - swap buffers - int16_t* temp = errorCurRow; - errorCurRow = errorNextRow; - errorNextRow = temp; - memset(errorNextRow, 0, (width + 2) * sizeof(int16_t)); - } - } prevRowY += 1; uint8_t* outPtr = data; @@ -282,12 +168,18 @@ BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const { // Helper lambda to pack 2bpp color into the output stream auto packPixel = [&](const uint8_t lum) { uint8_t color; - if (useFS) { - // Floyd-Steinberg error diffusion - color = quantizeFloydSteinberg(lum, currentX, width, errorCurRow, errorNextRow, false); + if (atkinsonDitherer) { + color = atkinsonDitherer->processPixel(adjustPixel(lum), currentX); + } else if (fsDitherer) { + color = fsDitherer->processPixel(adjustPixel(lum), currentX); } else { - // Simple quantization or noise dithering - color = quantize(lum, currentX, prevRowY); + if (bpp > 2) { + // Simple quantization or noise dithering + color = quantize(adjustPixel(lum), currentX, prevRowY); + } else { + // do not quantize 2bpp image + color = static_cast(lum >> 6); + } } currentOutByte |= (color << bitShift); if (bitShift == 0) { @@ -345,6 +237,11 @@ BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const { return BmpReaderError::UnsupportedBpp; } + if (atkinsonDitherer) + atkinsonDitherer->nextRow(); + else if (fsDitherer) + fsDitherer->nextRow(); + // Flush remaining bits if width is not a multiple of 4 if (bitShift != 6) *outPtr = currentOutByte; @@ -356,12 +253,9 @@ BmpReaderError Bitmap::rewindToData() const { return BmpReaderError::SeekPixelDataFailed; } - // Reset Floyd-Steinberg error buffers when rewinding - if (USE_FLOYD_STEINBERG && errorCurRow && errorNextRow) { - memset(errorCurRow, 0, (width + 2) * sizeof(int16_t)); - memset(errorNextRow, 0, (width + 2) * sizeof(int16_t)); - prevRowY = -1; - } + // Reset dithering when rewinding + if (fsDitherer) fsDitherer->reset(); + if (atkinsonDitherer) atkinsonDitherer->reset(); return BmpReaderError::Ok; } diff --git a/lib/GfxRenderer/Bitmap.h b/lib/GfxRenderer/Bitmap.h index a3f2e00..9ac7cfb 100644 --- a/lib/GfxRenderer/Bitmap.h +++ b/lib/GfxRenderer/Bitmap.h @@ -2,6 +2,10 @@ #include +#include + +#include "BitmapHelpers.h" + enum class BmpReaderError : uint8_t { Ok = 0, FileInvalid, @@ -28,7 +32,7 @@ class Bitmap { public: static const char* errorToString(BmpReaderError err); - explicit Bitmap(FsFile& file) : file(file) {} + explicit Bitmap(FsFile& file, bool dithering = false) : file(file), dithering(dithering) {} ~Bitmap(); BmpReaderError parseHeaders(); BmpReaderError readNextRow(uint8_t* data, uint8_t* rowBuffer) const; @@ -44,6 +48,7 @@ class Bitmap { static uint32_t readLE32(FsFile& f); FsFile& file; + bool dithering = false; int width = 0; int height = 0; bool topDown = false; @@ -56,4 +61,7 @@ class Bitmap { 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; }; diff --git a/lib/GfxRenderer/BitmapHelpers.cpp b/lib/GfxRenderer/BitmapHelpers.cpp new file mode 100644 index 0000000..b0d9dc0 --- /dev/null +++ b/lib/GfxRenderer/BitmapHelpers.cpp @@ -0,0 +1,90 @@ +#include "BitmapHelpers.h" + +#include + +// Brightness/Contrast adjustments: +constexpr bool USE_BRIGHTNESS = false; // true: apply brightness/gamma adjustments +constexpr int BRIGHTNESS_BOOST = 10; // Brightness offset (0-50) +constexpr bool GAMMA_CORRECTION = false; // Gamma curve (brightens midtones) +constexpr float CONTRAST_FACTOR = 1.15f; // Contrast multiplier (1.0 = no change, >1 = more contrast) +constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering + +// Integer approximation of gamma correction (brightens midtones) +// Uses a simple curve: out = 255 * sqrt(in/255) ≈ sqrt(in * 255) +static inline int applyGamma(int gray) { + if (!GAMMA_CORRECTION) return gray; + // Fast integer square root approximation for gamma ~0.5 (brightening) + // This brightens dark/mid tones while preserving highlights + const int product = gray * 255; + // Newton-Raphson integer sqrt (2 iterations for good accuracy) + int x = gray; + if (x > 0) { + x = (x + product / x) >> 1; + x = (x + product / x) >> 1; + } + return x > 255 ? 255 : x; +} + +// Apply contrast adjustment around midpoint (128) +// factor > 1.0 increases contrast, < 1.0 decreases +static inline int applyContrast(int gray) { + // Integer-based contrast: (gray - 128) * factor + 128 + // Using fixed-point: factor 1.15 ≈ 115/100 + constexpr int factorNum = static_cast(CONTRAST_FACTOR * 100); + int adjusted = ((gray - 128) * factorNum) / 100 + 128; + if (adjusted < 0) adjusted = 0; + if (adjusted > 255) adjusted = 255; + return adjusted; +} +// Combined brightness/contrast/gamma adjustment +int adjustPixel(int gray) { + if (!USE_BRIGHTNESS) return gray; + + // Order: contrast first, then brightness, then gamma + gray = applyContrast(gray); + gray += BRIGHTNESS_BOOST; + if (gray > 255) gray = 255; + if (gray < 0) gray = 0; + gray = applyGamma(gray); + + return gray; +} +// Simple quantization without dithering - divide into 4 levels +// The thresholds are fine-tuned to the X4 display +uint8_t quantizeSimple(int gray) { + if (gray < 45) { + return 0; + } else if (gray < 70) { + return 1; + } else if (gray < 140) { + return 2; + } else { + return 3; + } +} + +// Hash-based noise dithering - survives downsampling without moiré artifacts +// Uses integer hash to generate pseudo-random threshold per pixel +static inline uint8_t quantizeNoise(int gray, int x, int y) { + uint32_t hash = static_cast(x) * 374761393u + static_cast(y) * 668265263u; + hash = (hash ^ (hash >> 13)) * 1274126177u; + const int threshold = static_cast(hash >> 24); + + 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; + } +} + +// Main quantization function - selects between methods based on config +uint8_t quantize(int gray, int x, int y) { + if (USE_NOISE_DITHERING) { + return quantizeNoise(gray, x, y); + } else { + return quantizeSimple(gray); + } +} diff --git a/lib/GfxRenderer/BitmapHelpers.h b/lib/GfxRenderer/BitmapHelpers.h new file mode 100644 index 0000000..300527e --- /dev/null +++ b/lib/GfxRenderer/BitmapHelpers.h @@ -0,0 +1,233 @@ +#pragma once + +#include + +// Helper functions +uint8_t quantize(int gray, int x, int y); +uint8_t quantizeSimple(int gray); +int adjustPixel(int gray); + +// Atkinson dithering - distributes only 6/8 (75%) of error for cleaner results +// Error distribution pattern: +// X 1/8 1/8 +// 1/8 1/8 1/8 +// 1/8 +// Less error buildup = fewer artifacts than Floyd-Steinberg +class AtkinsonDitherer { + public: + explicit AtkinsonDitherer(int width) : width(width) { + errorRow0 = new int16_t[width + 4](); // Current row + errorRow1 = new int16_t[width + 4](); // Next row + errorRow2 = new int16_t[width + 4](); // Row after next + } + + ~AtkinsonDitherer() { + delete[] errorRow0; + delete[] errorRow1; + delete[] errorRow2; + } + // **1. EXPLICITLY DELETE THE COPY CONSTRUCTOR** + AtkinsonDitherer(const AtkinsonDitherer& other) = delete; + + // **2. EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR** + AtkinsonDitherer& operator=(const AtkinsonDitherer& other) = delete; + + uint8_t processPixel(int gray, int x) { + // Add accumulated error + int adjusted = gray + errorRow0[x + 2]; + if (adjusted < 0) adjusted = 0; + if (adjusted > 255) adjusted = 255; + + // Quantize to 4 levels + uint8_t quantized; + int quantizedValue; + if (false) { // original thresholds + if (adjusted < 43) { + quantized = 0; + quantizedValue = 0; + } else if (adjusted < 128) { + quantized = 1; + quantizedValue = 85; + } else if (adjusted < 213) { + quantized = 2; + quantizedValue = 170; + } else { + quantized = 3; + quantizedValue = 255; + } + } else { // fine-tuned to X4 eink display + if (adjusted < 30) { + quantized = 0; + quantizedValue = 15; + } else if (adjusted < 50) { + quantized = 1; + quantizedValue = 30; + } else if (adjusted < 140) { + quantized = 2; + quantizedValue = 80; + } else { + quantized = 3; + quantizedValue = 210; + } + } + + // Calculate error (only distribute 6/8 = 75%) + int error = (adjusted - quantizedValue) >> 3; // error/8 + + // Distribute 1/8 to each of 6 neighbors + errorRow0[x + 3] += error; // Right + errorRow0[x + 4] += error; // Right+1 + errorRow1[x + 1] += error; // Bottom-left + errorRow1[x + 2] += error; // Bottom + errorRow1[x + 3] += error; // Bottom-right + errorRow2[x + 2] += error; // Two rows down + + return quantized; + } + + void nextRow() { + int16_t* temp = errorRow0; + errorRow0 = errorRow1; + errorRow1 = errorRow2; + errorRow2 = temp; + memset(errorRow2, 0, (width + 4) * sizeof(int16_t)); + } + + void reset() { + memset(errorRow0, 0, (width + 4) * sizeof(int16_t)); + memset(errorRow1, 0, (width + 4) * sizeof(int16_t)); + memset(errorRow2, 0, (width + 4) * sizeof(int16_t)); + } + + private: + int width; + int16_t* errorRow0; + int16_t* errorRow1; + int16_t* errorRow2; +}; + +// Floyd-Steinberg error diffusion dithering with serpentine scanning +// Serpentine scanning alternates direction each row to reduce "worm" artifacts +// Error distribution pattern (left-to-right): +// X 7/16 +// 3/16 5/16 1/16 +// Error distribution pattern (right-to-left, mirrored): +// 1/16 5/16 3/16 +// 7/16 X +class FloydSteinbergDitherer { + public: + explicit FloydSteinbergDitherer(int width) : width(width), rowCount(0) { + errorCurRow = new int16_t[width + 2](); // +2 for boundary handling + errorNextRow = new int16_t[width + 2](); + } + + ~FloydSteinbergDitherer() { + delete[] errorCurRow; + delete[] errorNextRow; + } + + // **1. EXPLICITLY DELETE THE COPY CONSTRUCTOR** + FloydSteinbergDitherer(const FloydSteinbergDitherer& other) = delete; + + // **2. EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR** + FloydSteinbergDitherer& operator=(const FloydSteinbergDitherer& other) = delete; + + // Process a single pixel and return quantized 2-bit value + // x is the logical x position (0 to width-1), direction handled internally + uint8_t processPixel(int gray, int x) { + // Add accumulated error to this pixel + int adjusted = gray + errorCurRow[x + 1]; + + // Clamp to valid range + if (adjusted < 0) adjusted = 0; + if (adjusted > 255) adjusted = 255; + + // Quantize to 4 levels (0, 85, 170, 255) + uint8_t quantized; + int quantizedValue; + if (false) { // original thresholds + if (adjusted < 43) { + quantized = 0; + quantizedValue = 0; + } else if (adjusted < 128) { + quantized = 1; + quantizedValue = 85; + } else if (adjusted < 213) { + quantized = 2; + quantizedValue = 170; + } else { + quantized = 3; + quantizedValue = 255; + } + } else { // fine-tuned to X4 eink display + if (adjusted < 30) { + quantized = 0; + quantizedValue = 15; + } else if (adjusted < 50) { + quantized = 1; + quantizedValue = 30; + } else if (adjusted < 140) { + quantized = 2; + quantizedValue = 80; + } else { + quantized = 3; + quantizedValue = 210; + } + } + + // Calculate error + int error = adjusted - quantizedValue; + + // Distribute error to neighbors (serpentine: direction-aware) + if (!isReverseRow()) { + // Left to right: standard distribution + // Right: 7/16 + errorCurRow[x + 2] += (error * 7) >> 4; + // Bottom-left: 3/16 + errorNextRow[x] += (error * 3) >> 4; + // Bottom: 5/16 + errorNextRow[x + 1] += (error * 5) >> 4; + // Bottom-right: 1/16 + errorNextRow[x + 2] += (error) >> 4; + } else { + // Right to left: mirrored distribution + // Left: 7/16 + errorCurRow[x] += (error * 7) >> 4; + // Bottom-right: 3/16 + errorNextRow[x + 2] += (error * 3) >> 4; + // Bottom: 5/16 + errorNextRow[x + 1] += (error * 5) >> 4; + // Bottom-left: 1/16 + errorNextRow[x] += (error) >> 4; + } + + return quantized; + } + + // Call at the end of each row to swap buffers + void nextRow() { + // Swap buffers + int16_t* temp = errorCurRow; + errorCurRow = errorNextRow; + errorNextRow = temp; + // Clear the next row buffer + memset(errorNextRow, 0, (width + 2) * sizeof(int16_t)); + rowCount++; + } + + // Check if current row should be processed in reverse + bool isReverseRow() const { return (rowCount & 1) != 0; } + + // Reset for a new image or MCU block + void reset() { + memset(errorCurRow, 0, (width + 2) * sizeof(int16_t)); + memset(errorNextRow, 0, (width + 2) * sizeof(int16_t)); + rowCount = 0; + } + + private: + int width; + int rowCount; + int16_t* errorCurRow; + int16_t* errorNextRow; +}; diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 8c8db88..30c1314 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -7,6 +7,8 @@ #include #include +#include "BitmapHelpers.h" + // Context structure for picojpeg callback struct JpegReadContext { FsFile& file; @@ -23,282 +25,12 @@ constexpr bool USE_8BIT_OUTPUT = false; // true: 8-bit grayscale (no quantizati constexpr bool USE_ATKINSON = true; // Atkinson dithering (cleaner than F-S, less error diffusion) constexpr bool USE_FLOYD_STEINBERG = false; // Floyd-Steinberg error diffusion (can cause "worm" artifacts) constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering (good for downsampling) -// Brightness/Contrast adjustments: -constexpr bool USE_BRIGHTNESS = true; // true: apply brightness/gamma adjustments -constexpr int BRIGHTNESS_BOOST = 10; // Brightness offset (0-50) -constexpr bool GAMMA_CORRECTION = true; // Gamma curve (brightens midtones) -constexpr float CONTRAST_FACTOR = 1.15f; // Contrast multiplier (1.0 = no change, >1 = more contrast) // Pre-resize to target display size (CRITICAL: avoids dithering artifacts from post-downsampling) constexpr bool USE_PRESCALE = true; // true: scale image to target size before dithering constexpr int TARGET_MAX_WIDTH = 480; // Max width for cover images (portrait display width) constexpr int TARGET_MAX_HEIGHT = 800; // Max height for cover images (portrait display height) // ============================================================================ -// Integer approximation of gamma correction (brightens midtones) -// Uses a simple curve: out = 255 * sqrt(in/255) ≈ sqrt(in * 255) -static inline int applyGamma(int gray) { - if (!GAMMA_CORRECTION) return gray; - // Fast integer square root approximation for gamma ~0.5 (brightening) - // This brightens dark/mid tones while preserving highlights - const int product = gray * 255; - // Newton-Raphson integer sqrt (2 iterations for good accuracy) - int x = gray; - if (x > 0) { - x = (x + product / x) >> 1; - x = (x + product / x) >> 1; - } - return x > 255 ? 255 : x; -} - -// Apply contrast adjustment around midpoint (128) -// factor > 1.0 increases contrast, < 1.0 decreases -static inline int applyContrast(int gray) { - // Integer-based contrast: (gray - 128) * factor + 128 - // Using fixed-point: factor 1.15 ≈ 115/100 - constexpr int factorNum = static_cast(CONTRAST_FACTOR * 100); - int adjusted = ((gray - 128) * factorNum) / 100 + 128; - if (adjusted < 0) adjusted = 0; - if (adjusted > 255) adjusted = 255; - return adjusted; -} - -// Combined brightness/contrast/gamma adjustment -static inline int adjustPixel(int gray) { - if (!USE_BRIGHTNESS) return gray; - - // Order: contrast first, then brightness, then gamma - gray = applyContrast(gray); - gray += BRIGHTNESS_BOOST; - if (gray > 255) gray = 255; - if (gray < 0) gray = 0; - gray = applyGamma(gray); - - return gray; -} - -// Simple quantization without dithering - just divide into 4 levels -static inline uint8_t quantizeSimple(int gray) { - gray = adjustPixel(gray); - // Simple 2-bit quantization: 0-63=0, 64-127=1, 128-191=2, 192-255=3 - return static_cast(gray >> 6); -} - -// Hash-based noise dithering - survives downsampling without moiré artifacts -// Uses integer hash to generate pseudo-random threshold per pixel -static inline uint8_t quantizeNoise(int gray, int x, int y) { - gray = adjustPixel(gray); - - // Generate noise threshold using integer hash (no regular pattern to alias) - uint32_t hash = static_cast(x) * 374761393u + static_cast(y) * 668265263u; - hash = (hash ^ (hash >> 13)) * 1274126177u; - const int threshold = static_cast(hash >> 24); // 0-255 - - // Map gray (0-255) to 4 levels with dithering - 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; - } -} - -// Main quantization function - selects between methods based on config -static inline uint8_t quantize(int gray, int x, int y) { - if (USE_NOISE_DITHERING) { - return quantizeNoise(gray, x, y); - } else { - return quantizeSimple(gray); - } -} - -// Atkinson dithering - distributes only 6/8 (75%) of error for cleaner results -// Error distribution pattern: -// X 1/8 1/8 -// 1/8 1/8 1/8 -// 1/8 -// Less error buildup = fewer artifacts than Floyd-Steinberg -class AtkinsonDitherer { - public: - AtkinsonDitherer(int width) : width(width) { - errorRow0 = new int16_t[width + 4](); // Current row - errorRow1 = new int16_t[width + 4](); // Next row - errorRow2 = new int16_t[width + 4](); // Row after next - } - - ~AtkinsonDitherer() { - delete[] errorRow0; - delete[] errorRow1; - delete[] errorRow2; - } - - uint8_t processPixel(int gray, int x) { - // Apply brightness/contrast/gamma adjustments - gray = adjustPixel(gray); - - // Add accumulated error - int adjusted = gray + errorRow0[x + 2]; - if (adjusted < 0) adjusted = 0; - if (adjusted > 255) adjusted = 255; - - // Quantize to 4 levels - uint8_t quantized; - int quantizedValue; - if (adjusted < 43) { - quantized = 0; - quantizedValue = 0; - } else if (adjusted < 128) { - quantized = 1; - quantizedValue = 85; - } else if (adjusted < 213) { - quantized = 2; - quantizedValue = 170; - } else { - quantized = 3; - quantizedValue = 255; - } - - // Calculate error (only distribute 6/8 = 75%) - int error = (adjusted - quantizedValue) >> 3; // error/8 - - // Distribute 1/8 to each of 6 neighbors - errorRow0[x + 3] += error; // Right - errorRow0[x + 4] += error; // Right+1 - errorRow1[x + 1] += error; // Bottom-left - errorRow1[x + 2] += error; // Bottom - errorRow1[x + 3] += error; // Bottom-right - errorRow2[x + 2] += error; // Two rows down - - return quantized; - } - - void nextRow() { - int16_t* temp = errorRow0; - errorRow0 = errorRow1; - errorRow1 = errorRow2; - errorRow2 = temp; - memset(errorRow2, 0, (width + 4) * sizeof(int16_t)); - } - - void reset() { - memset(errorRow0, 0, (width + 4) * sizeof(int16_t)); - memset(errorRow1, 0, (width + 4) * sizeof(int16_t)); - memset(errorRow2, 0, (width + 4) * sizeof(int16_t)); - } - - private: - int width; - int16_t* errorRow0; - int16_t* errorRow1; - int16_t* errorRow2; -}; - -// Floyd-Steinberg error diffusion dithering with serpentine scanning -// Serpentine scanning alternates direction each row to reduce "worm" artifacts -// Error distribution pattern (left-to-right): -// X 7/16 -// 3/16 5/16 1/16 -// Error distribution pattern (right-to-left, mirrored): -// 1/16 5/16 3/16 -// 7/16 X -class FloydSteinbergDitherer { - public: - FloydSteinbergDitherer(int width) : width(width), rowCount(0) { - errorCurRow = new int16_t[width + 2](); // +2 for boundary handling - errorNextRow = new int16_t[width + 2](); - } - - ~FloydSteinbergDitherer() { - delete[] errorCurRow; - delete[] errorNextRow; - } - - // Process a single pixel and return quantized 2-bit value - // x is the logical x position (0 to width-1), direction handled internally - uint8_t processPixel(int gray, int x, bool reverseDirection) { - // Add accumulated error to this pixel - int adjusted = gray + errorCurRow[x + 1]; - - // Clamp to valid range - if (adjusted < 0) adjusted = 0; - if (adjusted > 255) adjusted = 255; - - // Quantize to 4 levels (0, 85, 170, 255) - uint8_t quantized; - int quantizedValue; - if (adjusted < 43) { - quantized = 0; - quantizedValue = 0; - } else if (adjusted < 128) { - quantized = 1; - quantizedValue = 85; - } else if (adjusted < 213) { - quantized = 2; - quantizedValue = 170; - } else { - quantized = 3; - quantizedValue = 255; - } - - // Calculate error - int error = adjusted - quantizedValue; - - // Distribute error to neighbors (serpentine: direction-aware) - if (!reverseDirection) { - // Left to right: standard distribution - // Right: 7/16 - errorCurRow[x + 2] += (error * 7) >> 4; - // Bottom-left: 3/16 - errorNextRow[x] += (error * 3) >> 4; - // Bottom: 5/16 - errorNextRow[x + 1] += (error * 5) >> 4; - // Bottom-right: 1/16 - errorNextRow[x + 2] += (error) >> 4; - } else { - // Right to left: mirrored distribution - // Left: 7/16 - errorCurRow[x] += (error * 7) >> 4; - // Bottom-right: 3/16 - errorNextRow[x + 2] += (error * 3) >> 4; - // Bottom: 5/16 - errorNextRow[x + 1] += (error * 5) >> 4; - // Bottom-left: 1/16 - errorNextRow[x] += (error) >> 4; - } - - return quantized; - } - - // Call at the end of each row to swap buffers - void nextRow() { - // Swap buffers - int16_t* temp = errorCurRow; - errorCurRow = errorNextRow; - errorNextRow = temp; - // Clear the next row buffer - memset(errorNextRow, 0, (width + 2) * sizeof(int16_t)); - rowCount++; - } - - // Check if current row should be processed in reverse - bool isReverseRow() const { return (rowCount & 1) != 0; } - - // Reset for a new image or MCU block - void reset() { - memset(errorCurRow, 0, (width + 2) * sizeof(int16_t)); - memset(errorNextRow, 0, (width + 2) * sizeof(int16_t)); - rowCount = 0; - } - - private: - int width; - int rowCount; - int16_t* errorCurRow; - int16_t* errorNextRow; -}; - inline void write16(Print& out, const uint16_t value) { out.write(value & 0xFF); out.write((value >> 8) & 0xFF); @@ -623,12 +355,12 @@ bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) { } } else { for (int x = 0; x < outWidth; x++) { - const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x]; + const uint8_t gray = adjustPixel(mcuRowBuffer[bufferY * imageInfo.m_width + x]); uint8_t twoBit; if (atkinsonDitherer) { twoBit = atkinsonDitherer->processPixel(gray, x); } else if (fsDitherer) { - twoBit = fsDitherer->processPixel(gray, x, fsDitherer->isReverseRow()); + twoBit = fsDitherer->processPixel(gray, x); } else { twoBit = quantize(gray, x, y); } @@ -686,12 +418,12 @@ bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) { } } else { for (int x = 0; x < outWidth; x++) { - const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0; + const uint8_t gray = adjustPixel((rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0); uint8_t twoBit; if (atkinsonDitherer) { twoBit = atkinsonDitherer->processPixel(gray, x); } else if (fsDitherer) { - twoBit = fsDitherer->processPixel(gray, x, fsDitherer->isReverseRow()); + twoBit = fsDitherer->processPixel(gray, x); } else { twoBit = quantize(gray, x, currentOutY); } diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 7c79fcb..3305a16 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -86,7 +86,7 @@ void SleepActivity::renderCustomSleepScreen() const { if (SdMan.openFileForRead("SLP", filename, file)) { Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str()); delay(100); - Bitmap bitmap(file); + Bitmap bitmap(file, true); if (bitmap.parseHeaders() == BmpReaderError::Ok) { renderBitmapSleepScreen(bitmap); dir.close(); @@ -101,7 +101,7 @@ void SleepActivity::renderCustomSleepScreen() const { // render a custom sleep screen instead of the default. FsFile file; if (SdMan.openFileForRead("SLP", "/sleep.bmp", file)) { - Bitmap bitmap(file); + Bitmap bitmap(file, true); if (bitmap.parseHeaders() == BmpReaderError::Ok) { Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis()); renderBitmapSleepScreen(bitmap);