Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
6.4 kB · 187 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188#include "ScreenshotUtil.h"
#include <Arduino.h>#include <BitmapHelpers.h>#include <FsHelpers.h>#include <GfxRenderer.h>#include <HalStorage.h>#include <Logging.h>
#include <cstring>#include <string>
#include "Bitmap.h" // Required for BmpHeader struct definition#include "activities/Activity.h"
void ScreenshotUtil::buildFilename(const ScreenshotInfo& info, char* buf, size_t bufSize) { const unsigned long ts = millis();
if (info.readerType == ScreenshotInfo::ReaderType::None || info.title[0] == '\0') { snprintf(buf, bufSize, "/screenshots/screenshot-%lu.bmp", ts); return; }
char sanitizedTitle[64]; FsHelpers::sanitizePathComponentForFat32(info.title, sanitizedTitle, sizeof(sanitizedTitle)); if (sanitizedTitle[0] == '\0') { snprintf(buf, bufSize, "/screenshots/screenshot-%lu.bmp", ts); return; }
int pct = info.progressPercent; if (pct < 0) pct = 0; if (pct > 100) pct = 100;
// Display spine index as 1-based for user-facing filenames const int chapterNum = info.spineIndex + 1;
if (info.readerType == ScreenshotInfo::ReaderType::Epub && info.spineIndex >= 0) { snprintf(buf, bufSize, "/screenshots/%s/%s_ch%d_p%d_%dpct_%lu.bmp", sanitizedTitle, sanitizedTitle, chapterNum, info.currentPage, pct, ts); } else { snprintf(buf, bufSize, "/screenshots/%s/%s_p%d_%dpct_%lu.bmp", sanitizedTitle, sanitizedTitle, info.currentPage, pct, ts); }
// Truncate title if total path exceeds FAT32 limit if (strlen(buf) > 255) { size_t titleLen = strlen(sanitizedTitle); size_t overhead = strlen(buf) - 2 * titleLen; if (overhead < 255) { size_t maxTitleLen = (255 - overhead) / 2; // Walk back to a valid UTF-8 boundary to avoid corrupting multibyte characters while (maxTitleLen > 0 && (sanitizedTitle[maxTitleLen] & 0xC0) == 0x80) { maxTitleLen--; } sanitizedTitle[maxTitleLen] = '\0'; if (info.readerType == ScreenshotInfo::ReaderType::Epub && info.spineIndex >= 0) { snprintf(buf, bufSize, "/screenshots/%s/%s_ch%d_p%d_%dpct_%lu.bmp", sanitizedTitle, sanitizedTitle, chapterNum, info.currentPage, pct, ts); } else { snprintf(buf, bufSize, "/screenshots/%s/%s_p%d_%dpct_%lu.bmp", sanitizedTitle, sanitizedTitle, info.currentPage, pct, ts); } } else { snprintf(buf, bufSize, "/screenshots/screenshot-%lu.bmp", ts); } }}
void ScreenshotUtil::takeScreenshot(GfxRenderer& renderer) { const uint8_t* fb = renderer.getFrameBuffer(); if (!fb) { LOG_ERR("SCR", "Framebuffer not available"); return; }
ScreenshotInfo info = activityManager.getScreenshotInfo(); char filename[256]; buildFilename(info, filename, sizeof(filename));
bool saved = saveFramebufferAsBmp(filename, fb, renderer.getDisplayWidth(), renderer.getDisplayHeight()); if (saved) { LOG_DBG("SCR", "Screenshot saved to %s", filename); } else { LOG_ERR("SCR", "Failed to save screenshot"); return; }
// Display a border around the screen to indicate a screenshot was taken if (renderer.storeBwBuffer()) { int marginTop, marginRight, marginBottom, marginLeft; renderer.getOrientedViewableTRBL(&marginTop, &marginRight, &marginBottom, &marginLeft); int width = renderer.getScreenWidth() - marginLeft - marginRight - 1; int height = renderer.getScreenHeight() - marginTop - marginBottom - 1; // Add extra margin to the border to make it more visible renderer.drawRect(marginLeft + 1, marginTop + 1, width - 2, height - 2, 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; } } }
HalFile file; if (!Storage.openFileForWrite("SCR", filename, file)) { LOG_ERR("SCR", "Failed to save screenshot"); return false; }
BmpHeader header;
createBmpHeader(&header, phyWidth, phyHeight, BmpRowOrder::BottomUp);
bool write_error = false; if (file.write(reinterpret_cast<uint8_t*>(&header), sizeof(header)) != sizeof(header)) { write_error = true; }
if (write_error) { // Explicitly close() file before calling Storage.remove() file.close(); Storage.remove(filename); return false; }
const uint32_t rowSizePadded = (phyWidth + 31) / 32 * 4; // Max row size for 528px height (X3) after rotation = 68 bytes; use fixed buffer to avoid VLA constexpr size_t kMaxRowSize = 68; if (rowSizePadded > kMaxRowSize) { LOG_ERR("SCR", "Row size %u exceeds buffer capacity", rowSizePadded); // Explicitly close() file before calling Storage.remove() 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 }
// Explicitly close() file before calling Storage.remove() file.close();
if (write_error) { Storage.remove(filename); return false; }
return true;}