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>
This commit is contained in:
Eliz
2026-02-22 04:22:32 +00:00
committed by GitHub
parent 5f5561b684
commit c1fad16e10
19 changed files with 264 additions and 10 deletions

View File

@@ -17,6 +17,7 @@
#include "RecentBooksStore.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/ScreenshotUtil.h"
namespace {
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
@@ -431,6 +432,15 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
pendingGoHome = true;
break;
}
case EpubReaderMenuActivity::MenuAction::SCREENSHOT: {
{
RenderLock lock(*this);
pendingScreenshot = true;
}
exitActivity();
requestUpdate();
break;
}
case EpubReaderMenuActivity::MenuAction::SYNC: {
if (KOREADER_STORE.hasCredentials()) {
const int currentPage = section ? section->currentPage : 0;
@@ -616,6 +626,11 @@ void EpubReaderActivity::render(Activity::RenderLock&& lock) {
renderer.clearFontCache();
}
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
if (pendingScreenshot) {
pendingScreenshot = false;
ScreenshotUtil::takeScreenshot(renderer);
}
}
void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {

View File

@@ -20,7 +20,8 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
float pendingSpineProgress = 0.0f;
bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free
bool pendingGoHome = false; // Defer go home to avoid race condition with display task
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
bool pendingScreenshot = false;
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
const std::function<void()> onGoBack;
const std::function<void()> onGoHome;

View File

@@ -12,7 +12,7 @@
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
public:
// Menu actions available from the reader menu.
enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, ROTATE_SCREEN, GO_HOME, SYNC, DELETE_CACHE };
enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, ROTATE_SCREEN, SCREENSHOT, GO_HOME, SYNC, DELETE_CACHE };
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
const int currentPage, const int totalPages, const int bookProgressPercent,
@@ -39,13 +39,11 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
};
// Fixed menu layout (order matters for up/down navigation).
const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER},
{MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION},
{MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT},
{MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON},
{MenuAction::SYNC, StrId::STR_SYNC_PROGRESS},
{MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}};
const std::vector<MenuItem> menuItems = {
{MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}, {MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION},
{MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}, {MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON},
{MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}, {MenuAction::SYNC, StrId::STR_SYNC_PROGRESS},
{MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}};
int selectedIndex = 0;
ButtonNavigator buttonNavigator;

View File

@@ -31,6 +31,7 @@
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/ButtonNavigator.h"
#include "util/ScreenshotUtil.h"
HalDisplay display;
HalGPIO gpio;
@@ -404,6 +405,20 @@ void loop() {
powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity
}
static bool screenshotButtonsReleased = true;
if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.isPressed(HalGPIO::BTN_DOWN)) {
if (screenshotButtonsReleased) {
screenshotButtonsReleased = false;
if (currentActivity) {
Activity::RenderLock lock(*currentActivity);
ScreenshotUtil::takeScreenshot(renderer);
}
}
return;
} else {
screenshotButtonsReleased = true;
}
const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs();
if (millis() - lastActivityTime >= sleepTimeoutMs) {
LOG_DBG("SLP", "Auto-sleep triggered after %lu ms of inactivity", sleepTimeoutMs);
@@ -413,6 +428,10 @@ void loop() {
}
if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() > SETTINGS.getPowerButtonDuration()) {
// If the screenshot combination is potentially being pressed, don't sleep
if (gpio.isPressed(HalGPIO::BTN_DOWN)) {
return;
}
enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
return;
@@ -448,4 +467,4 @@ void loop() {
delay(10);
}
}
}
}

117
src/util/ScreenshotUtil.cpp Normal file
View File

