feat: Scale cover images up if they're smaller than the device resolution (#964)

## Summary

**What is the goal of this PR?**
* Implement feature request
[#954](https://github.com/crosspoint-reader/crosspoint-reader/issues/954)
* Ensure cover images are scaled up to match the dimensions of the
screen, as well as scaled down

**What changes are included?**
* Naïve implementation for scaling up the source image

## Additional Context

If you find the extra comments to be excessive I can pare them back. 

Edit: Fixed title

---

### 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? _**< YES >**_
This commit is contained in:
Sam Lord
2026-02-19 13:00:51 +00:00
committed by GitHub
parent eb241ab3fc
commit 6527f43cb1
2 changed files with 33 additions and 17 deletions

View File

@@ -236,8 +236,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
uint32_t scaleY_fp = 65536; uint32_t scaleY_fp = 65536;
bool needsScaling = false; bool needsScaling = false;
if (targetWidth > 0 && targetHeight > 0 && (imageInfo.m_width > targetWidth || imageInfo.m_height > targetHeight)) { if (targetWidth > 0 && targetHeight > 0 && (imageInfo.m_width != targetWidth || imageInfo.m_height != targetHeight)) {
// Calculate scale to fit within target dimensions while maintaining aspect ratio // Calculate scale to fit/fill target dimensions while maintaining aspect ratio
const float scaleToFitWidth = static_cast<float>(targetWidth) / imageInfo.m_width; const float scaleToFitWidth = static_cast<float>(targetWidth) / imageInfo.m_width;
const float scaleToFitHeight = static_cast<float>(targetHeight) / imageInfo.m_height; const float scaleToFitHeight = static_cast<float>(targetHeight) / imageInfo.m_height;
// We scale to the smaller dimension, so we can potentially crop later. // We scale to the smaller dimension, so we can potentially crop later.
@@ -261,8 +261,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
scaleY_fp = (static_cast<uint32_t>(imageInfo.m_height) << 16) / outHeight; scaleY_fp = (static_cast<uint32_t>(imageInfo.m_height) << 16) / outHeight;
needsScaling = true; needsScaling = true;
LOG_DBG("JPG", "Pre-scaling %dx%d -> %dx%d (fit to %dx%d)", imageInfo.m_width, imageInfo.m_height, outWidth, LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", imageInfo.m_width, imageInfo.m_height, outWidth, outHeight,
outHeight, targetWidth, targetHeight); targetWidth, targetHeight);
} }
// Write BMP header with output dimensions // Write BMP header with output dimensions
@@ -466,12 +466,13 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
rowCount[outX] += count; rowCount[outX] += count;
} }
// Check if we've crossed into the next output row // Check if we've crossed into the next output row(s)
// Current source Y in fixed point: y << 16 // Current source Y in fixed point: y << 16
const uint32_t srcY_fp = static_cast<uint32_t>(y + 1) << 16; const uint32_t srcY_fp = static_cast<uint32_t>(y + 1) << 16;
// Output row when source Y crosses the boundary // Output all rows whose boundaries we've crossed (handles both up and downscaling)
if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) { // For upscaling, one source row may produce multiple output rows
while (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) {
memset(rowBuffer, 0, bytesPerRow); memset(rowBuffer, 0, bytesPerRow);
if (USE_8BIT_OUTPUT && !oneBit) { if (USE_8BIT_OUTPUT && !oneBit) {
@@ -516,12 +517,18 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
bmpOut.write(rowBuffer, bytesPerRow); bmpOut.write(rowBuffer, bytesPerRow);
currentOutY++; currentOutY++;
// Reset accumulators for next output row
memset(rowAccum, 0, outWidth * sizeof(uint32_t));
memset(rowCount, 0, outWidth * sizeof(uint16_t));
// Update boundary for next output row // Update boundary for next output row
nextOutY_srcStart = static_cast<uint32_t>(currentOutY + 1) * scaleY_fp; nextOutY_srcStart = static_cast<uint32_t>(currentOutY + 1) * scaleY_fp;
// For upscaling: don't reset accumulators if next output row uses same source data
// Only reset when we'll move to a new source row
if (srcY_fp >= nextOutY_srcStart) {
// More output rows to emit from same source - keep accumulator data
continue;
}
// Moving to next source row - reset accumulators
memset(rowAccum, 0, outWidth * sizeof(uint32_t));
memset(rowCount, 0, outWidth * sizeof(uint16_t));
} }
} }
} }

View File

@@ -603,7 +603,7 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
bool needsScaling = false; bool needsScaling = false;
if (targetWidth > 0 && targetHeight > 0 && if (targetWidth > 0 && targetHeight > 0 &&
(static_cast<int>(width) > targetWidth || static_cast<int>(height) > targetHeight)) { (static_cast<int>(width) != targetWidth || static_cast<int>(height) != targetHeight)) {
const float scaleToFitWidth = static_cast<float>(targetWidth) / width; const float scaleToFitWidth = static_cast<float>(targetWidth) / width;
const float scaleToFitHeight = static_cast<float>(targetHeight) / height; const float scaleToFitHeight = static_cast<float>(targetHeight) / height;
float scale = 1.0; float scale = 1.0;
@@ -622,7 +622,7 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
scaleY_fp = (static_cast<uint32_t>(height) << 16) / outHeight; scaleY_fp = (static_cast<uint32_t>(height) << 16) / outHeight;
needsScaling = true; needsScaling = true;
LOG_DBG("PNG", "Pre-scaling %ux%u -> %dx%d (fit to %dx%d)", width, height, outWidth, outHeight, targetWidth, LOG_DBG("PNG", "Scaling %ux%u -> %dx%d (target %dx%d)", width, height, outWidth, outHeight, targetWidth,
targetHeight); targetHeight);
} }
@@ -767,10 +767,12 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
rowCount[outX] += count; rowCount[outX] += count;
} }
// Check if we've crossed into the next output row // Check if we've crossed into the next output row(s)
const uint32_t srcY_fp = static_cast<uint32_t>(y + 1) << 16; const uint32_t srcY_fp = static_cast<uint32_t>(y + 1) << 16;
if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) { // Output all rows whose boundaries we've crossed (handles both up and downscaling)
// For upscaling, one source row may produce multiple output rows
while (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) {
memset(rowBuffer, 0, bytesPerRow); memset(rowBuffer, 0, bytesPerRow);
if (USE_8BIT_OUTPUT && !oneBit) { if (USE_8BIT_OUTPUT && !oneBit) {
@@ -812,10 +814,17 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
bmpOut.write(rowBuffer, bytesPerRow); bmpOut.write(rowBuffer, bytesPerRow);
currentOutY++; currentOutY++;
nextOutY_srcStart = static_cast<uint32_t>(currentOutY + 1) * scaleY_fp;
// For upscaling: don't reset accumulators if next output row uses same source data
// Only reset when we'll move to a new source row
if (srcY_fp >= nextOutY_srcStart) {
// More output rows to emit from same source - keep accumulator data
continue;
}
// Moving to next source row - reset accumulators
memset(rowAccum, 0, outWidth * sizeof(uint32_t)); memset(rowAccum, 0, outWidth * sizeof(uint32_t));
memset(rowCount, 0, outWidth * sizeof(uint16_t)); memset(rowCount, 0, outWidth * sizeof(uint16_t));
nextOutY_srcStart = static_cast<uint32_t>(currentOutY + 1) * scaleY_fp;
} }
} }