Files
Zach Nelson 5ba85290ab refactor: Deduplicated BMP header writing in Xtc (#1439)
## 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**_
2026-04-10 00:35:43 +03:00

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;
}