diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 1be189e1..9613767c 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -8,6 +8,7 @@ #include #include "Bitmap.h" +#include "Dithering.h" // Context structure for picojpeg callback struct JpegReadContext { @@ -31,191 +32,6 @@ constexpr int TARGET_MAX_WIDTH = 480; // Max width for cover images (portrait constexpr int TARGET_MAX_HEIGHT = 800; // Max height for cover images (portrait display height) // ============================================================================ -// 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);