diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 78607573..910c0e11 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -359,7 +359,7 @@ const std::string& Epub::getLanguage() const { } std::string Epub::getCoverBmpPath(bool cropped) const { - const auto coverFileName = "cover" + cropped ? "_crop" : ""; + const auto coverFileName = std::string("cover") + (cropped ? "_crop" : ""); return cachePath + "/" + coverFileName + ".bmp"; } diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 08420bf9..68e66050 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -152,8 +152,17 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height); } +void GfxRenderer::drawVal(const int x, const int y, const uint8_t val) const { + if (renderMode == BW && val < 3) { + drawPixel(x, y); + } else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) { + drawPixel(x, y, false); + } else if (renderMode == GRAYSCALE_LSB && val == 1) { + drawPixel(x, y, false); + } +} void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight, - const float cropX, const float cropY) const { + const float cropX, const float cropY, bool extend) const { // For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit) if (bitmap.is1Bit() && cropX == 0.0f && cropY == 0.0f) { drawBitmap1Bit(bitmap, x, y, maxWidth, maxHeight); @@ -233,12 +242,85 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3; - 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); + drawVal(screenX, screenY, val); + + // draw extended pixels + // amount of pixels taken from bitmap and repeated to extend. + // Trade-off between risk of repeating "edgy" content like text and more quality + int extendY = 20; + int extendX = 20; + int drawExtY = 0; + if (extend) { + int imgHeight = std::floor(scale * (bitmap.getHeight() - cropPixY)); + int imgWidth = std::floor(scale * (bitmap.getWidth() - cropPixX)); + // 1. TOP EXTENSION + // Check if the current pixel is within the strip to be mirrored + if (screenY >= y && screenY < y + extendY) { + // How many times do we need to mirror to fill the gap 'y'? + // Using +1 to ensure we cover fractional blocks at the screen edge + int numIterations = (y / extendY) + 1; + + for (int ny = 0; ny < numIterations; ny++) { + // Compute 2 target rows t1, t2 for "accordeon" effect. + // Mirror Fold (e.g., pixel 0 goes to y-1, pixel 1 to y-2) + int t1 = y - 1 - (2 * ny * extendY + (screenY - y)); + // Reverse Fold (creates the 'accordion' continuity) + int t2 = y - 1 - (2 * ny * extendY + (2 * extendY - 1 - (screenY - y))); + + if (t1 >= 0 && t1 < y) drawVal(screenX, t1, val); + if (t2 >= 0 && t2 < y) drawVal(screenX, t2, val); + } + } + + // 2. BOTTOM EXTENSION + int imgBottom = y + imgHeight; + int gapBottom = getScreenHeight() - imgBottom; + + if (screenY >= imgBottom - extendY && screenY < imgBottom) { + int numIterations = (gapBottom / extendY) + 1; + + for (int ny = 0; ny < numIterations; ny++) { + // Mirror Fold (pixel at imgBottom-1 goes to imgBottom) + int t1 = imgBottom + (2 * ny * extendY + (imgBottom - 1 - screenY)); + // Reverse Fold + int t2 = imgBottom + (2 * ny * extendY + (2 * extendY - 1 - (imgBottom - 1 - screenY))); + + if (t1 >= imgBottom && t1 < getScreenHeight()) drawVal(screenX, t1, val); + if (t2 >= imgBottom && t2 < getScreenHeight()) drawVal(screenX, t2, val); + } + } + + // --- 2. LEFT EXTENSION --- + int imgRight = x + imgWidth; // x is the left margin/offset + // If the current pixel is within the leftmost 'extendX' pixels of the image + if (screenX >= x && screenX < x + extendX) { + int numIterations = (x / extendX) + 1; + for (int nx = 0; nx < numIterations; nx++) { + // Mirror Fold (pixel at 'x' maps to 'x-1') + int t1 = x - 1 - (2 * nx * extendX + (screenX - x)); + // Reverse Fold + int t2 = x - 1 - (2 * nx * extendX + (2 * extendX - 1 - (screenX - x))); + + if (t1 >= 0 && t1 < x) drawVal(t1, screenY, val); + if (t2 >= 0 && t2 < x) drawVal(t2, screenY, val); + } + } + + // --- 3. RIGHT EXTENSION --- + int gapRight = getScreenWidth() - imgRight; + // If the current pixel is within the rightmost 'extendX' pixels of the image + if (screenX >= imgRight - extendX && screenX < imgRight) { + int numIterations = (gapRight / extendX) + 1; + for (int nx = 0; nx < numIterations; nx++) { + // Mirror Fold (pixel at 'imgRight-1' maps to 'imgRight') + int t1 = imgRight + (2 * nx * extendX + (imgRight - 1 - screenX)); + // Reverse Fold + int t2 = imgRight + (2 * nx * extendX + (2 * extendX - 1 - (imgRight - 1 - screenX))); + + if (t1 >= imgRight && t1 < getScreenWidth()) drawVal(t1, screenY, val); + if (t2 >= imgRight && t2 < getScreenWidth()) drawVal(t2, screenY, val); + } + } } } } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index b1fea69b..22954b2a 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -62,12 +62,13 @@ class GfxRenderer { // Drawing void drawPixel(int x, int y, bool state = true) const; + void drawVal(const int x, const int y, const uint8_t val) const; void drawLine(int x1, int y1, int x2, int y2, bool state = true) const; void drawRect(int x, int y, int width, int height, bool state = true) const; void fillRect(int x, int y, int width, int height, bool state = true) const; void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const; - void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0, - float cropY = 0) const; + void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0, float cropY = 0, + bool extend = false) const; void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const; void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 8ce32a2c..b5ee133e 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, EXTEND = 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 c0d6844f..2bd46bf0 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -177,22 +177,23 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { y = (pageHeight - bitmap.getHeight()) / 2; } + bool extended = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::EXTEND; 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, pageWidth, pageHeight, cropX, cropY, extended); 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, pageWidth, pageHeight, cropX, cropY, extended); 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, pageWidth, pageHeight, cropX, cropY, extended); renderer.copyGrayscaleMsbBuffers(); renderer.displayGrayBuffer(); diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 943fdb4c..1f5c47c2 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", "Extend"}), 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,