You've already forked crosspoint-reader
mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-04-29 10:26:52 -07:00
5ba85290ab
## Summary **What is the goal of this PR?** Replaced manual 1-bit BMP header logic in `Xtc::generateCoverBmp()` and `Xtc::generateThumbBmp()` with calls to the existing `createBmpHeader()` utility. Added a `BmpRowOrder` enum and param to support the top-down row order of XTC cover images. --- ### 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? _**PARTIALLY**_
150 lines
5.3 KiB
C++
150 lines
5.3 KiB
C++
#include "BitmapHelpers.h"
|
|
|
|
#include <cstdint>
|
|
#include <cstring> // Added for memset
|
|
|
|
#include "Bitmap.h"
|
|
|
|
// 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<int>(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<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
|
|
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
|
const int threshold = static_cast<int>(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);
|
|
}
|
|
}
|
|
|
|
// 1-bit noise dithering for fast home screen rendering
|
|
// Uses hash-based noise for consistent dithering that works well at small sizes
|
|
uint8_t quantize1bit(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<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
|
|
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
|
const int threshold = static_cast<int>(hash >> 24); // 0-255
|
|
|
|
// Simple threshold with noise: gray >= (128 + noise offset) -> white
|
|
// The noise adds variation around the 128 midpoint
|
|
const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192
|
|
return (gray >= adjustedThreshold) ? 1 : 0;
|
|
}
|
|
|
|
void createBmpHeader(BmpHeader* bmpHeader, int width, int height, BmpRowOrder rowOrder) {
|
|
if (!bmpHeader) return;
|
|
|
|
// Zero out the memory to ensure no garbage data if called on uninitialized stack memory
|
|
std::memset(bmpHeader, 0, sizeof(BmpHeader));
|
|
|
|
uint32_t rowSize = (width + 31) / 32 * 4;
|
|
uint32_t imageSize = rowSize * height;
|
|
uint32_t fileSize = sizeof(BmpHeader) + imageSize;
|
|
|
|
bmpHeader->fileHeader.bfType = 0x4D42;
|
|
bmpHeader->fileHeader.bfSize = fileSize;
|
|
bmpHeader->fileHeader.bfReserved1 = 0;
|
|
bmpHeader->fileHeader.bfReserved2 = 0;
|
|
bmpHeader->fileHeader.bfOffBits = sizeof(BmpHeader);
|
|
|
|
bmpHeader->infoHeader.biSize = sizeof(bmpHeader->infoHeader);
|
|
bmpHeader->infoHeader.biWidth = width;
|
|
bmpHeader->infoHeader.biHeight = (rowOrder == BmpRowOrder::TopDown) ? -height : height;
|
|
bmpHeader->infoHeader.biPlanes = 1;
|
|
bmpHeader->infoHeader.biBitCount = 1;
|
|
bmpHeader->infoHeader.biCompression = 0;
|
|
bmpHeader->infoHeader.biSizeImage = imageSize;
|
|
bmpHeader->infoHeader.biXPelsPerMeter = 2835; // 72 DPI
|
|
bmpHeader->infoHeader.biYPelsPerMeter = 2835; // 72 DPI
|
|
bmpHeader->infoHeader.biClrUsed = 2;
|
|
bmpHeader->infoHeader.biClrImportant = 2;
|
|
|
|
// Color 0 (black)
|
|
bmpHeader->colors[0].rgbBlue = 0;
|
|
bmpHeader->colors[0].rgbGreen = 0;
|
|
bmpHeader->colors[0].rgbRed = 0;
|
|
bmpHeader->colors[0].rgbReserved = 0;
|
|
|
|
// Color 1 (white)
|
|
bmpHeader->colors[1].rgbBlue = 255;
|
|
bmpHeader->colors[1].rgbGreen = 255;
|
|
bmpHeader->colors[1].rgbRed = 255;
|
|
bmpHeader->colors[1].rgbReserved = 0;
|
|
} |