A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Take screenshots (#759)

## Summary

* **What is the goal of this PR?** Implements a take-screenshot feature
* **What changes are included?**

- Quick press Power button and Down button at the same time to take a
screenshot
- Screenshots are saved in `screenshots` folder

## Additional Context

- Currently it does not use the device orientation.

---

Example screenshots:


![screenshot-6771.bmp](https://github.com/user-attachments/files/25157071/screenshot-6771.bmp)

[screenshot-6771.bmp](https://github.com/user-attachments/files/25157071/screenshot-6771.bmp)


![screenshot-14158.bmp](https://github.com/user-attachments/files/25157073/screenshot-14158.bmp)

[screenshot-14158.bmp](https://github.com/user-attachments/files/25157073/screenshot-14158.bmp)


### AI Usage

Did you use AI tools to help write this code? _** YES

---------

Co-authored-by: Eliz Kilic <elizk@google.com>
Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>

authored by

Eliz
Eliz Kilic
Xuan Son Nguyen
Copilot
Arthur Tazhitdinov
and committed by
GitHub
c1fad16e 5f5561b6

+264 -10
+5
USER_GUIDE.md
··· 38 38 39 39 Button layout can be customized in **[Settings](#35-settings)**. 40 40 41 + ### Taking a Screenshot 42 + When the Power Button and Volume Down button are pressed at the same time, it will take a screenshot and save it in the folder `screenshots/`. 43 + 44 + Alternatively, while reading a book, press the **Confirm** button to open the reader menu and select **Take screenshot**. 45 + 41 46 --- 42 47 43 48 ## 2. Power & Startup
+32
lib/GfxRenderer/Bitmap.h
··· 6 6 7 7 #include "BitmapHelpers.h" 8 8 9 + #pragma pack(push, 1) 10 + struct BmpHeader { 11 + struct { 12 + uint16_t bfType; 13 + uint32_t bfSize; 14 + uint16_t bfReserved1; 15 + uint16_t bfReserved2; 16 + uint32_t bfOffBits; 17 + } fileHeader; 18 + struct { 19 + uint32_t biSize; 20 + int32_t biWidth; 21 + int32_t biHeight; 22 + uint16_t biPlanes; 23 + uint16_t biBitCount; 24 + uint32_t biCompression; 25 + uint32_t biSizeImage; 26 + int32_t biXPelsPerMeter; 27 + int32_t biYPelsPerMeter; 28 + uint32_t biClrUsed; 29 + uint32_t biClrImportant; 30 + } infoHeader; 31 + struct RgbQuad { 32 + uint8_t rgbBlue; 33 + uint8_t rgbGreen; 34 + uint8_t rgbRed; 35 + uint8_t rgbReserved; 36 + }; 37 + RgbQuad colors[2]; 38 + }; 39 + #pragma pack(pop) 40 + 9 41 enum class BmpReaderError : uint8_t { 10 42 Ok = 0, 11 43 FileInvalid,
+44
lib/GfxRenderer/BitmapHelpers.cpp
··· 1 1 #include "BitmapHelpers.h" 2 2 3 3 #include <cstdint> 4 + #include <cstring> // Added for memset 5 + 6 + #include "Bitmap.h" 4 7 5 8 // Brightness/Contrast adjustments: 6 9 constexpr bool USE_BRIGHTNESS = false; // true: apply brightness/gamma adjustments ··· 104 107 const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192 105 108 return (gray >= adjustedThreshold) ? 1 : 0; 106 109 } 110 + 111 + void createBmpHeader(BmpHeader* bmpHeader, int width, int height) { 112 + if (!bmpHeader) return; 113 + 114 + // Zero out the memory to ensure no garbage data if called on uninitialized stack memory 115 + std::memset(bmpHeader, 0, sizeof(BmpHeader)); 116 + 117 + uint32_t rowSize = (width + 31) / 32 * 4; 118 + uint32_t imageSize = rowSize * height; 119 + uint32_t fileSize = sizeof(BmpHeader) + imageSize; 120 + 121 + bmpHeader->fileHeader.bfType = 0x4D42; 122 + bmpHeader->fileHeader.bfSize = fileSize; 123 + bmpHeader->fileHeader.bfReserved1 = 0; 124 + bmpHeader->fileHeader.bfReserved2 = 0; 125 + bmpHeader->fileHeader.bfOffBits = sizeof(BmpHeader); 126 + 127 + bmpHeader->infoHeader.biSize = sizeof(bmpHeader->infoHeader); 128 + bmpHeader->infoHeader.biWidth = width; 129 + bmpHeader->infoHeader.biHeight = height; 130 + bmpHeader->infoHeader.biPlanes = 1; 131 + bmpHeader->infoHeader.biBitCount = 1; 132 + bmpHeader->infoHeader.biCompression = 0; 133 + bmpHeader->infoHeader.biSizeImage = imageSize; 134 + bmpHeader->infoHeader.biXPelsPerMeter = 0; 135 + bmpHeader->infoHeader.biYPelsPerMeter = 0; 136 + bmpHeader->infoHeader.biClrUsed = 0; 137 + bmpHeader->infoHeader.biClrImportant = 0; 138 + 139 + // Color 0 (black) 140 + bmpHeader->colors[0].rgbBlue = 0; 141 + bmpHeader->colors[0].rgbGreen = 0; 142 + bmpHeader->colors[0].rgbRed = 0; 143 + bmpHeader->colors[0].rgbReserved = 0; 144 + 145 + // Color 1 (white) 146 + bmpHeader->colors[1].rgbBlue = 255; 147 + bmpHeader->colors[1].rgbGreen = 255; 148 + bmpHeader->colors[1].rgbRed = 255; 149 + bmpHeader->colors[1].rgbReserved = 0; 150 + }
+6
lib/GfxRenderer/BitmapHelpers.h
··· 1 1 #pragma once 2 2 3 + #include <cstdint> 3 4 #include <cstring> 5 + 6 + struct BmpHeader; 4 7 5 8 // Helper functions 6 9 uint8_t quantize(int gray, int x, int y); 7 10 uint8_t quantizeSimple(int gray); 8 11 uint8_t quantize1bit(int gray, int x, int y); 9 12 int adjustPixel(int gray); 13 + 14 + // Populates a 1-bit BMP header in the provided memory. 15 + void createBmpHeader(BmpHeader* bmpHeader, int width, int height); 10 16 11 17 // 1-bit Atkinson dithering - better quality than noise dithering for thumbnails 12 18 // Error distribution pattern (same as 2-bit but quantizes to 2 levels):
+1
lib/I18n/I18nKeys.h
··· 355 355 STR_BOOK_S_STYLE, 356 356 STR_EMBEDDED_STYLE, 357 357 STR_OPDS_SERVER_URL, 358 + STR_SCREENSHOT_BUTTON, 358 359 // Sentinel - must be last 359 360 _COUNT 360 361 };
+1
lib/I18n/translations/czech.yaml
··· 317 317 STR_BOOK_S_STYLE: "Styl knihy" 318 318 STR_EMBEDDED_STYLE: "Vložený styl" 319 319 STR_OPDS_SERVER_URL: "URL serveru OPDS" 320 + STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
+1
lib/I18n/translations/english.yaml
··· 317 317 STR_BOOK_S_STYLE: "Book's Style" 318 318 STR_EMBEDDED_STYLE: "Embedded Style" 319 319 STR_OPDS_SERVER_URL: "OPDS Server URL" 320 + STR_SCREENSHOT_BUTTON: "Take screenshot"
+1
lib/I18n/translations/french.yaml
··· 317 317 STR_BOOK_S_STYLE: "Style du livre" 318 318 STR_EMBEDDED_STYLE: "Style intégré" 319 319 STR_OPDS_SERVER_URL: "URL du serveur OPDS" 320 + STR_SCREENSHOT_BUTTON: "Prendre une capture d'écran"
+1
lib/I18n/translations/german.yaml
··· 317 317 STR_BOOK_S_STYLE: "Buch-Stil" 318 318 STR_EMBEDDED_STYLE: "Eingebetteter Stil" 319 319 STR_OPDS_SERVER_URL: "OPDS-Server-URL" 320 + STR_SCREENSHOT_BUTTON: "Screenshot aufnehmen"
+1
lib/I18n/translations/portuguese.yaml
··· 317 317 STR_BOOK_S_STYLE: "Estilo do livro" 318 318 STR_EMBEDDED_STYLE: "Estilo embutido" 319 319 STR_OPDS_SERVER_URL: "URL do servidor OPDS" 320 + STR_SCREENSHOT_BUTTON: "Capturar tela"
+1
lib/I18n/translations/russian.yaml
··· 317 317 STR_BOOK_S_STYLE: "Стиль книги" 318 318 STR_EMBEDDED_STYLE: "Встроенный стиль" 319 319 STR_OPDS_SERVER_URL: "URL OPDS сервера" 320 + STR_SCREENSHOT_BUTTON: "Сделать снимок экрана"
+1
lib/I18n/translations/spanish.yaml
··· 317 317 STR_BOOK_S_STYLE: "Estilo del libro" 318 318 STR_EMBEDDED_STYLE: "Estilo integrado" 319 319 STR_OPDS_SERVER_URL: "URL del servidor OPDS" 320 + STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla"
+1
lib/I18n/translations/swedish.yaml
··· 317 317 STR_BOOK_S_STYLE: "Bokstil" 318 318 STR_EMBEDDED_STYLE: "Inbäddad stil" 319 319 STR_OPDS_SERVER_URL: "OPDS-serveradress" 320 + STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
+15
src/activities/reader/EpubReaderActivity.cpp
··· 17 17 #include "RecentBooksStore.h" 18 18 #include "components/UITheme.h" 19 19 #include "fontIds.h" 20 + #include "util/ScreenshotUtil.h" 20 21 21 22 namespace { 22 23 // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() ··· 431 432 pendingGoHome = true; 432 433 break; 433 434 } 435 + case EpubReaderMenuActivity::MenuAction::SCREENSHOT: { 436 + { 437 + RenderLock lock(*this); 438 + pendingScreenshot = true; 439 + } 440 + exitActivity(); 441 + requestUpdate(); 442 + break; 443 + } 434 444 case EpubReaderMenuActivity::MenuAction::SYNC: { 435 445 if (KOREADER_STORE.hasCredentials()) { 436 446 const int currentPage = section ? section->currentPage : 0; ··· 616 626 renderer.clearFontCache(); 617 627 } 618 628 saveProgress(currentSpineIndex, section->currentPage, section->pageCount); 629 + 630 + if (pendingScreenshot) { 631 + pendingScreenshot = false; 632 + ScreenshotUtil::takeScreenshot(renderer); 633 + } 619 634 } 620 635 621 636 void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
+2 -1
src/activities/reader/EpubReaderActivity.h
··· 20 20 float pendingSpineProgress = 0.0f; 21 21 bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free 22 22 bool pendingGoHome = false; // Defer go home to avoid race condition with display task 23 - bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit 23 + bool pendingScreenshot = false; 24 + bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit 24 25 const std::function<void()> onGoBack; 25 26 const std::function<void()> onGoHome; 26 27
+6 -8
src/activities/reader/EpubReaderMenuActivity.h
··· 12 12 class EpubReaderMenuActivity final : public ActivityWithSubactivity { 13 13 public: 14 14 // Menu actions available from the reader menu. 15 - enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, ROTATE_SCREEN, GO_HOME, SYNC, DELETE_CACHE }; 15 + enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, ROTATE_SCREEN, SCREENSHOT, GO_HOME, SYNC, DELETE_CACHE }; 16 16 17 17 explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, 18 18 const int currentPage, const int totalPages, const int bookProgressPercent, ··· 39 39 }; 40 40 41 41 // Fixed menu layout (order matters for up/down navigation). 42 - const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}, 43 - {MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION}, 44 - {MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}, 45 - {MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}, 46 - {MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}, 47 - {MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}}; 48 - 42 + const std::vector<MenuItem> menuItems = { 43 + {MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}, {MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION}, 44 + {MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}, {MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}, 45 + {MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}, {MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}, 46 + {MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}}; 49 47 int selectedIndex = 0; 50 48 51 49 ButtonNavigator buttonNavigator;
+20 -1
src/main.cpp
··· 31 31 #include "components/UITheme.h" 32 32 #include "fontIds.h" 33 33 #include "util/ButtonNavigator.h" 34 + #include "util/ScreenshotUtil.h" 34 35 35 36 HalDisplay display; 36 37 HalGPIO gpio; ··· 404 405 powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity 405 406 } 406 407 408 + static bool screenshotButtonsReleased = true; 409 + if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.isPressed(HalGPIO::BTN_DOWN)) { 410 + if (screenshotButtonsReleased) { 411 + screenshotButtonsReleased = false; 412 + if (currentActivity) { 413 + Activity::RenderLock lock(*currentActivity); 414 + ScreenshotUtil::takeScreenshot(renderer); 415 + } 416 + } 417 + return; 418 + } else { 419 + screenshotButtonsReleased = true; 420 + } 421 + 407 422 const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs(); 408 423 if (millis() - lastActivityTime >= sleepTimeoutMs) { 409 424 LOG_DBG("SLP", "Auto-sleep triggered after %lu ms of inactivity", sleepTimeoutMs); ··· 413 428 } 414 429 415 430 if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() > SETTINGS.getPowerButtonDuration()) { 431 + // If the screenshot combination is potentially being pressed, don't sleep 432 + if (gpio.isPressed(HalGPIO::BTN_DOWN)) { 433 + return; 434 + } 416 435 enterDeepSleep(); 417 436 // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start 418 437 return; ··· 448 467 delay(10); 449 468 } 450 469 } 451 - } 470 + }
+117
src/util/ScreenshotUtil.cpp
··· 1 + #include "ScreenshotUtil.h" 2 + 3 + #include <Arduino.h> 4 + #include <BitmapHelpers.h> 5 + #include <GfxRenderer.h> 6 + #include <HalStorage.h> 7 + #include <Logging.h> 8 + 9 + #include <string> 10 + 11 + #include "Bitmap.h" // Required for BmpHeader struct definition 12 + 13 + void ScreenshotUtil::takeScreenshot(GfxRenderer& renderer) { 14 + const uint8_t* fb = renderer.getFrameBuffer(); 15 + if (fb) { 16 + String filename_str = "/screenshots/screenshot-" + String(millis()) + ".bmp"; 17 + if (ScreenshotUtil::saveFramebufferAsBmp(filename_str.c_str(), fb, HalDisplay::DISPLAY_WIDTH, 18 + HalDisplay::DISPLAY_HEIGHT)) { 19 + LOG_DBG("SCR", "Screenshot saved to %s", filename_str.c_str()); 20 + } else { 21 + LOG_ERR("SCR", "Failed to save screenshot"); 22 + } 23 + } else { 24 + LOG_ERR("SCR", "Framebuffer not available"); 25 + } 26 + 27 + // Display a border around the screen to indicate a screenshot was taken 28 + if (renderer.storeBwBuffer()) { 29 + renderer.drawRect(6, 6, HalDisplay::DISPLAY_HEIGHT - 12, HalDisplay::DISPLAY_WIDTH - 12, 2, true); 30 + renderer.displayBuffer(); 31 + delay(1000); 32 + renderer.restoreBwBuffer(); 33 + renderer.displayBuffer(HalDisplay::RefreshMode::HALF_REFRESH); 34 + } 35 + } 36 + 37 + bool ScreenshotUtil::saveFramebufferAsBmp(const char* filename, const uint8_t* framebuffer, int width, int height) { 38 + if (!framebuffer) { 39 + return false; 40 + } 41 + 42 + // Note: the width and height, we rotate the image 90d counter-clockwise to match the default display orientation 43 + int phyWidth = height; 44 + int phyHeight = width; 45 + 46 + std::string path(filename); 47 + size_t last_slash = path.find_last_of('/'); 48 + if (last_slash != std::string::npos) { 49 + std::string dir = path.substr(0, last_slash); 50 + if (!Storage.exists(dir.c_str())) { 51 + if (!Storage.mkdir(dir.c_str())) { 52 + return false; 53 + } 54 + } 55 + } 56 + 57 + FsFile file; 58 + if (!Storage.openFileForWrite("SCR", filename, file)) { 59 + LOG_ERR("SCR", "Failed to save screenshot"); 60 + return false; 61 + } 62 + 63 + BmpHeader header; 64 + 65 + createBmpHeader(&header, phyWidth, phyHeight); 66 + 67 + bool write_error = false; 68 + if (file.write(reinterpret_cast<uint8_t*>(&header), sizeof(header)) != sizeof(header)) { 69 + write_error = true; 70 + } 71 + 72 + if (write_error) { 73 + file.close(); 74 + Storage.remove(filename); 75 + return false; 76 + } 77 + 78 + const uint32_t rowSizePadded = (phyWidth + 31) / 32 * 4; 79 + // Max row size for 480px width = 60 bytes; use fixed buffer to avoid VLA 80 + constexpr size_t kMaxRowSize = 64; 81 + if (rowSizePadded > kMaxRowSize) { 82 + LOG_ERR("SCR", "Row size %u exceeds buffer capacity", rowSizePadded); 83 + file.close(); 84 + Storage.remove(filename); 85 + return false; 86 + } 87 + 88 + // rotate the image 90d counter-clockwise on-the-fly while writing to save memory 89 + uint8_t rowBuffer[kMaxRowSize]; 90 + memset(rowBuffer, 0, rowSizePadded); 91 + 92 + for (int outY = 0; outY < phyHeight; outY++) { 93 + for (int outX = 0; outX < phyWidth; outX++) { 94 + // 90d counter-clockwise: source (srcX, srcY) 95 + // BMP rows are bottom-to-top, so outY=0 is the bottom of the displayed image 96 + int srcX = width - 1 - outY; // phyHeight == width 97 + int srcY = phyWidth - 1 - outX; // phyWidth == height 98 + int fbIndex = srcY * (width / 8) + (srcX / 8); 99 + uint8_t pixel = (framebuffer[fbIndex] >> (7 - (srcX % 8))) & 0x01; 100 + rowBuffer[outX / 8] |= pixel << (7 - (outX % 8)); 101 + } 102 + if (file.write(rowBuffer, rowSizePadded) != rowSizePadded) { 103 + write_error = true; 104 + break; 105 + } 106 + memset(rowBuffer, 0, rowSizePadded); // Clear the buffer for the next row 107 + } 108 + 109 + file.close(); 110 + 111 + if (write_error) { 112 + Storage.remove(filename); 113 + return false; 114 + } 115 + 116 + return true; 117 + }
+8
src/util/ScreenshotUtil.h
··· 1 + #pragma once 2 + #include <GfxRenderer.h> 3 + 4 + class ScreenshotUtil { 5 + public: 6 + static void takeScreenshot(GfxRenderer& renderer); 7 + static bool saveFramebufferAsBmp(const char* filename, const uint8_t* framebuffer, int width, int height); 8 + };