mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Compare commits
9 Commits
b6488f3739
...
871c702084
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
871c702084 | ||
|
|
e5c0ddc9fa | ||
|
|
b1dcb7733b | ||
|
|
0d82b03981 | ||
|
|
5a97334ace | ||
|
|
436dc9e593 | ||
|
|
85b7e8124d | ||
|
|
6d6559022c | ||
|
|
5671b05d04 |
14
README.md
14
README.md
@ -95,6 +95,20 @@ Connect your Xteink X4 to your computer via USB-C and run the following command.
|
|||||||
```sh
|
```sh
|
||||||
pio run --target upload
|
pio run --target upload
|
||||||
```
|
```
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
After flashing the new features, it’s recommended to capture detailed logs from the serial port.
|
||||||
|
|
||||||
|
First, make sure all required Python packages are installed:
|
||||||
|
|
||||||
|
```python
|
||||||
|
python3 -m pip install serial colorama matplotlib
|
||||||
|
```
|
||||||
|
after that run the script:
|
||||||
|
```sh
|
||||||
|
python3 scripts/debugging_monitor.py
|
||||||
|
```
|
||||||
|
This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS.
|
||||||
|
|
||||||
## Internals
|
## Internals
|
||||||
|
|
||||||
|
|||||||
@ -428,28 +428,28 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
|
std::string Epub::getCoverHomeBmpPath() const { return cachePath + "/cover_home.bmp"; }
|
||||||
|
|
||||||
bool Epub::generateThumbBmp() const {
|
bool Epub::generateCoverHomeBmp() const {
|
||||||
// Already generated, return true
|
// Already generated, return true
|
||||||
if (SdMan.exists(getThumbBmpPath().c_str())) {
|
if (SdMan.exists(getCoverHomeBmpPath().c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] Cannot generate thumb BMP, cache not loaded\n", millis());
|
Serial.printf("[%lu] [EBP] Cannot generate home BMP, cache not loaded\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||||
if (coverImageHref.empty()) {
|
if (coverImageHref.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No known cover image for thumbnail\n", millis());
|
Serial.printf("[%lu] [EBP] No known cover image for home screen\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||||
Serial.printf("[%lu] [EBP] Generating thumb BMP from JPG cover image\n", millis());
|
Serial.printf("[%lu] [EBP] Generating home BMP from JPG cover image\n", millis());
|
||||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||||
|
|
||||||
FsFile coverJpg;
|
FsFile coverJpg;
|
||||||
@ -463,30 +463,50 @@ bool Epub::generateThumbBmp() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile thumbBmp;
|
FsFile homeBmp;
|
||||||
if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) {
|
if (!SdMan.openFileForWrite("EBP", getCoverHomeBmpPath(), homeBmp)) {
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Use smaller target size for Continue Reading card (half of screen: 240x400)
|
|
||||||
|
// For home screen, use 400px height with proportional width for optimal performance
|
||||||
// Generate 1-bit BMP for fast home screen rendering (no gray passes needed)
|
// Generate 1-bit BMP for fast home screen rendering (no gray passes needed)
|
||||||
constexpr int THUMB_TARGET_WIDTH = 240;
|
constexpr int HOME_TARGET_HEIGHT = 400;
|
||||||
constexpr int THUMB_TARGET_HEIGHT = 400;
|
|
||||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
|
FsFile tempJpg;
|
||||||
THUMB_TARGET_HEIGHT);
|
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, tempJpg)) {
|
||||||
|
coverJpg.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First get JPEG dimensions to calculate proper width
|
||||||
|
int jpegWidth, jpegHeight;
|
||||||
|
if (!JpegToBmpConverter::getJpegDimensions(tempJpg, &jpegWidth, &jpegHeight)) {
|
||||||
|
Serial.printf("[%lu] [EBP] Failed to get JPEG dimensions for home cover\n", millis());
|
||||||
|
coverJpg.close();
|
||||||
|
tempJpg.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tempJpg.close();
|
||||||
|
|
||||||
|
// Calculate proportional width for 400px height
|
||||||
|
const int targetWidth = (400 * jpegWidth) / jpegHeight;
|
||||||
|
|
||||||
|
const bool success =
|
||||||
|
JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, homeBmp, targetWidth, HOME_TARGET_HEIGHT);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
thumbBmp.close();
|
homeBmp.close();
|
||||||
SdMan.remove(coverJpgTempPath.c_str());
|
SdMan.remove(coverJpgTempPath.c_str());
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis());
|
Serial.printf("[%lu] [EBP] Failed to generate home BMP from JPG cover image\n", millis());
|
||||||
SdMan.remove(getThumbBmpPath().c_str());
|
SdMan.remove(getCoverHomeBmpPath().c_str());
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(),
|
Serial.printf("[%lu] [EBP] Generated home BMP from JPG cover image, success: %s\n", millis(),
|
||||||
success ? "yes" : "no");
|
success ? "yes" : "no");
|
||||||
return success;
|
return success;
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping thumbnail\n", millis());
|
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping home screen\n", millis());
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -47,8 +47,9 @@ class Epub {
|
|||||||
const std::string& getLanguage() const;
|
const std::string& getLanguage() const;
|
||||||
std::string getCoverBmpPath(bool cropped = false) const;
|
std::string getCoverBmpPath(bool cropped = false) const;
|
||||||
bool generateCoverBmp(bool cropped = false) const;
|
bool generateCoverBmp(bool cropped = false) const;
|
||||||
std::string getThumbBmpPath() const;
|
// Home screen support (optimized 400px height covers for Continue Reading card)
|
||||||
bool generateThumbBmp() const;
|
std::string getCoverHomeBmpPath() const;
|
||||||
|
bool generateCoverHomeBmp() const;
|
||||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||||
bool trailingNullByte = false) const;
|
bool trailingNullByte = false) const;
|
||||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||||
|
|||||||
@ -415,13 +415,21 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
|
|||||||
|
|
||||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||||
const EpdFontFamily::Style style) const {
|
const EpdFontFamily::Style style) const {
|
||||||
|
if (!text || maxWidth <= 0) return "";
|
||||||
|
|
||||||
std::string item = text;
|
std::string item = text;
|
||||||
int itemWidth = getTextWidth(fontId, item.c_str(), style);
|
const char* ellipsis = "...";
|
||||||
while (itemWidth > maxWidth && item.length() > 8) {
|
int textWidth = getTextWidth(fontId, item.c_str(), style);
|
||||||
item.replace(item.length() - 5, 5, "...");
|
if (textWidth <= maxWidth) {
|
||||||
itemWidth = getTextWidth(fontId, item.c_str(), style);
|
// Text fits, return as is
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
return item;
|
|
||||||
|
while (!item.empty() && getTextWidth(fontId, (item + ellipsis).c_str(), style) >= maxWidth) {
|
||||||
|
utf8RemoveLastChar(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.empty() ? ellipsis : item + ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
|
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
|
||||||
|
|||||||
@ -569,3 +569,32 @@ bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print
|
|||||||
int targetMaxHeight) {
|
int targetMaxHeight) {
|
||||||
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true);
|
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get JPEG dimensions without full conversion
|
||||||
|
bool JpegToBmpConverter::getJpegDimensions(FsFile& jpegFile, int* width, int* height) {
|
||||||
|
// Reset file position to beginning
|
||||||
|
if (!jpegFile.seek(0)) {
|
||||||
|
Serial.printf("[%lu] [JPG] Failed to seek to beginning of JPEG file\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize JPEG decoder
|
||||||
|
pjpeg_image_info_t imageInfo = {};
|
||||||
|
JpegReadContext context{jpegFile, {}, 0, 0};
|
||||||
|
|
||||||
|
const int decodeStatus = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, false);
|
||||||
|
if (decodeStatus != 0) {
|
||||||
|
Serial.printf("[%lu] [JPG] pjpeg_decode_init failed with status %d\n", millis(), decodeStatus);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get dimensions from image info
|
||||||
|
*width = imageInfo.m_width;
|
||||||
|
*height = imageInfo.m_height;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [JPG] Read JPEG dimensions: %dx%d\n", millis(), *width, *height);
|
||||||
|
|
||||||
|
// Reset file position after reading
|
||||||
|
jpegFile.seek(0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@ -16,4 +16,6 @@ class JpegToBmpConverter {
|
|||||||
static bool jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
static bool jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
||||||
// Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering
|
// Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering
|
||||||
static bool jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
static bool jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
||||||
|
// Extract JPEG dimensions without loading full image
|
||||||
|
static bool getJpegDimensions(FsFile& jpegFile, int* width, int* height);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -29,3 +29,20 @@ uint32_t utf8NextCodepoint(const unsigned char** string) {
|
|||||||
|
|
||||||
return cp;
|
return cp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t utf8RemoveLastChar(std::string& str) {
|
||||||
|
if (str.empty()) return 0;
|
||||||
|
size_t pos = str.size() - 1;
|
||||||
|
while (pos > 0 && (static_cast<unsigned char>(str[pos]) & 0xC0) == 0x80) {
|
||||||
|
--pos;
|
||||||
|
}
|
||||||
|
str.resize(pos);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate string by removing N UTF-8 characters from the end
|
||||||
|
void utf8TruncateChars(std::string& str, const size_t numChars) {
|
||||||
|
for (size_t i = 0; i < numChars && !str.empty(); ++i) {
|
||||||
|
utf8RemoveLastChar(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
#define REPLACEMENT_GLYPH 0xFFFD
|
#define REPLACEMENT_GLYPH 0xFFFD
|
||||||
|
|
||||||
uint32_t utf8NextCodepoint(const unsigned char** string);
|
uint32_t utf8NextCodepoint(const unsigned char** string);
|
||||||
|
// Remove the last UTF-8 codepoint from a std::string and return the new size.
|
||||||
|
size_t utf8RemoveLastChar(std::string& str);
|
||||||
|
// Truncate string by removing N UTF-8 codepoints from the end.
|
||||||
|
void utf8TruncateChars(std::string& str, size_t numChars);
|
||||||
|
|||||||
@ -301,11 +301,11 @@ bool Xtc::generateCoverBmp() const {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
|
std::string Xtc::getCoverHomeBmpPath() const { return cachePath + "/cover_home.bmp"; }
|
||||||
|
|
||||||
bool Xtc::generateThumbBmp() const {
|
bool Xtc::generateCoverHomeBmp() const {
|
||||||
// Already generated
|
// Already generated
|
||||||
if (SdMan.exists(getThumbBmpPath().c_str())) {
|
if (SdMan.exists(getCoverHomeBmpPath().c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,43 +332,18 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
// Get bit depth
|
// Get bit depth
|
||||||
const uint8_t bitDepth = parser->getBitDepth();
|
const uint8_t bitDepth = parser->getBitDepth();
|
||||||
|
|
||||||
// Calculate target dimensions for thumbnail (fit within 240x400 Continue Reading card)
|
// For home screen, use 400px height with proportional width for optimal performance
|
||||||
constexpr int THUMB_TARGET_WIDTH = 240;
|
// Generate 1-bit BMP for fast home screen rendering (no gray passes needed)
|
||||||
constexpr int THUMB_TARGET_HEIGHT = 400;
|
constexpr int HOME_TARGET_HEIGHT = 400;
|
||||||
|
|
||||||
// Calculate scale factor
|
// Calculate proportional width for 400px height
|
||||||
float scaleX = static_cast<float>(THUMB_TARGET_WIDTH) / pageInfo.width;
|
const uint16_t targetWidth = static_cast<uint16_t>((HOME_TARGET_HEIGHT * pageInfo.width) / pageInfo.height);
|
||||||
float scaleY = static_cast<float>(THUMB_TARGET_HEIGHT) / pageInfo.height;
|
|
||||||
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
|
||||||
|
|
||||||
// Only scale down, never up
|
Serial.printf("[%lu] [XTC] Generating home BMP: %dx%d -> %dx%d\n", millis(), pageInfo.width, pageInfo.height,
|
||||||
if (scale >= 1.0f) {
|
targetWidth, HOME_TARGET_HEIGHT);
|
||||||
// Page is already small enough, just use cover.bmp
|
|
||||||
// Copy cover.bmp to thumb.bmp
|
|
||||||
if (generateCoverBmp()) {
|
|
||||||
FsFile src, dst;
|
|
||||||
if (SdMan.openFileForRead("XTC", getCoverBmpPath(), src)) {
|
|
||||||
if (SdMan.openFileForWrite("XTC", getThumbBmpPath(), dst)) {
|
|
||||||
uint8_t buffer[512];
|
|
||||||
while (src.available()) {
|
|
||||||
size_t bytesRead = src.read(buffer, sizeof(buffer));
|
|
||||||
dst.write(buffer, bytesRead);
|
|
||||||
}
|
|
||||||
dst.close();
|
|
||||||
}
|
|
||||||
src.close();
|
|
||||||
}
|
|
||||||
Serial.printf("[%lu] [XTC] Copied cover to thumb (no scaling needed)\n", millis());
|
|
||||||
return SdMan.exists(getThumbBmpPath().c_str());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t thumbWidth = static_cast<uint16_t>(pageInfo.width * scale);
|
|
||||||
uint16_t thumbHeight = static_cast<uint16_t>(pageInfo.height * scale);
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)\n", millis(), pageInfo.width,
|
Serial.printf("[%lu] [XTC] Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)\n", millis(), pageInfo.width,
|
||||||
pageInfo.height, thumbWidth, thumbHeight, scale);
|
pageInfo.height, targetWidth, HOME_TARGET_HEIGHT);
|
||||||
|
|
||||||
// Allocate buffer for page data
|
// Allocate buffer for page data
|
||||||
size_t bitmapSize;
|
size_t bitmapSize;
|
||||||
@ -393,15 +368,15 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
|
|
||||||
// Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes)
|
// Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes)
|
||||||
FsFile thumbBmp;
|
FsFile thumbBmp;
|
||||||
if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(), thumbBmp)) {
|
if (!SdMan.openFileForWrite("XTC", getCoverHomeBmpPath(), thumbBmp)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis());
|
Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis());
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write 1-bit BMP header for fast home screen rendering
|
// Write 1-bit BMP header for fast home screen rendering
|
||||||
const uint32_t rowSize = (thumbWidth + 31) / 32 * 4; // 1 bit per pixel, aligned to 4 bytes
|
const uint32_t rowSize = (targetWidth + 31) / 32 * 4; // 1 bit per pixel, aligned to 4 bytes
|
||||||
const uint32_t imageSize = rowSize * thumbHeight;
|
const uint32_t imageSize = rowSize * HOME_TARGET_HEIGHT;
|
||||||
const uint32_t fileSize = 14 + 40 + 8 + imageSize; // 8 bytes for 2-color palette
|
const uint32_t fileSize = 14 + 40 + 8 + imageSize; // 8 bytes for 2-color palette
|
||||||
|
|
||||||
// File header
|
// File header
|
||||||
@ -416,9 +391,9 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
// DIB header
|
// DIB header
|
||||||
uint32_t dibHeaderSize = 40;
|
uint32_t dibHeaderSize = 40;
|
||||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
|
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
|
||||||
int32_t widthVal = thumbWidth;
|
int32_t widthVal = targetWidth;
|
||||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&widthVal), 4);
|
thumbBmp.write(reinterpret_cast<const uint8_t*>(&widthVal), 4);
|
||||||
int32_t heightVal = -static_cast<int32_t>(thumbHeight); // Negative for top-down
|
int32_t heightVal = -static_cast<int32_t>(HOME_TARGET_HEIGHT); // Negative for top-down
|
||||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&heightVal), 4);
|
thumbBmp.write(reinterpret_cast<const uint8_t*>(&heightVal), 4);
|
||||||
uint16_t planes = 1;
|
uint16_t planes = 1;
|
||||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
|
thumbBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
|
||||||
@ -451,8 +426,8 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed-point scale factor (16.16)
|
// Fixed-point scale factor (16.16) - scale to fit 400px height
|
||||||
uint32_t scaleInv_fp = static_cast<uint32_t>(65536.0f / scale);
|
uint32_t scaleInv_fp = static_cast<uint32_t>(65536.0f * static_cast<float>(pageInfo.height) / HOME_TARGET_HEIGHT);
|
||||||
|
|
||||||
// Pre-calculate plane info for 2-bit mode
|
// Pre-calculate plane info for 2-bit mode
|
||||||
const size_t planeSize = (bitDepth == 2) ? ((static_cast<size_t>(pageInfo.width) * pageInfo.height + 7) / 8) : 0;
|
const size_t planeSize = (bitDepth == 2) ? ((static_cast<size_t>(pageInfo.width) * pageInfo.height + 7) / 8) : 0;
|
||||||
@ -461,7 +436,7 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
const size_t colBytes = (bitDepth == 2) ? ((pageInfo.height + 7) / 8) : 0;
|
const size_t colBytes = (bitDepth == 2) ? ((pageInfo.height + 7) / 8) : 0;
|
||||||
const size_t srcRowBytes = (bitDepth == 1) ? ((pageInfo.width + 7) / 8) : 0;
|
const size_t srcRowBytes = (bitDepth == 1) ? ((pageInfo.width + 7) / 8) : 0;
|
||||||
|
|
||||||
for (uint16_t dstY = 0; dstY < thumbHeight; dstY++) {
|
for (uint16_t dstY = 0; dstY < HOME_TARGET_HEIGHT; dstY++) {
|
||||||
memset(rowBuffer, 0xFF, rowSize); // Start with all white (bit 1)
|
memset(rowBuffer, 0xFF, rowSize); // Start with all white (bit 1)
|
||||||
|
|
||||||
// Calculate source Y range with bounds checking
|
// Calculate source Y range with bounds checking
|
||||||
@ -472,7 +447,7 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
if (srcYEnd <= srcYStart) srcYEnd = srcYStart + 1;
|
if (srcYEnd <= srcYStart) srcYEnd = srcYStart + 1;
|
||||||
if (srcYEnd > pageInfo.height) srcYEnd = pageInfo.height;
|
if (srcYEnd > pageInfo.height) srcYEnd = pageInfo.height;
|
||||||
|
|
||||||
for (uint16_t dstX = 0; dstX < thumbWidth; dstX++) {
|
for (uint16_t dstX = 0; dstX < targetWidth; dstX++) {
|
||||||
// Calculate source X range with bounds checking
|
// Calculate source X range with bounds checking
|
||||||
uint32_t srcXStart = (static_cast<uint32_t>(dstX) * scaleInv_fp) >> 16;
|
uint32_t srcXStart = (static_cast<uint32_t>(dstX) * scaleInv_fp) >> 16;
|
||||||
uint32_t srcXEnd = (static_cast<uint32_t>(dstX + 1) * scaleInv_fp) >> 16;
|
uint32_t srcXEnd = (static_cast<uint32_t>(dstX + 1) * scaleInv_fp) >> 16;
|
||||||
@ -557,8 +532,8 @@ bool Xtc::generateThumbBmp() const {
|
|||||||
thumbBmp.close();
|
thumbBmp.close();
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Generated thumb BMP (%dx%d): %s\n", millis(), thumbWidth, thumbHeight,
|
Serial.printf("[%lu] [XTC] Generated home BMP (%dx%d): %s\n", millis(), targetWidth, HOME_TARGET_HEIGHT,
|
||||||
getThumbBmpPath().c_str());
|
getCoverHomeBmpPath().c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -63,9 +63,9 @@ class Xtc {
|
|||||||
// Cover image support (for sleep screen)
|
// Cover image support (for sleep screen)
|
||||||
std::string getCoverBmpPath() const;
|
std::string getCoverBmpPath() const;
|
||||||
bool generateCoverBmp() const;
|
bool generateCoverBmp() const;
|
||||||
// Thumbnail support (for Continue Reading card)
|
// Home screen support (optimized 400px height covers for Continue Reading card)
|
||||||
std::string getThumbBmpPath() const;
|
std::string getCoverHomeBmpPath() const;
|
||||||
bool generateThumbBmp() const;
|
bool generateCoverHomeBmp() const;
|
||||||
|
|
||||||
// Page access
|
// Page access
|
||||||
uint32_t getPageCount() const;
|
uint32_t getPageCount() const;
|
||||||
|
|||||||
214
scripts/debugging_monitor.py
Executable file
214
scripts/debugging_monitor.py
Executable file
@ -0,0 +1,214 @@
|
|||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
from collections import deque
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Try to import potentially missing packages
|
||||||
|
try:
|
||||||
|
import serial
|
||||||
|
from colorama import init, Fore, Style
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.animation as animation
|
||||||
|
except ImportError as e:
|
||||||
|
missing_package = e.name
|
||||||
|
print("\n" + "!" * 50)
|
||||||
|
print(f" Error: The required package '{missing_package}' is not installed.")
|
||||||
|
print("!" * 50)
|
||||||
|
|
||||||
|
print(f"\nTo fix this, please run the following command in your terminal:\n")
|
||||||
|
|
||||||
|
install_cmd = "pip install "
|
||||||
|
packages = []
|
||||||
|
if 'serial' in str(e): packages.append("pyserial")
|
||||||
|
if 'colorama' in str(e): packages.append("colorama")
|
||||||
|
if 'matplotlib' in str(e): packages.append("matplotlib")
|
||||||
|
|
||||||
|
print(f" {install_cmd}{' '.join(packages)}")
|
||||||
|
|
||||||
|
print("\nExiting...")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Global Variables for Data Sharing ---
|
||||||
|
# Store last 50 data points
|
||||||
|
MAX_POINTS = 50
|
||||||
|
time_data = deque(maxlen=MAX_POINTS)
|
||||||
|
free_mem_data = deque(maxlen=MAX_POINTS)
|
||||||
|
total_mem_data = deque(maxlen=MAX_POINTS)
|
||||||
|
data_lock = threading.Lock() # Prevent reading while writing
|
||||||
|
|
||||||
|
# Initialize colors
|
||||||
|
init(autoreset=True)
|
||||||
|
|
||||||
|
def get_color_for_line(line):
|
||||||
|
"""
|
||||||
|
Classify log lines by type and assign appropriate colors.
|
||||||
|
"""
|
||||||
|
line_upper = line.upper()
|
||||||
|
|
||||||
|
if any(keyword in line_upper for keyword in ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"]):
|
||||||
|
return Fore.RED
|
||||||
|
if "[MEM]" in line_upper or "FREE:" in line_upper:
|
||||||
|
return Fore.CYAN
|
||||||
|
if any(keyword in line_upper for keyword in ["[GFX]", "[ERS]", "DISPLAY", "RAM WRITE", "RAM COMPLETE", "REFRESH", "POWERING ON", "FRAME BUFFER", "LUT"]):
|
||||||
|
return Fore.MAGENTA
|
||||||
|
if any(keyword in line_upper for keyword in ["[EBP]", "[BMC]", "[ZIP]", "[PARSER]", "[EHP]", "LOADING EPUB", "CACHE", "DECOMPRESSED", "PARSING"]):
|
||||||
|
return Fore.GREEN
|
||||||
|
if "[ACT]" in line_upper or "ENTERING ACTIVITY" in line_upper or "EXITING ACTIVITY" in line_upper:
|
||||||
|
return Fore.YELLOW
|
||||||
|
if any(keyword in line_upper for keyword in ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"]):
|
||||||
|
return Fore.BLUE
|
||||||
|
if any(keyword in line_upper for keyword in ["[CPS]", "SETTINGS", "[CLEAR_CACHE]"]):
|
||||||
|
return Fore.LIGHTYELLOW_EX
|
||||||
|
if any(keyword in line_upper for keyword in ["ESP-ROM", "BUILD:", "RST:", "BOOT:", "SPIWP:", "MODE:", "LOAD:", "ENTRY", "[SD]", "STARTING CROSSPOINT", "VERSION"]):
|
||||||
|
return Fore.LIGHTBLACK_EX
|
||||||
|
if "[RBS]" in line_upper:
|
||||||
|
return Fore.LIGHTCYAN_EX
|
||||||
|
if "[KRS]" in line_upper:
|
||||||
|
return Fore.LIGHTMAGENTA_EX
|
||||||
|
if any(keyword in line_upper for keyword in ["EINKDISPLAY:", "STATIC FRAME", "INITIALIZING", "SPI INITIALIZED", "GPIO PINS", "RESETTING", "SSD1677", "E-INK"]):
|
||||||
|
return Fore.LIGHTMAGENTA_EX
|
||||||
|
if any(keyword in line_upper for keyword in ["[FNS]", "FOOTNOTE"]):
|
||||||
|
return Fore.LIGHTGREEN_EX
|
||||||
|
if any(keyword in line_upper for keyword in ["[CHAP]", "[OPDS]", "[COF]"]):
|
||||||
|
return Fore.LIGHTYELLOW_EX
|
||||||
|
|
||||||
|
return Fore.WHITE
|
||||||
|
|
||||||
|
def parse_memory_line(line):
|
||||||
|
"""
|
||||||
|
Extracts Free and Total bytes from the specific log line.
|
||||||
|
Format: [MEM] Free: 196344 bytes, Total: 226412 bytes, Min Free: 112620 bytes
|
||||||
|
"""
|
||||||
|
# Regex to find 'Free: <digits>' and 'Total: <digits>'
|
||||||
|
match = re.search(r"Free:\s*(\d+).*Total:\s*(\d+)", line)
|
||||||
|
if match:
|
||||||
|
try:
|
||||||
|
free_bytes = int(match.group(1))
|
||||||
|
total_bytes = int(match.group(2))
|
||||||
|
return free_bytes, total_bytes
|
||||||
|
except ValueError:
|
||||||
|
return None, None
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def serial_worker(port, baud):
|
||||||
|
"""
|
||||||
|
Runs in a background thread. Handles reading serial, printing to console,
|
||||||
|
and updating the data lists.
|
||||||
|
"""
|
||||||
|
print(f"{Fore.CYAN}--- Opening {port} at {baud} baud ---{Style.RESET_ALL}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
ser = serial.Serial(port, baud, timeout=0.1)
|
||||||
|
ser.dtr = False
|
||||||
|
ser.rts = False
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"{Fore.RED}Error opening port: {e}{Style.RESET_ALL}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
raw_data = ser.readline().decode('utf-8', errors='replace')
|
||||||
|
|
||||||
|
if not raw_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
clean_line = raw_data.strip()
|
||||||
|
if not clean_line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add PC timestamp
|
||||||
|
pc_time = datetime.now().strftime("%H:%M:%S")
|
||||||
|
formatted_line = re.sub(r"^\[\d+\]", f"[{pc_time}]", clean_line)
|
||||||
|
|
||||||
|
# Check for Memory Line
|
||||||
|
if "[MEM]" in formatted_line:
|
||||||
|
free_val, total_val = parse_memory_line(formatted_line)
|
||||||
|
if free_val is not None:
|
||||||
|
with data_lock:
|
||||||
|
time_data.append(pc_time)
|
||||||
|
free_mem_data.append(free_val / 1024) # Convert to KB
|
||||||
|
total_mem_data.append(total_val / 1024) # Convert to KB
|
||||||
|
|
||||||
|
# Print to console
|
||||||
|
line_color = get_color_for_line(formatted_line)
|
||||||
|
print(f"{line_color}{formatted_line}")
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
print(f"{Fore.RED}Device disconnected.{Style.RESET_ALL}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
# If thread is killed violently (e.g. main exit), silence errors
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if 'ser' in locals() and ser.is_open:
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
def update_graph(frame):
|
||||||
|
"""
|
||||||
|
Called by Matplotlib animation to redraw the chart.
|
||||||
|
"""
|
||||||
|
with data_lock:
|
||||||
|
if not time_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Convert deques to lists for plotting
|
||||||
|
x = list(time_data)
|
||||||
|
y_free = list(free_mem_data)
|
||||||
|
y_total = list(total_mem_data)
|
||||||
|
|
||||||
|
plt.cla() # Clear axis
|
||||||
|
|
||||||
|
# Plot Total RAM
|
||||||
|
plt.plot(x, y_total, label='Total RAM (KB)', color='red', linestyle='--')
|
||||||
|
|
||||||
|
# Plot Free RAM
|
||||||
|
plt.plot(x, y_free, label='Free RAM (KB)', color='green', marker='o')
|
||||||
|
|
||||||
|
# Fill area under Free RAM
|
||||||
|
plt.fill_between(x, y_free, color='green', alpha=0.1)
|
||||||
|
|
||||||
|
plt.title("ESP32 Memory Monitor")
|
||||||
|
plt.ylabel("Memory (KB)")
|
||||||
|
plt.xlabel("Time")
|
||||||
|
plt.legend(loc='upper left')
|
||||||
|
plt.grid(True, linestyle=':', alpha=0.6)
|
||||||
|
|
||||||
|
# Rotate date labels
|
||||||
|
plt.xticks(rotation=45, ha='right')
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="ESP32 Monitor with Graph")
|
||||||
|
parser.add_argument("port", nargs="?", default="/dev/ttyACM0", help="Serial port")
|
||||||
|
parser.add_argument("--baud", type=int, default=115200, help="Baud rate")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 1. Start the Serial Reader in a separate thread
|
||||||
|
# Daemon=True means this thread dies when the main program closes
|
||||||
|
t = threading.Thread(target=serial_worker, args=(args.port, args.baud), daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
# 2. Set up the Graph (Main Thread)
|
||||||
|
try:
|
||||||
|
plt.style.use('light_background')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
fig = plt.figure(figsize=(10, 6))
|
||||||
|
|
||||||
|
# Update graph every 1000ms
|
||||||
|
ani = animation.FuncAnimation(fig, update_graph, interval=1000)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"{Fore.YELLOW}Starting Graph Window... (Close window to exit){Style.RESET_ALL}")
|
||||||
|
plt.show()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\n{Fore.YELLOW}Exiting...{Style.RESET_ALL}")
|
||||||
|
plt.close('all') # Force close any lingering plot windows
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -4,6 +4,7 @@
|
|||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
|
#include <Utf8.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -59,8 +60,8 @@ void HomeActivity::onEnter() {
|
|||||||
lastBookAuthor = std::string(epub.getAuthor());
|
lastBookAuthor = std::string(epub.getAuthor());
|
||||||
}
|
}
|
||||||
// Try to generate thumbnail image for Continue Reading card
|
// Try to generate thumbnail image for Continue Reading card
|
||||||
if (epub.generateThumbBmp()) {
|
if (epub.generateCoverHomeBmp()) {
|
||||||
coverBmpPath = epub.getThumbBmpPath();
|
coverBmpPath = epub.getCoverHomeBmpPath();
|
||||||
hasCoverImage = true;
|
hasCoverImage = true;
|
||||||
}
|
}
|
||||||
} else if (StringUtils::checkFileExtension(lastBookTitle, ".xtch") ||
|
} else if (StringUtils::checkFileExtension(lastBookTitle, ".xtch") ||
|
||||||
@ -75,8 +76,8 @@ void HomeActivity::onEnter() {
|
|||||||
lastBookAuthor = std::string(xtc.getAuthor());
|
lastBookAuthor = std::string(xtc.getAuthor());
|
||||||
}
|
}
|
||||||
// Try to generate thumbnail image for Continue Reading card
|
// Try to generate thumbnail image for Continue Reading card
|
||||||
if (xtc.generateThumbBmp()) {
|
if (xtc.generateCoverHomeBmp()) {
|
||||||
coverBmpPath = xtc.getThumbBmpPath();
|
coverBmpPath = xtc.getCoverHomeBmpPath();
|
||||||
hasCoverImage = true;
|
hasCoverImage = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,10 +224,32 @@ void HomeActivity::render() {
|
|||||||
constexpr int bottomMargin = 60;
|
constexpr int bottomMargin = 60;
|
||||||
|
|
||||||
// --- Top "book" card for the current title (selectorIndex == 0) ---
|
// --- Top "book" card for the current title (selectorIndex == 0) ---
|
||||||
const int bookWidth = pageWidth / 2;
|
// Load cover image to get its dimensions
|
||||||
const int bookHeight = pageHeight / 2;
|
int coverWidth = 0;
|
||||||
const int bookX = (pageWidth - bookWidth) / 2;
|
int coverHeight = 0;
|
||||||
|
if (hasContinueReading && hasCoverImage && !coverBmpPath.empty()) {
|
||||||
|
FsFile coverFile;
|
||||||
|
if (SdMan.openFileForRead("HOME", coverBmpPath, coverFile)) {
|
||||||
|
Bitmap testBitmap(coverFile);
|
||||||
|
if (testBitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
|
coverWidth = testBitmap.getWidth();
|
||||||
|
coverHeight = testBitmap.getHeight();
|
||||||
|
}
|
||||||
|
coverFile.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate card dimensions based on cover image
|
||||||
|
// Use 400px height as specified, with proportional width
|
||||||
|
constexpr int CARD_HEIGHT = 400;
|
||||||
|
const int cardWidth = (coverWidth > 0 && coverHeight > 0)
|
||||||
|
? (CARD_HEIGHT * coverWidth) / coverHeight
|
||||||
|
: 240; // Fallback to 240px width if no image (maintain aspect ratio)
|
||||||
|
|
||||||
|
const int bookX = (pageWidth - cardWidth) / 2;
|
||||||
constexpr int bookY = 30;
|
constexpr int bookY = 30;
|
||||||
|
const int bookWidth = cardWidth;
|
||||||
|
const int bookHeight = CARD_HEIGHT;
|
||||||
const bool bookSelected = hasContinueReading && selectorIndex == 0;
|
const bool bookSelected = hasContinueReading && selectorIndex == 0;
|
||||||
|
|
||||||
// Bookmark dimensions (used in multiple places)
|
// Bookmark dimensions (used in multiple places)
|
||||||
@ -245,27 +268,9 @@ void HomeActivity::render() {
|
|||||||
if (SdMan.openFileForRead("HOME", coverBmpPath, file)) {
|
if (SdMan.openFileForRead("HOME", coverBmpPath, file)) {
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
// Calculate position to center image within the book card
|
// Since the book card already has the exact same size as the image,
|
||||||
int coverX, coverY;
|
// we can draw it at the same position with the same dimensions
|
||||||
|
renderer.drawBitmap(bitmap, bookX, bookY, bookWidth, bookHeight);
|
||||||
if (bitmap.getWidth() > bookWidth || bitmap.getHeight() > bookHeight) {
|
|
||||||
const float imgRatio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
|
||||||
const float boxRatio = static_cast<float>(bookWidth) / static_cast<float>(bookHeight);
|
|
||||||
|
|
||||||
if (imgRatio > boxRatio) {
|
|
||||||
coverX = bookX;
|
|
||||||
coverY = bookY + (bookHeight - static_cast<int>(bookWidth / imgRatio)) / 2;
|
|
||||||
} else {
|
|
||||||
coverX = bookX + (bookWidth - static_cast<int>(bookHeight * imgRatio)) / 2;
|
|
||||||
coverY = bookY;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
coverX = bookX + (bookWidth - bitmap.getWidth()) / 2;
|
|
||||||
coverY = bookY + (bookHeight - bitmap.getHeight()) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the cover image centered within the book card
|
|
||||||
renderer.drawBitmap(bitmap, coverX, coverY, bookWidth, bookHeight);
|
|
||||||
|
|
||||||
// Draw border around the card
|
// Draw border around the card
|
||||||
renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
|
renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
|
||||||
@ -366,7 +371,7 @@ void HomeActivity::render() {
|
|||||||
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
|
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
|
||||||
// Remove "..." first, then remove one UTF-8 char, then add "..." back
|
// Remove "..." first, then remove one UTF-8 char, then add "..." back
|
||||||
lines.back().resize(lines.back().size() - 3); // Remove "..."
|
lines.back().resize(lines.back().size() - 3); // Remove "..."
|
||||||
StringUtils::utf8RemoveLastChar(lines.back());
|
utf8RemoveLastChar(lines.back());
|
||||||
lines.back().append("...");
|
lines.back().append("...");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -375,7 +380,7 @@ void HomeActivity::render() {
|
|||||||
int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
|
int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
|
||||||
while (wordWidth > maxLineWidth && !i.empty()) {
|
while (wordWidth > maxLineWidth && !i.empty()) {
|
||||||
// Word itself is too long, trim it (UTF-8 safe)
|
// Word itself is too long, trim it (UTF-8 safe)
|
||||||
StringUtils::utf8RemoveLastChar(i);
|
utf8RemoveLastChar(i);
|
||||||
// Check if we have room for ellipsis
|
// Check if we have room for ellipsis
|
||||||
std::string withEllipsis = i + "...";
|
std::string withEllipsis = i + "...";
|
||||||
wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str());
|
wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str());
|
||||||
@ -428,7 +433,7 @@ void HomeActivity::render() {
|
|||||||
if (!lastBookAuthor.empty()) {
|
if (!lastBookAuthor.empty()) {
|
||||||
std::string trimmedAuthor = lastBookAuthor;
|
std::string trimmedAuthor = lastBookAuthor;
|
||||||
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
||||||
StringUtils::utf8RemoveLastChar(trimmedAuthor);
|
utf8RemoveLastChar(trimmedAuthor);
|
||||||
}
|
}
|
||||||
if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) <
|
if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) <
|
||||||
renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) {
|
renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) {
|
||||||
@ -462,14 +467,14 @@ void HomeActivity::render() {
|
|||||||
// Trim author if too long (UTF-8 safe)
|
// Trim author if too long (UTF-8 safe)
|
||||||
bool wasTrimmed = false;
|
bool wasTrimmed = false;
|
||||||
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
||||||
StringUtils::utf8RemoveLastChar(trimmedAuthor);
|
utf8RemoveLastChar(trimmedAuthor);
|
||||||
wasTrimmed = true;
|
wasTrimmed = true;
|
||||||
}
|
}
|
||||||
if (wasTrimmed && !trimmedAuthor.empty()) {
|
if (wasTrimmed && !trimmedAuthor.empty()) {
|
||||||
// Make room for ellipsis
|
// Make room for ellipsis
|
||||||
while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth &&
|
while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth &&
|
||||||
!trimmedAuthor.empty()) {
|
!trimmedAuthor.empty()) {
|
||||||
StringUtils::utf8RemoveLastChar(trimmedAuthor);
|
utf8RemoveLastChar(trimmedAuthor);
|
||||||
}
|
}
|
||||||
trimmedAuthor.append("...");
|
trimmedAuthor.append("...");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -570,8 +570,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
|
|||||||
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
|
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
|
||||||
titleMarginLeftAdjusted = titleMarginLeft;
|
titleMarginLeftAdjusted = titleMarginLeft;
|
||||||
}
|
}
|
||||||
while (titleWidth > availableTitleSpace && title.length() > 11) {
|
if (titleWidth > availableTitleSpace) {
|
||||||
title.replace(title.length() - 8, 8, "...");
|
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace);
|
||||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -533,8 +533,8 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int
|
|||||||
|
|
||||||
std::string title = txt->getTitle();
|
std::string title = txt->getTitle();
|
||||||
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||||
while (titleWidth > availableTextWidth && title.length() > 11) {
|
if (titleWidth > availableTextWidth) {
|
||||||
title.replace(title.length() - 8, 8, "...");
|
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTextWidth);
|
||||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,23 +61,4 @@ bool checkFileExtension(const String& fileName, const char* extension) {
|
|||||||
return localFile.endsWith(localExtension);
|
return localFile.endsWith(localExtension);
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t utf8RemoveLastChar(std::string& str) {
|
|
||||||
if (str.empty()) return 0;
|
|
||||||
size_t pos = str.size() - 1;
|
|
||||||
// Walk back to find the start of the last UTF-8 character
|
|
||||||
// UTF-8 continuation bytes start with 10xxxxxx (0x80-0xBF)
|
|
||||||
while (pos > 0 && (static_cast<unsigned char>(str[pos]) & 0xC0) == 0x80) {
|
|
||||||
--pos;
|
|
||||||
}
|
|
||||||
str.resize(pos);
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate string by removing N UTF-8 characters from the end
|
|
||||||
void utf8TruncateChars(std::string& str, const size_t numChars) {
|
|
||||||
for (size_t i = 0; i < numChars && !str.empty(); ++i) {
|
|
||||||
utf8RemoveLastChar(str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace StringUtils
|
} // namespace StringUtils
|
||||||
|
|||||||
@ -19,10 +19,4 @@ std::string sanitizeFilename(const std::string& name, size_t maxLength = 100);
|
|||||||
bool checkFileExtension(const std::string& fileName, const char* extension);
|
bool checkFileExtension(const std::string& fileName, const char* extension);
|
||||||
bool checkFileExtension(const String& fileName, const char* extension);
|
bool checkFileExtension(const String& fileName, const char* extension);
|
||||||
|
|
||||||
// UTF-8 safe string truncation - removes one character from the end
|
|
||||||
// Returns the new size after removing one UTF-8 character
|
|
||||||
size_t utf8RemoveLastChar(std::string& str);
|
|
||||||
|
|
||||||
// Truncate string by removing N UTF-8 characters from the end
|
|
||||||
void utf8TruncateChars(std::string& str, size_t numChars);
|
|
||||||
} // namespace StringUtils
|
} // namespace StringUtils
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user