diff --git a/lib/GfxRenderer/Bitmap.cpp b/lib/GfxRenderer/Bitmap.cpp index dac17892..e8db53da 100644 --- a/lib/GfxRenderer/Bitmap.cpp +++ b/lib/GfxRenderer/Bitmap.cpp @@ -8,15 +8,22 @@ // ============================================================================ // 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 = true; // Disabled - dithering done at JPEG conversion constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering -constexpr bool GAMMA_CORRECTION = true; // Gamma curve, only if USE_BRIGHTNESS=true +// 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) // ============================================================================ // 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; @@ -25,6 +32,7 @@ static inline int applyGamma(int gray) { return x > 255 ? 255 : x; } +// TODO: remove static inline uint8_t boostBrightness(int gray, uint8_t boost) { if (boost > 0) { gray += boost; @@ -33,10 +41,34 @@ static inline uint8_t boostBrightness(int gray, uint8_t boost) { } return gray; } -// Simple quantization without dithering - just divide into 4 levels -static inline uint8_t quantizeSimple(int gray) { - // return static_cast(gray >> 6); +// 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 < 50) { return 0; } else if (gray < 70) { @@ -49,6 +81,7 @@ static inline uint8_t quantizeSimple(int gray) { } // 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; @@ -64,8 +97,8 @@ static inline uint8_t quantizeNoise(int gray, int x, int y) { } } -// Main quantization function -static inline uint8_t quantize(int gray, int x, int y) { +// 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 { diff --git a/lib/GfxRenderer/Bitmap.h b/lib/GfxRenderer/Bitmap.h index 80f6083e..c7e05380 100644 --- a/lib/GfxRenderer/Bitmap.h +++ b/lib/GfxRenderer/Bitmap.h @@ -62,3 +62,8 @@ class Bitmap { mutable int16_t* errorNextRow = nullptr; mutable int prevRowY = -1; // Track row progression for error propagation }; + +// Helper functions +uint8_t quantize(int gray, int x, int y); +uint8_t quantizeSimple(int gray); +int adjustPixel(int gray); diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 3910836c..1be189e1 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -7,6 +7,8 @@ #include #include +#include "Bitmap.h" + // Context structure for picojpeg callback struct JpegReadContext { FsFile& file; @@ -23,97 +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 = 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) // 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 @@ -686,7 +603,7 @@ 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);