All checks were successful
CI / build (push) Successful in 2m23s
On ESP32-C3 with USB CDC, Serial.printf() blocks indefinitely when USB is not connected. This caused device freezes when booted without USB. Solution: Call Serial.setTxTimeoutMs(0) after Serial.begin() to make all Serial output non-blocking. Also added if (Serial) guards to high-traffic logging paths in EpubReaderActivity as belt-and-suspenders protection. Includes documentation of the debugging process and Serial call inventory. Also applies clang-format to fix pre-existing formatting issues.
375 lines
12 KiB
C++
375 lines
12 KiB
C++
#include "Txt.h"
|
|
|
|
#include <FsHelpers.h>
|
|
#include <JpegToBmpConverter.h>
|
|
|
|
Txt::Txt(std::string path, std::string cacheBasePath)
|
|
: filepath(std::move(path)), cacheBasePath(std::move(cacheBasePath)) {
|
|
// Generate cache path from file path hash
|
|
const size_t hash = std::hash<std::string>{}(filepath);
|
|
cachePath = this->cacheBasePath + "/txt_" + std::to_string(hash);
|
|
}
|
|
|
|
bool Txt::load() {
|
|
if (loaded) {
|
|
return true;
|
|
}
|
|
|
|
if (!SdMan.exists(filepath.c_str())) {
|
|
Serial.printf("[%lu] [TXT] File does not exist: %s\n", millis(), filepath.c_str());
|
|
return false;
|
|
}
|
|
|
|
FsFile file;
|
|
if (!SdMan.openFileForRead("TXT", filepath, file)) {
|
|
Serial.printf("[%lu] [TXT] Failed to open file: %s\n", millis(), filepath.c_str());
|
|
return false;
|
|
}
|
|
|
|
fileSize = file.size();
|
|
file.close();
|
|
|
|
loaded = true;
|
|
Serial.printf("[%lu] [TXT] Loaded TXT file: %s (%zu bytes)\n", millis(), filepath.c_str(), fileSize);
|
|
return true;
|
|
}
|
|
|
|
std::string Txt::getTitle() const {
|
|
// Extract filename without path and extension
|
|
size_t lastSlash = filepath.find_last_of('/');
|
|
std::string filename = (lastSlash != std::string::npos) ? filepath.substr(lastSlash + 1) : filepath;
|
|
|
|
// Remove .txt extension
|
|
if (filename.length() >= 4 && filename.substr(filename.length() - 4) == ".txt") {
|
|
filename = filename.substr(0, filename.length() - 4);
|
|
}
|
|
|
|
return filename;
|
|
}
|
|
|
|
void Txt::setupCacheDir() const {
|
|
if (!SdMan.exists(cacheBasePath.c_str())) {
|
|
SdMan.mkdir(cacheBasePath.c_str());
|
|
}
|
|
if (!SdMan.exists(cachePath.c_str())) {
|
|
SdMan.mkdir(cachePath.c_str());
|
|
}
|
|
}
|
|
|
|
std::string Txt::findCoverImage() const {
|
|
// Get the folder containing the txt file
|
|
size_t lastSlash = filepath.find_last_of('/');
|
|
std::string folder = (lastSlash != std::string::npos) ? filepath.substr(0, lastSlash) : "";
|
|
if (folder.empty()) {
|
|
folder = "/";
|
|
}
|
|
|
|
// Get the base filename without extension (e.g., "mybook" from "/books/mybook.txt")
|
|
std::string baseName = getTitle();
|
|
|
|
// Image extensions to try
|
|
const char* extensions[] = {".bmp", ".jpg", ".jpeg", ".png", ".BMP", ".JPG", ".JPEG", ".PNG"};
|
|
|
|
// First priority: look for image with same name as txt file (e.g., mybook.jpg)
|
|
for (const auto& ext : extensions) {
|
|
std::string coverPath = folder + "/" + baseName + ext;
|
|
if (SdMan.exists(coverPath.c_str())) {
|
|
Serial.printf("[%lu] [TXT] Found matching cover image: %s\n", millis(), coverPath.c_str());
|
|
return coverPath;
|
|
}
|
|
}
|
|
|
|
// Fallback: look for cover image files
|
|
const char* coverNames[] = {"cover", "Cover", "COVER"};
|
|
for (const auto& name : coverNames) {
|
|
for (const auto& ext : extensions) {
|
|
std::string coverPath = folder + "/" + std::string(name) + ext;
|
|
if (SdMan.exists(coverPath.c_str())) {
|
|
Serial.printf("[%lu] [TXT] Found fallback cover image: %s\n", millis(), coverPath.c_str());
|
|
return coverPath;
|
|
}
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
std::string Txt::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
|
|
|
bool Txt::generateCoverBmp() const {
|
|
// Already generated, return true
|
|
if (SdMan.exists(getCoverBmpPath().c_str())) {
|
|
return true;
|
|
}
|
|
|
|
std::string coverImagePath = findCoverImage();
|
|
if (coverImagePath.empty()) {
|
|
Serial.printf("[%lu] [TXT] No cover image found for TXT file\n", millis());
|
|
return false;
|
|
}
|
|
|
|
// Setup cache directory
|
|
setupCacheDir();
|
|
|
|
// Get file extension
|
|
const size_t len = coverImagePath.length();
|
|
const bool isJpg =
|
|
(len >= 4 && (coverImagePath.substr(len - 4) == ".jpg" || coverImagePath.substr(len - 4) == ".JPG")) ||
|
|
(len >= 5 && (coverImagePath.substr(len - 5) == ".jpeg" || coverImagePath.substr(len - 5) == ".JPEG"));
|
|
const bool isBmp = len >= 4 && (coverImagePath.substr(len - 4) == ".bmp" || coverImagePath.substr(len - 4) == ".BMP");
|
|
|
|
if (isBmp) {
|
|
// Copy BMP file to cache
|
|
Serial.printf("[%lu] [TXT] Copying BMP cover image to cache\n", millis());
|
|
FsFile src, dst;
|
|
if (!SdMan.openFileForRead("TXT", coverImagePath, src)) {
|
|
return false;
|
|
}
|
|
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), dst)) {
|
|
src.close();
|
|
return false;
|
|
}
|
|
uint8_t buffer[1024];
|
|
while (src.available()) {
|
|
size_t bytesRead = src.read(buffer, sizeof(buffer));
|
|
dst.write(buffer, bytesRead);
|
|
}
|
|
src.close();
|
|
dst.close();
|
|
Serial.printf("[%lu] [TXT] Copied BMP cover to cache\n", millis());
|
|
return true;
|
|
}
|
|
|
|
if (isJpg) {
|
|
// Convert JPG/JPEG to BMP (same approach as Epub)
|
|
Serial.printf("[%lu] [TXT] Generating BMP from JPG cover image\n", millis());
|
|
FsFile coverJpg, coverBmp;
|
|
if (!SdMan.openFileForRead("TXT", coverImagePath, coverJpg)) {
|
|
return false;
|
|
}
|
|
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) {
|
|
coverJpg.close();
|
|
return false;
|
|
}
|
|
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp);
|
|
coverJpg.close();
|
|
coverBmp.close();
|
|
|
|
if (!success) {
|
|
Serial.printf("[%lu] [TXT] Failed to generate BMP from JPG cover image\n", millis());
|
|
SdMan.remove(getCoverBmpPath().c_str());
|
|
} else {
|
|
Serial.printf("[%lu] [TXT] Generated BMP from JPG cover image\n", millis());
|
|
}
|
|
return success;
|
|
}
|
|
|
|
// PNG files are not supported (would need a PNG decoder)
|
|
Serial.printf("[%lu] [TXT] Cover image format not supported (only BMP/JPG/JPEG)\n", millis());
|
|
return false;
|
|
}
|
|
|
|
std::string Txt::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
|
|
|
|
bool Txt::generateThumbBmp() const {
|
|
// Already generated, return true
|
|
if (SdMan.exists(getThumbBmpPath().c_str())) {
|
|
return true;
|
|
}
|
|
|
|
std::string coverImagePath = findCoverImage();
|
|
if (coverImagePath.empty()) {
|
|
Serial.printf("[%lu] [TXT] No cover image found for thumbnail\n", millis());
|
|
return false;
|
|
}
|
|
|
|
// Setup cache directory
|
|
setupCacheDir();
|
|
|
|
// Get file extension
|
|
const size_t len = coverImagePath.length();
|
|
const bool isJpg =
|
|
(len >= 4 && (coverImagePath.substr(len - 4) == ".jpg" || coverImagePath.substr(len - 4) == ".JPG")) ||
|
|
(len >= 5 && (coverImagePath.substr(len - 5) == ".jpeg" || coverImagePath.substr(len - 5) == ".JPEG"));
|
|
|
|
if (isJpg) {
|
|
// Convert JPG to 1-bit BMP thumbnail
|
|
Serial.printf("[%lu] [TXT] Generating thumb BMP from JPG cover image\n", millis());
|
|
FsFile coverJpg, thumbBmp;
|
|
if (!SdMan.openFileForRead("TXT", coverImagePath, coverJpg)) {
|
|
return false;
|
|
}
|
|
if (!SdMan.openFileForWrite("TXT", getThumbBmpPath(), thumbBmp)) {
|
|
coverJpg.close();
|
|
return false;
|
|
}
|
|
constexpr int THUMB_TARGET_WIDTH = 240;
|
|
constexpr int THUMB_TARGET_HEIGHT = 400;
|
|
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
|
|
THUMB_TARGET_HEIGHT);
|
|
coverJpg.close();
|
|
thumbBmp.close();
|
|
|
|
if (!success) {
|
|
Serial.printf("[%lu] [TXT] Failed to generate thumb BMP from JPG cover image\n", millis());
|
|
SdMan.remove(getThumbBmpPath().c_str());
|
|
} else {
|
|
Serial.printf("[%lu] [TXT] Generated thumb BMP from JPG cover image\n", millis());
|
|
}
|
|
return success;
|
|
}
|
|
|
|
// For BMP files, just copy cover.bmp to thumb.bmp (no scaling for BMP)
|
|
if (generateCoverBmp() && SdMan.exists(getCoverBmpPath().c_str())) {
|
|
FsFile src, dst;
|
|
if (SdMan.openFileForRead("TXT", getCoverBmpPath(), src)) {
|
|
if (SdMan.openFileForWrite("TXT", 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] [TXT] Copied cover to thumb\n", millis());
|
|
return SdMan.exists(getThumbBmpPath().c_str());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
std::string Txt::getMicroThumbBmpPath() const { return cachePath + "/micro_thumb.bmp"; }
|
|
|
|
bool Txt::generateMicroThumbBmp() const {
|
|
// Already generated, return true
|
|
if (SdMan.exists(getMicroThumbBmpPath().c_str())) {
|
|
return true;
|
|
}
|
|
|
|
std::string coverImagePath = findCoverImage();
|
|
if (coverImagePath.empty()) {
|
|
Serial.printf("[%lu] [TXT] No cover image found for micro thumbnail\n", millis());
|
|
return false;
|
|
}
|
|
|
|
// Setup cache directory
|
|
setupCacheDir();
|
|
|
|
// Get file extension
|
|
const size_t len = coverImagePath.length();
|
|
const bool isJpg =
|
|
(len >= 4 && (coverImagePath.substr(len - 4) == ".jpg" || coverImagePath.substr(len - 4) == ".JPG")) ||
|
|
(len >= 5 && (coverImagePath.substr(len - 5) == ".jpeg" || coverImagePath.substr(len - 5) == ".JPEG"));
|
|
|
|
if (isJpg) {
|
|
// Convert JPG to 1-bit BMP micro thumbnail
|
|
Serial.printf("[%lu] [TXT] Generating micro thumb BMP from JPG cover image\n", millis());
|
|
FsFile coverJpg, microThumbBmp;
|
|
if (!SdMan.openFileForRead("TXT", coverImagePath, coverJpg)) {
|
|
return false;
|
|
}
|
|
if (!SdMan.openFileForWrite("TXT", getMicroThumbBmpPath(), microThumbBmp)) {
|
|
coverJpg.close();
|
|
return false;
|
|
}
|
|
constexpr int MICRO_THUMB_TARGET_WIDTH = 45;
|
|
constexpr int MICRO_THUMB_TARGET_HEIGHT = 60;
|
|
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(
|
|
coverJpg, microThumbBmp, MICRO_THUMB_TARGET_WIDTH, MICRO_THUMB_TARGET_HEIGHT);
|
|
coverJpg.close();
|
|
microThumbBmp.close();
|
|
|
|
if (!success) {
|
|
Serial.printf("[%lu] [TXT] Failed to generate micro thumb BMP from JPG cover image\n", millis());
|
|
SdMan.remove(getMicroThumbBmpPath().c_str());
|
|
} else {
|
|
Serial.printf("[%lu] [TXT] Generated micro thumb BMP from JPG cover image\n", millis());
|
|
}
|
|
return success;
|
|
}
|
|
|
|
// For BMP files, just copy cover.bmp to micro_thumb.bmp (no scaling for BMP)
|
|
if (generateCoverBmp() && SdMan.exists(getCoverBmpPath().c_str())) {
|
|
FsFile src, dst;
|
|
if (SdMan.openFileForRead("TXT", getCoverBmpPath(), src)) {
|
|
if (SdMan.openFileForWrite("TXT", getMicroThumbBmpPath(), 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] [TXT] Copied cover to micro thumb\n", millis());
|
|
return SdMan.exists(getMicroThumbBmpPath().c_str());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Txt::generateAllCovers(const std::function<void(int)>& progressCallback) const {
|
|
// Check if all covers already exist
|
|
const bool hasCover = SdMan.exists(getCoverBmpPath().c_str());
|
|
const bool hasThumb = SdMan.exists(getThumbBmpPath().c_str());
|
|
const bool hasMicroThumb = SdMan.exists(getMicroThumbBmpPath().c_str());
|
|
|
|
if (hasCover && hasThumb && hasMicroThumb) {
|
|
Serial.printf("[%lu] [TXT] All covers already cached\n", millis());
|
|
if (progressCallback) progressCallback(100);
|
|
return true;
|
|
}
|
|
|
|
std::string coverImagePath = findCoverImage();
|
|
if (coverImagePath.empty()) {
|
|
Serial.printf("[%lu] [TXT] No cover image found, skipping cover generation\n", millis());
|
|
return false;
|
|
}
|
|
|
|
Serial.printf("[%lu] [TXT] Generating all covers (cover:%d, thumb:%d, micro:%d)\n", millis(), !hasCover, !hasThumb,
|
|
!hasMicroThumb);
|
|
|
|
// Generate each cover type that's missing with progress updates
|
|
if (!hasCover) {
|
|
(void)generateCoverBmp();
|
|
}
|
|
if (progressCallback) progressCallback(33);
|
|
|
|
if (!hasThumb) {
|
|
(void)generateThumbBmp();
|
|
}
|
|
if (progressCallback) progressCallback(66);
|
|
|
|
if (!hasMicroThumb) {
|
|
(void)generateMicroThumbBmp();
|
|
}
|
|
if (progressCallback) progressCallback(100);
|
|
|
|
Serial.printf("[%lu] [TXT] All cover generation complete\n", millis());
|
|
return true;
|
|
}
|
|
|
|
bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const {
|
|
if (!loaded) {
|
|
return false;
|
|
}
|
|
|
|
FsFile file;
|
|
if (!SdMan.openFileForRead("TXT", filepath, file)) {
|
|
return false;
|
|
}
|
|
|
|
if (!file.seek(offset)) {
|
|
file.close();
|
|
return false;
|
|
}
|
|
|
|
size_t bytesRead = file.read(buffer, length);
|
|
file.close();
|
|
|
|
return bytesRead > 0;
|
|
}
|