diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 31787e2..eb079d7 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -161,21 +161,30 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con } float scale = 1.0f; - bool isScaled = false; int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f); int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f); Serial.printf("[%lu] [GFX] Cropping %dx%d by %dx%d pix, is %s\n", millis(), bitmap.getWidth(), bitmap.getHeight(), cropPixX, cropPixY, bitmap.isTopDown() ? "top-down" : "bottom-up"); - if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) { - scale = static_cast(maxWidth) / static_cast((1.0f - cropX) * bitmap.getWidth()); - isScaled = true; + // Calculate effective image dimensions after cropping + const int effectiveWidth = static_cast((1.0f - cropX) * bitmap.getWidth()); + const int effectiveHeight = static_cast((1.0f - cropY) * bitmap.getHeight()); + + // Calculate scale to fit within maxWidth/maxHeight (supports both up and down scaling) + if (maxWidth > 0 && maxHeight > 0) { + const float scaleX = static_cast(maxWidth) / static_cast(effectiveWidth); + const float scaleY = static_cast(maxHeight) / static_cast(effectiveHeight); + scale = std::min(scaleX, scaleY); + } else if (maxWidth > 0) { + scale = static_cast(maxWidth) / static_cast(effectiveWidth); + } else if (maxHeight > 0) { + scale = static_cast(maxHeight) / static_cast(effectiveHeight); } - if (maxHeight > 0 && (1.0f - cropY) * bitmap.getHeight() > maxHeight) { - scale = std::min(scale, static_cast(maxHeight) / static_cast((1.0f - cropY) * bitmap.getHeight())); - isScaled = true; - } - Serial.printf("[%lu] [GFX] Scaling by %f - %s\n", millis(), scale, isScaled ? "scaled" : "not scaled"); + + const bool isUpscaling = scale > 1.0f; + const bool isDownscaling = scale < 1.0f; + Serial.printf("[%lu] [GFX] Scaling by %f - %s\n", millis(), scale, + isUpscaling ? "upscaling" : (isDownscaling ? "downscaling" : "no scaling")); // Calculate output row size (2 bits per pixel, packed into bytes) // IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide @@ -190,18 +199,10 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con return; } - for (int bmpY = 0; bmpY < (bitmap.getHeight() - cropPixY); bmpY++) { - // The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative). - // Screen's (0, 0) is the top-left corner. - int screenY = -cropPixY + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY); - if (isScaled) { - screenY = std::floor(screenY * scale); - } - screenY += y; // the offset should not be scaled - if (screenY >= getScreenHeight()) { - break; - } + // Track the last drawn Y position for upscaling (to fill gaps) + int lastDrawnY = -1; + for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) { if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) { Serial.printf("[%lu] [GFX] Failed to read row %d from bitmap\n", millis(), bmpY); free(outputRow); @@ -209,36 +210,51 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con return; } - if (screenY < 0) { + // Skip rows in the crop area + if (bmpY < cropPixY || bmpY >= bitmap.getHeight() - cropPixY) { continue; } - if (bmpY < cropPixY) { - // Skip the row if it's outside the crop area - continue; - } + // Calculate the source Y coordinate (relative to cropped area) + const int srcY = bmpY - cropPixY; - for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) { - int screenX = bmpX - cropPixX; - if (isScaled) { - screenX = std::floor(screenX * scale); - } - screenX += x; // the offset should not be scaled - if (screenX >= getScreenWidth()) { - break; - } - if (screenX < 0) { - continue; - } + // The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative). + // Screen's (0, 0) is the top-left corner. + const int logicalY = bitmap.isTopDown() ? srcY : (effectiveHeight - 1 - srcY); - const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3; + // Calculate screen Y position + const int screenYStart = y + static_cast(std::floor(logicalY * scale)); + // For upscaling, calculate the end position for this source row + const int screenYEnd = isUpscaling ? (y + static_cast(std::floor((logicalY + 1) * scale))) : (screenYStart + 1); - if (renderMode == BW && val < 3) { - drawPixel(screenX, screenY); - } else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) { - drawPixel(screenX, screenY, false); - } else if (renderMode == GRAYSCALE_LSB && val == 1) { - drawPixel(screenX, screenY, false); + // Draw to all Y positions this source row maps to (for upscaling, this fills gaps) + for (int screenY = screenYStart; screenY < screenYEnd; screenY++) { + if (screenY < 0) continue; + if (screenY >= getScreenHeight()) break; + + for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) { + const int srcX = bmpX - cropPixX; + + // Calculate screen X position + const int screenXStart = x + static_cast(std::floor(srcX * scale)); + // For upscaling, calculate the end position for this source pixel + const int screenXEnd = isUpscaling ? (x + static_cast(std::floor((srcX + 1) * scale))) : (screenXStart + 1); + + const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3; + + // Draw to all X positions this source pixel maps to (for upscaling, this fills gaps) + for (int screenX = screenXStart; screenX < screenXEnd; screenX++) { + if (screenX < 0) continue; + if (screenX >= getScreenWidth()) break; + + if (renderMode == BW && val < 3) { + drawPixel(screenX, screenY); + } else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) { + drawPixel(screenX, screenY, false); + } else if (renderMode == GRAYSCALE_LSB && val == 1) { + drawPixel(screenX, screenY, false); + } + } } } } @@ -250,16 +266,20 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight) const { float scale = 1.0f; - bool isScaled = false; - if (maxWidth > 0 && bitmap.getWidth() > maxWidth) { + + // Calculate scale to fit within maxWidth/maxHeight (supports both up and down scaling) + if (maxWidth > 0 && maxHeight > 0) { + const float scaleX = static_cast(maxWidth) / static_cast(bitmap.getWidth()); + const float scaleY = static_cast(maxHeight) / static_cast(bitmap.getHeight()); + scale = std::min(scaleX, scaleY); + } else if (maxWidth > 0) { scale = static_cast(maxWidth) / static_cast(bitmap.getWidth()); - isScaled = true; - } - if (maxHeight > 0 && bitmap.getHeight() > maxHeight) { - scale = std::min(scale, static_cast(maxHeight) / static_cast(bitmap.getHeight())); - isScaled = true; + } else if (maxHeight > 0) { + scale = static_cast(maxHeight) / static_cast(bitmap.getHeight()); } + const bool isUpscaling = scale > 1.0f; + // For 1-bit BMP, output is still 2-bit packed (for consistency with readNextRow) const int outputRowSize = (bitmap.getWidth() + 3) / 4; auto* outputRow = static_cast(malloc(outputRowSize)); @@ -282,33 +302,39 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y, } // Calculate screen Y based on whether BMP is top-down or bottom-up - const int bmpYOffset = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY; - int screenY = y + (isScaled ? static_cast(std::floor(bmpYOffset * scale)) : bmpYOffset); - if (screenY >= getScreenHeight()) { - continue; // Continue reading to keep row counter in sync - } - if (screenY < 0) { - continue; - } + const int logicalY = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY; - for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) { - int screenX = x + (isScaled ? static_cast(std::floor(bmpX * scale)) : bmpX); - if (screenX >= getScreenWidth()) { - break; - } - if (screenX < 0) { - continue; - } + // Calculate screen Y position + const int screenYStart = y + static_cast(std::floor(logicalY * scale)); + // For upscaling, calculate the end position for this source row + const int screenYEnd = isUpscaling ? (y + static_cast(std::floor((logicalY + 1) * scale))) : (screenYStart + 1); - // Get 2-bit value (result of readNextRow quantization) - const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3; + // Draw to all Y positions this source row maps to (for upscaling, this fills gaps) + for (int screenY = screenYStart; screenY < screenYEnd; screenY++) { + if (screenY < 0) continue; + if (screenY >= getScreenHeight()) continue; - // For 1-bit source: 0 or 1 -> map to black (0,1,2) or white (3) - // val < 3 means black pixel (draw it) - if (val < 3) { - drawPixel(screenX, screenY, true); + for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) { + // Calculate screen X position + const int screenXStart = x + static_cast(std::floor(bmpX * scale)); + // For upscaling, calculate the end position for this source pixel + const int screenXEnd = isUpscaling ? (x + static_cast(std::floor((bmpX + 1) * scale))) : (screenXStart + 1); + + // Get 2-bit value (result of readNextRow quantization) + const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3; + + // For 1-bit source: 0 or 1 -> map to black (0,1,2) or white (3) + // val < 3 means black pixel (draw it) + if (val < 3) { + // Draw to all X positions this source pixel maps to (for upscaling, this fills gaps) + for (int screenX = screenXStart; screenX < screenXEnd; screenX++) { + if (screenX < 0) continue; + if (screenX >= getScreenWidth()) break; + drawPixel(screenX, screenY, true); + } + } + // White pixels (val == 3) are not drawn (leave background) } - // White pixels (val == 3) are not drawn (leave background) } } diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 2a253b0..1cee7d6 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -17,7 +17,7 @@ class CrossPointSettings { // Should match with SettingsActivity text enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4 }; - enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1 }; + enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, ACTUAL = 2 }; // Status bar display type enum enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2 }; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index c0d6844..91d75ab 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -141,58 +141,79 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); float cropX = 0, cropY = 0; + int drawWidth = pageWidth; + int drawHeight = pageHeight; Serial.printf("[%lu] [SLP] bitmap %d x %d, screen %d x %d\n", millis(), bitmap.getWidth(), bitmap.getHeight(), pageWidth, pageHeight); - if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) { - // image will scale, make sure placement is right - float ratio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); - const float screenRatio = static_cast(pageWidth) / static_cast(pageHeight); - Serial.printf("[%lu] [SLP] bitmap ratio: %f, screen ratio: %f\n", millis(), ratio, screenRatio); - if (ratio > screenRatio) { - // image wider than viewport ratio, scaled down image needs to be centered vertically - if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) { - cropX = 1.0f - (screenRatio / ratio); - Serial.printf("[%lu] [SLP] Cropping bitmap x: %f\n", millis(), cropX); - ratio = (1.0f - cropX) * static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); - } - x = 0; - y = std::round((static_cast(pageHeight) - static_cast(pageWidth) / ratio) / 2); - Serial.printf("[%lu] [SLP] Centering with ratio %f to y=%d\n", millis(), ratio, y); - } else { - // image taller than viewport ratio, scaled down image needs to be centered horizontally - if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) { - cropY = 1.0f - (ratio / screenRatio); - Serial.printf("[%lu] [SLP] Cropping bitmap y: %f\n", millis(), cropY); - ratio = static_cast(bitmap.getWidth()) / ((1.0f - cropY) * static_cast(bitmap.getHeight())); - } - x = std::round((static_cast(pageWidth) - static_cast(pageHeight) * ratio) / 2); - y = 0; - Serial.printf("[%lu] [SLP] Centering with ratio %f to x=%d\n", millis(), ratio, x); - } - } else { - // center the image + const float bitmapRatio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); + const float screenRatio = static_cast(pageWidth) / static_cast(pageHeight); + Serial.printf("[%lu] [SLP] bitmap ratio: %f, screen ratio: %f\n", millis(), bitmapRatio, screenRatio); + + const auto coverMode = SETTINGS.sleepScreenCoverMode; + + if (coverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::ACTUAL) { + // ACTUAL mode: Show image at actual size, centered (no scaling) x = (pageWidth - bitmap.getWidth()) / 2; y = (pageHeight - bitmap.getHeight()) / 2; + // Don't constrain to screen dimensions - drawBitmap will clip + drawWidth = 0; + drawHeight = 0; + Serial.printf("[%lu] [SLP] ACTUAL mode: centering at %d, %d\n", millis(), x, y); + } else if (coverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) { + // CROP mode: Scale to fill screen completely (may crop edges) + // Calculate crop values to fill the screen while maintaining aspect ratio + if (bitmapRatio > screenRatio) { + // Image is wider than screen ratio - crop horizontally + cropX = 1.0f - (screenRatio / bitmapRatio); + Serial.printf("[%lu] [SLP] CROP mode: cropping x by %f\n", millis(), cropX); + } else if (bitmapRatio < screenRatio) { + // Image is taller than screen ratio - crop vertically + cropY = 1.0f - (bitmapRatio / screenRatio); + Serial.printf("[%lu] [SLP] CROP mode: cropping y by %f\n", millis(), cropY); + } + // After cropping, the image should fill the screen exactly + x = 0; + y = 0; + Serial.printf("[%lu] [SLP] CROP mode: drawing at 0, 0 with crop %f, %f\n", millis(), cropX, cropY); + } else { + // FIT mode (default): Scale to fit entire image within screen (may have letterboxing) + // Calculate the scaled dimensions + float scale; + if (bitmapRatio > screenRatio) { + // Image is wider than screen ratio - fit to width + scale = static_cast(pageWidth) / static_cast(bitmap.getWidth()); + } else { + // Image is taller than screen ratio - fit to height + scale = static_cast(pageHeight) / static_cast(bitmap.getHeight()); + } + const int scaledWidth = static_cast(bitmap.getWidth() * scale); + const int scaledHeight = static_cast(bitmap.getHeight() * scale); + + // Center the scaled image + x = (pageWidth - scaledWidth) / 2; + y = (pageHeight - scaledHeight) / 2; + Serial.printf("[%lu] [SLP] FIT mode: scale %f, scaled size %d x %d, position %d, %d\n", millis(), scale, + scaledWidth, scaledHeight, x, y); } Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y); renderer.clearScreen(); - renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); + renderer.drawBitmap(bitmap, x, y, drawWidth, drawHeight, cropX, cropY); renderer.displayBuffer(EInkDisplay::HALF_REFRESH); if (bitmap.hasGreyscale()) { bitmap.rewindToData(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); - renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); + renderer.drawBitmap(bitmap, x, y, drawWidth, drawHeight, cropX, cropY); renderer.copyGrayscaleLsbBuffers(); bitmap.rewindToData(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); + renderer.drawBitmap(bitmap, x, y, drawWidth, drawHeight, cropX, cropY); renderer.copyGrayscaleMsbBuffers(); renderer.displayGrayBuffer(); diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7abdb78..36710b3 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -15,7 +15,7 @@ constexpr int displaySettingsCount = 5; const SettingInfo displaySettings[displaySettingsCount] = { // Should match with SLEEP_SCREEN_MODE SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), - SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), + SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop", "Actual"}), SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}), SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,