@@ -0,0 +1,117 @@
#include "ScreenshotUtil.h"
#include <Arduino.h>
#include <BitmapHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <Logging.h>
#include <string>
#include "Bitmap.h" // Required for BmpHeader struct definition
void ScreenshotUtil::takeScreenshot(GfxRenderer& renderer) {
const uint8_t* fb = renderer.getFrameBuffer();
if (fb) {
String filename_str = "/screenshots/screenshot-" + String(millis()) + ".bmp";
if (ScreenshotUtil::saveFramebufferAsBmp(filename_str.c_str(), fb, HalDisplay::DISPLAY_WIDTH,
HalDisplay::DISPLAY_HEIGHT)) {
LOG_DBG("SCR", "Screenshot saved to %s", filename_str.c_str());
} else {
LOG_ERR("SCR", "Failed to save screenshot");
}
} else {
LOG_ERR("SCR", "Framebuffer not available");
}
// Display a border around the screen to indicate a screenshot was taken
if (renderer.storeBwBuffer()) {
renderer.drawRect(6, 6, HalDisplay::DISPLAY_HEIGHT - 12, HalDisplay::DISPLAY_WIDTH - 12, 2, true);
renderer.displayBuffer();
delay(1000);
renderer.restoreBwBuffer();
renderer.displayBuffer(HalDisplay::RefreshMode::HALF_REFRESH);
}
}
bool ScreenshotUtil::saveFramebufferAsBmp(const char* filename, const uint8_t* framebuffer, int width, int height) {
if (!framebuffer) {
return false;
}
// Note: the width and height, we rotate the image 90d counter-clockwise to match the default display orientation
int phyWidth = height;
int phyHeight = width;
std::string path(filename);
size_t last_slash = path.find_last_of('/');
if (last_slash != std::string::npos) {
std::string dir = path.substr(0, last_slash);
if (!Storage.exists(dir.c_str())) {
if (!Storage.mkdir(dir.c_str())) {
return false;
}
}
}
FsFile file;
if (!Storage.openFileForWrite("SCR", filename, file)) {
LOG_ERR("SCR", "Failed to save screenshot");
return false;
}
BmpHeader header;
createBmpHeader(&header, phyWidth, phyHeight);
bool write_error = false;
if (file.write(reinterpret_cast<uint8_t*>(&header), sizeof(header)) != sizeof(header)) {
write_error = true;
}
if (write_error) {
file.close();
Storage.remove(filename);
return false;
}
const uint32_t rowSizePadded = (phyWidth + 31) / 32 * 4;
// Max row size for 480px width = 60 bytes; use fixed buffer to avoid VLA
constexpr size_t kMaxRowSize = 64;
if (rowSizePadded > kMaxRowSize) {
LOG_ERR("SCR", "Row size %u exceeds buffer capacity", rowSizePadded);
file.close();
Storage.remove(filename);
return false;
}
// rotate the image 90d counter-clockwise on-the-fly while writing to save memory
uint8_t rowBuffer[kMaxRowSize];
memset(rowBuffer, 0, rowSizePadded);
for (int outY = 0; outY < phyHeight; outY++) {
for (int outX = 0; outX < phyWidth; outX++) {
// 90d counter-clockwise: source (srcX, srcY)
// BMP rows are bottom-to-top, so outY=0 is the bottom of the displayed image
int srcX = width - 1 - outY; // phyHeight == width
int srcY = phyWidth - 1 - outX; // phyWidth == height
int fbIndex = srcY * (width / 8) + (srcX / 8);
uint8_t pixel = (framebuffer[fbIndex] >> (7 - (srcX % 8))) & 0x01;
rowBuffer[outX / 8] |= pixel << (7 - (outX % 8));
}
if (file.write(rowBuffer, rowSizePadded) != rowSizePadded) {
write_error = true;
break;
}
memset(rowBuffer, 0, rowSizePadded); // Clear the buffer for the next row
}
file.close();
if (write_error) {
Storage.remove(filename);
return false;
}
return true;
}

View File

@@ -0,0 +1,8 @@
#pragma once
#include <GfxRenderer.h>
class ScreenshotUtil {
public:
static void takeScreenshot(GfxRenderer& renderer);
static bool saveFramebufferAsBmp(const char* filename, const uint8_t* framebuffer, int width, int height);
};