diff --git a/lib/GfxRenderer/Bitmap.cpp b/lib/GfxRenderer/Bitmap.cpp index 7c46df1c..8cc8a5f3 100644 --- a/lib/GfxRenderer/Bitmap.cpp +++ b/lib/GfxRenderer/Bitmap.cpp @@ -250,34 +250,29 @@ BmpReaderError Bitmap::parseHeaders() { delete[] errorNextRow; errorCurRow = new int16_t[width + 2](); // +2 for boundary handling errorNextRow = new int16_t[width + 2](); - lastRowY = -1; + prevRowY = -1; } return BmpReaderError::Ok; } // packed 2bpp output, 0 = black, 1 = dark gray, 2 = light gray, 3 = white -BmpReaderError Bitmap::readRow(uint8_t* data, uint8_t* rowBuffer, int rowY) const { +BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const { // Note: rowBuffer should be pre-allocated by the caller to size 'rowBytes' if (file.read(rowBuffer, rowBytes) != rowBytes) return BmpReaderError::ShortReadRow; // Handle Floyd-Steinberg error buffer progression const bool useFS = USE_FLOYD_STEINBERG && errorCurRow && errorNextRow; if (useFS) { - // Check if we need to advance to next row (or reset if jumping) - if (rowY != lastRowY + 1 && rowY != 0) { - // Non-sequential row access - reset error buffers - memset(errorCurRow, 0, (width + 2) * sizeof(int16_t)); - memset(errorNextRow, 0, (width + 2) * sizeof(int16_t)); - } else if (rowY > 0) { + if (prevRowY != -1) { // Sequential access - swap buffers int16_t* temp = errorCurRow; errorCurRow = errorNextRow; errorNextRow = temp; memset(errorNextRow, 0, (width + 2) * sizeof(int16_t)); } - lastRowY = rowY; } + prevRowY += 1; uint8_t* outPtr = data; uint8_t currentOutByte = 0; @@ -292,7 +287,7 @@ BmpReaderError Bitmap::readRow(uint8_t* data, uint8_t* rowBuffer, int rowY) cons color = quantizeFloydSteinberg(lum, currentX, width, errorCurRow, errorNextRow, false); } else { // Simple quantization or noise dithering - color = quantize(lum, currentX, rowY); + color = quantize(lum, currentX, prevRowY); } currentOutByte |= (color << bitShift); if (bitShift == 0) { @@ -365,7 +360,7 @@ BmpReaderError Bitmap::rewindToData() const { if (USE_FLOYD_STEINBERG && errorCurRow && errorNextRow) { memset(errorCurRow, 0, (width + 2) * sizeof(int16_t)); memset(errorNextRow, 0, (width + 2) * sizeof(int16_t)); - lastRowY = -1; + prevRowY = -1; } return BmpReaderError::Ok; diff --git a/lib/GfxRenderer/Bitmap.h b/lib/GfxRenderer/Bitmap.h index 7e799647..a3f2e00c 100644 --- a/lib/GfxRenderer/Bitmap.h +++ b/lib/GfxRenderer/Bitmap.h @@ -31,7 +31,7 @@ class Bitmap { explicit Bitmap(FsFile& file) : file(file) {} ~Bitmap(); BmpReaderError parseHeaders(); - BmpReaderError readRow(uint8_t* data, uint8_t* rowBuffer, int rowY) const; + BmpReaderError readNextRow(uint8_t* data, uint8_t* rowBuffer) const; BmpReaderError rewindToData() const; int getWidth() const { return width; } int getHeight() const { return height; } @@ -55,5 +55,5 @@ class Bitmap { // Floyd-Steinberg dithering state (mutable for const methods) mutable int16_t* errorCurRow = nullptr; mutable int16_t* errorNextRow = nullptr; - mutable int lastRowY = -1; // Track row progression for error propagation + mutable int prevRowY = -1; // Track row progression for error propagation }; diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 52cd6f4d..cc1288a7 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -152,18 +152,24 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height); } -void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, - const int maxHeight) const { +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 { float scale = 1.0f; bool isScaled = false; - if (maxWidth > 0 && bitmap.getWidth() > maxWidth) { - scale = static_cast(maxWidth) / static_cast(bitmap.getWidth()); + 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; } - if (maxHeight > 0 && bitmap.getHeight() > maxHeight) { - scale = std::min(scale, static_cast(maxHeight) / static_cast(bitmap.getHeight())); + 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"); // 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 @@ -178,29 +184,36 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con return; } - for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) { + 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 = y + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY); + 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; } - if (bitmap.readRow(outputRow, rowBytes, bmpY) != BmpReaderError::Ok) { + if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) { Serial.printf("[%lu] [GFX] Failed to read row %d from bitmap\n", millis(), bmpY); free(outputRow); free(rowBytes); return; } - for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) { - int screenX = x + bmpX; + if (bmpY < cropPixY) { + // Skip the row if it's outside the crop area + continue; + } + + 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; } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index af4f2d50..e3e9558d 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -66,7 +66,8 @@ class GfxRenderer { 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) const; + void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0, + float cropY = 0) const; // Text int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 9c61ef0d..8c8db889 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -468,7 +468,9 @@ bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) { // Calculate scale to fit within target dimensions while maintaining aspect ratio const float scaleToFitWidth = static_cast(TARGET_MAX_WIDTH) / imageInfo.m_width; const float scaleToFitHeight = static_cast(TARGET_MAX_HEIGHT) / imageInfo.m_height; - const float scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; + // We scale to the smaller dimension, so we can potentially crop later. + // TODO: ideally, we already crop here. + const float scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; outWidth = static_cast(imageInfo.m_width * scale); outHeight = static_cast(imageInfo.m_height * scale); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 2cad82ae..5415b90f 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -17,7 +17,8 @@ class CrossPointSettings { CrossPointSettings& operator=(const CrossPointSettings&) = delete; // Should match with SettingsActivity text - enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3 }; + enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4 }; + enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1 }; // Status bar display type enum enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2 }; @@ -54,6 +55,8 @@ class CrossPointSettings { // Sleep screen settings uint8_t sleepScreen = DARK; + // Sleep screen cover mode settings + uint8_t sleepScreenCoverMode = FIT; // Status bar settings uint8_t statusBar = FULL; // Text rendering settings @@ -96,6 +99,7 @@ class CrossPointSettings { float getReaderLineCompression() const; unsigned long getSleepTimeoutMs() const; int getRefreshFrequency() const; + int getReaderScreenMargin() const; }; // Helper macro to access settings diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index cc9fa9d9..8d8fd791 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -28,6 +28,10 @@ void SleepActivity::onEnter() { Activity::onEnter(); renderPopup("Entering Sleep..."); + if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::BLANK) { + return renderBlankSleepScreen(); + } + if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM) { return renderCustomSleepScreen(); } @@ -142,20 +146,36 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { int x, y; const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); + float cropX = 0, cropY = 0; + 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 - const float ratio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); + 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 = (pageHeight - pageWidth / ratio) / 2; + 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 - x = (pageWidth - pageHeight * ratio) / 2; + 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((pageWidth - pageHeight * ratio) / 2); y = 0; + Serial.printf("[%lu] [SLP] Centering with ratio %f to x=%d\n", millis(), ratio, x); } } else { // center the image @@ -163,21 +183,22 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { y = (pageHeight - bitmap.getHeight()) / 2; } + Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y); renderer.clearScreen(); - renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight); + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 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); + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); renderer.copyGrayscaleLsbBuffers(); bitmap.rewindToData(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight); + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); renderer.copyGrayscaleMsbBuffers(); renderer.displayGrayBuffer(); @@ -234,3 +255,8 @@ void SleepActivity::renderCoverSleepScreen() const { renderDefaultSleepScreen(); } + +void SleepActivity::renderBlankSleepScreen() const { + renderer.clearScreen(); + renderer.displayBuffer(EInkDisplay::HALF_REFRESH); +} diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 3a77d33b..283220ce 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -15,4 +15,5 @@ class SleepActivity final : public Activity { void renderCustomSleepScreen() const; void renderCoverSleepScreen() const; void renderBitmapSleepScreen(const Bitmap& bitmap) const; + void renderBlankSleepScreen() const; }; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index d3cd5016..1b5dc777 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -16,8 +16,6 @@ namespace { // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() constexpr unsigned long skipChapterMs = 700; constexpr unsigned long goHomeMs = 1000; -constexpr int topPadding = 5; -constexpr int horizontalPadding = 5; constexpr int statusBarMargin = 19; } // namespace @@ -253,9 +251,9 @@ void EpubReaderActivity::renderScreen() { int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, &orientedMarginLeft); - orientedMarginTop += topPadding; - orientedMarginLeft += horizontalPadding; - orientedMarginRight += horizontalPadding; + orientedMarginTop += SETTINGS.screenMargin; + orientedMarginLeft += SETTINGS.screenMargin; + orientedMarginRight += SETTINGS.screenMargin; orientedMarginBottom += statusBarMargin; if (!section) { diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 69c2bd03..a51339d6 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -1,6 +1,7 @@ #include "SettingsActivity.h" #include +#include #include "CrossPointSettings.h" #include "MappedInputManager.h" @@ -66,7 +67,6 @@ void SettingsActivity::taskTrampoline(void* param) { void SettingsActivity::onEnter() { Activity::onEnter(); - renderingMutex = xSemaphoreCreateMutex(); // Reset selection to first item @@ -76,7 +76,7 @@ void SettingsActivity::onEnter() { updateRequired = true; xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask", - 2048, // Stack size + 4096, // Stack size this, // Parameters 1, // Priority &displayTaskHandle // Task handle @@ -123,11 +123,9 @@ void SettingsActivity::loop() { updateRequired = true; } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || mappedInput.wasPressed(MappedInputManager::Button::Right)) { - // Move selection down - if (selectedSettingIndex < settingsCount - 1) { - selectedSettingIndex++; - updateRequired = true; - } + // Move selection down (with wrap around) + selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0; + updateRequired = true; } } diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 34351f8e..5ac17686 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -20,6 +20,29 @@ struct SettingInfo { uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE/ENUM) std::string CrossPointSettings::* stringValuePtr; // Pointer to string member (for TEXT) std::vector enumValues; + + struct ValueRange { + uint8_t min; + uint8_t max; + uint8_t step; + }; + // Bounds/step for VALUE type settings + ValueRange valueRange; + + // Static constructors + static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) { + return {name, SettingType::TOGGLE, ptr}; + } + + static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector values) { + return {name, SettingType::ENUM, ptr, std::move(values)}; + } + + static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; } + + static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) { + return {name, SettingType::VALUE, ptr, {}, valueRange}; + } }; class SettingsActivity final : public ActivityWithSubactivity {