mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 23:57:39 +03:00
add support for images
This commit is contained in:
parent
40f9ed485c
commit
9db4ef6f4b
32
flake.nix
Normal file
32
flake.nix
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
description = "CrossPoint Reader - ESP32 E-Paper Firmware";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
platformio
|
||||||
|
python3
|
||||||
|
git
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "CrossPoint Reader development environment"
|
||||||
|
echo "Commands:"
|
||||||
|
echo " pio run - Build firmware"
|
||||||
|
echo " pio run -t upload - Build and flash to device"
|
||||||
|
echo " pio run -t clean - Clean build artifacts"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,9 @@
|
|||||||
#include "Page.h"
|
#include "Page.h"
|
||||||
|
|
||||||
|
#include <Bitmap.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
@ -25,6 +28,50 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
|
|||||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
|
FsFile imageFile;
|
||||||
|
if (!SdMan.openFileForRead("PGI", cachePath, imageFile)) {
|
||||||
|
Serial.printf("[%lu] [PGI] Failed to open image: %s\n", millis(), cachePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap bitmap(imageFile);
|
||||||
|
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
|
||||||
|
Serial.printf("[%lu] [PGI] Failed to parse image headers: %s\n", millis(), cachePath.c_str());
|
||||||
|
imageFile.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the bitmap at the specified position
|
||||||
|
renderer.drawBitmap(bitmap, xPos + xOffset, yPos + yOffset, width, height);
|
||||||
|
imageFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PageImage::serialize(FsFile& file) {
|
||||||
|
serialization::writePod(file, xPos);
|
||||||
|
serialization::writePod(file, yPos);
|
||||||
|
serialization::writePod(file, width);
|
||||||
|
serialization::writePod(file, height);
|
||||||
|
serialization::writeString(file, cachePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
|
||||||
|
int16_t xPos;
|
||||||
|
int16_t yPos;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
std::string cachePath;
|
||||||
|
|
||||||
|
serialization::readPod(file, xPos);
|
||||||
|
serialization::readPod(file, yPos);
|
||||||
|
serialization::readPod(file, width);
|
||||||
|
serialization::readPod(file, height);
|
||||||
|
serialization::readString(file, cachePath);
|
||||||
|
|
||||||
|
return std::unique_ptr<PageImage>(new PageImage(std::move(cachePath), width, height, xPos, yPos));
|
||||||
|
}
|
||||||
|
|
||||||
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||||
for (auto& element : elements) {
|
for (auto& element : elements) {
|
||||||
element->render(renderer, fontId, xOffset, yOffset);
|
element->render(renderer, fontId, xOffset, yOffset);
|
||||||
@ -36,8 +83,9 @@ bool Page::serialize(FsFile& file) const {
|
|||||||
serialization::writePod(file, count);
|
serialization::writePod(file, count);
|
||||||
|
|
||||||
for (const auto& el : elements) {
|
for (const auto& el : elements) {
|
||||||
// Only PageLine exists currently
|
// Write element tag
|
||||||
serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine));
|
serialization::writePod(file, static_cast<uint8_t>(el->getTag()));
|
||||||
|
|
||||||
if (!el->serialize(file)) {
|
if (!el->serialize(file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -59,6 +107,9 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
|||||||
if (tag == TAG_PageLine) {
|
if (tag == TAG_PageLine) {
|
||||||
auto pl = PageLine::deserialize(file);
|
auto pl = PageLine::deserialize(file);
|
||||||
page->elements.push_back(std::move(pl));
|
page->elements.push_back(std::move(pl));
|
||||||
|
} else if (tag == TAG_PageImage) {
|
||||||
|
auto pi = PageImage::deserialize(file);
|
||||||
|
page->elements.push_back(std::move(pi));
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
enum PageElementTag : uint8_t {
|
enum PageElementTag : uint8_t {
|
||||||
TAG_PageLine = 1,
|
TAG_PageLine = 1,
|
||||||
|
TAG_PageImage = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
// represents something that has been added to a page
|
// represents something that has been added to a page
|
||||||
@ -19,6 +20,7 @@ class PageElement {
|
|||||||
virtual ~PageElement() = default;
|
virtual ~PageElement() = default;
|
||||||
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||||||
virtual bool serialize(FsFile& file) = 0;
|
virtual bool serialize(FsFile& file) = 0;
|
||||||
|
virtual PageElementTag getTag() const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// a line from a block element
|
// a line from a block element
|
||||||
@ -30,9 +32,27 @@ class PageLine final : public PageElement {
|
|||||||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
bool serialize(FsFile& file) override;
|
bool serialize(FsFile& file) override;
|
||||||
|
PageElementTag getTag() const override { return TAG_PageLine; }
|
||||||
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// an image element
|
||||||
|
class PageImage final : public PageElement {
|
||||||
|
std::string cachePath;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
|
||||||
|
public:
|
||||||
|
PageImage(std::string path, int w, int h, const int16_t xPos, const int16_t yPos)
|
||||||
|
: PageElement(xPos, yPos), cachePath(std::move(path)), width(w), height(h) {}
|
||||||
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
|
bool serialize(FsFile& file) override;
|
||||||
|
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||||
|
static std::unique_ptr<PageImage> deserialize(FsFile& file);
|
||||||
|
int getWidth() const { return width; }
|
||||||
|
int getHeight() const { return height; }
|
||||||
|
};
|
||||||
|
|
||||||
class Page {
|
class Page {
|
||||||
public:
|
public:
|
||||||
// the list of block index and line numbers on this page
|
// the list of block index and line numbers on this page
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 7;
|
constexpr uint8_t SECTION_FILE_VERSION = 8; // Incremented for image support
|
||||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(int) +
|
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(int) +
|
||||||
sizeof(int) + sizeof(int) + sizeof(uint32_t);
|
sizeof(int) + sizeof(int) + sizeof(uint32_t);
|
||||||
} // namespace
|
} // namespace
|
||||||
@ -169,8 +169,8 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
std::vector<uint32_t> lut = {};
|
std::vector<uint32_t> lut = {};
|
||||||
|
|
||||||
ChapterHtmlSlimParser visitor(
|
ChapterHtmlSlimParser visitor(
|
||||||
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight,
|
tmpHtmlPath, renderer, epub.get(), spineIndex, fontId, lineCompression, extraParagraphSpacing, viewportWidth,
|
||||||
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
viewportHeight, [this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
||||||
progressFn);
|
progressFn);
|
||||||
success = visitor.parseAndBuildPages();
|
success = visitor.parseAndBuildPages();
|
||||||
|
|
||||||
|
|||||||
25
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
25
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#include "ImageBlock.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
void ImageBlock::layout(GfxRenderer& renderer) {
|
||||||
|
// ImageBlock doesn't need layout - dimensions are already known
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageBlock::getScaledDimensions(const int viewportWidth, const int viewportHeight, int* outWidth,
|
||||||
|
int* outHeight) const {
|
||||||
|
if (width <= viewportWidth && height <= viewportHeight) {
|
||||||
|
// Image fits, no scaling needed
|
||||||
|
*outWidth = width;
|
||||||
|
*outHeight = height;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate scale factor to fit within viewport
|
||||||
|
float scaleX = static_cast<float>(viewportWidth) / static_cast<float>(width);
|
||||||
|
float scaleY = static_cast<float>(viewportHeight) / static_cast<float>(height);
|
||||||
|
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
|
|
||||||
|
*outWidth = static_cast<int>(static_cast<float>(width) * scale);
|
||||||
|
*outHeight = static_cast<int>(static_cast<float>(height) * scale);
|
||||||
|
}
|
||||||
28
lib/Epub/Epub/blocks/ImageBlock.h
Normal file
28
lib/Epub/Epub/blocks/ImageBlock.h
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Block.h"
|
||||||
|
|
||||||
|
class GfxRenderer;
|
||||||
|
|
||||||
|
// Represents an image block in the HTML document
|
||||||
|
class ImageBlock final : public Block {
|
||||||
|
public:
|
||||||
|
std::string cachePath; // Path to cached BMP file
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
bool isCached;
|
||||||
|
|
||||||
|
ImageBlock() : width(0), height(0), isCached(false) {}
|
||||||
|
ImageBlock(std::string path, int w, int h)
|
||||||
|
: cachePath(std::move(path)), width(w), height(h), isCached(true) {}
|
||||||
|
|
||||||
|
void layout(GfxRenderer& renderer) override;
|
||||||
|
BlockType getType() override { return IMAGE_BLOCK; }
|
||||||
|
bool isEmpty() override { return cachePath.empty() || !isCached; }
|
||||||
|
void finish() override {}
|
||||||
|
|
||||||
|
// Get scaled dimensions that fit within viewport
|
||||||
|
void getScaledDimensions(int viewportWidth, int viewportHeight, int* outWidth, int* outHeight) const;
|
||||||
|
};
|
||||||
@ -1,11 +1,15 @@
|
|||||||
#include "ChapterHtmlSlimParser.h"
|
#include "ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
|
#include <Bitmap.h>
|
||||||
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
|
#include <JpegToBmpConverter.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
|
|
||||||
#include "../Page.h"
|
#include "../Page.h"
|
||||||
|
#include "../blocks/ImageBlock.h"
|
||||||
#include "../htmlEntities.h"
|
#include "../htmlEntities.h"
|
||||||
|
|
||||||
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||||
@ -65,7 +69,22 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||||
// TODO: Start processing image tags
|
// Process image tag - extract src attribute
|
||||||
|
const char* src = nullptr;
|
||||||
|
if (atts != nullptr) {
|
||||||
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if (strcmp(atts[i], "src") == 0) {
|
||||||
|
src = atts[i + 1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src && self->epub) {
|
||||||
|
self->processImage(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip content inside image tag
|
||||||
self->skipUntilDepth = self->depth;
|
self->skipUntilDepth = self->depth;
|
||||||
self->depth += 1;
|
self->depth += 1;
|
||||||
return;
|
return;
|
||||||
@ -330,3 +349,190 @@ void ChapterHtmlSlimParser::makePages() {
|
|||||||
currentPageNextY += lineHeight / 2;
|
currentPageNextY += lineHeight / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string ChapterHtmlSlimParser::resolveImagePath(const char* src) const {
|
||||||
|
if (!src || !epub) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string srcPath(src);
|
||||||
|
|
||||||
|
// If the path is absolute (starts with /), it's relative to the EPUB root
|
||||||
|
if (srcPath[0] == '/') {
|
||||||
|
return srcPath.substr(1); // Remove leading slash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, resolve relative to the current chapter's directory
|
||||||
|
std::string basePath = epub->getBasePath();
|
||||||
|
|
||||||
|
// Simple path resolution - combine basePath with src
|
||||||
|
if (basePath.empty()) {
|
||||||
|
return srcPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure basePath ends with /
|
||||||
|
if (basePath.back() != '/') {
|
||||||
|
basePath += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return basePath + srcPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChapterHtmlSlimParser::processImage(const char* src) {
|
||||||
|
if (!src || !epub) {
|
||||||
|
Serial.printf("[%lu] [IMG] Invalid image source or epub pointer\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any pending text before adding image
|
||||||
|
if (currentTextBlock && !currentTextBlock->isEmpty()) {
|
||||||
|
makePages();
|
||||||
|
currentTextBlock.reset(new ParsedText(TextBlock::JUSTIFIED, extraParagraphSpacing));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the image path relative to the EPUB structure
|
||||||
|
std::string imagePath = resolveImagePath(src);
|
||||||
|
if (imagePath.empty()) {
|
||||||
|
Serial.printf("[%lu] [IMG] Failed to resolve image path: %s\n", millis(), src);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate cache path for this image
|
||||||
|
std::string cachePath = epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + "_img_" +
|
||||||
|
std::to_string(imageCounter++) + ".bmp";
|
||||||
|
|
||||||
|
// Check if image is already cached
|
||||||
|
if (!SdMan.exists(cachePath.c_str())) {
|
||||||
|
// Determine file extension
|
||||||
|
bool isJpeg = false;
|
||||||
|
bool isBmp = false;
|
||||||
|
size_t dotPos = imagePath.rfind('.');
|
||||||
|
if (dotPos != std::string::npos) {
|
||||||
|
std::string ext = imagePath.substr(dotPos);
|
||||||
|
isJpeg = (ext == ".jpg" || ext == ".jpeg" || ext == ".JPG" || ext == ".JPEG");
|
||||||
|
isBmp = (ext == ".bmp" || ext == ".BMP");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJpeg) {
|
||||||
|
// Extract JPEG and convert to BMP
|
||||||
|
std::string tempJpgPath = epub->getCachePath() + "/.temp_img.jpg";
|
||||||
|
|
||||||
|
FsFile tempJpg;
|
||||||
|
if (!SdMan.openFileForWrite("IMG", tempJpgPath.c_str(), tempJpg)) {
|
||||||
|
Serial.printf("[%lu] [IMG] Failed to create temp JPEG file\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!epub->readItemContentsToStream(imagePath, tempJpg, 1024)) {
|
||||||
|
Serial.printf("[%lu] [IMG] Failed to extract JPEG: %s\n", millis(), imagePath.c_str());
|
||||||
|
tempJpg.close();
|
||||||
|
SdMan.remove(tempJpgPath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tempJpg.close();
|
||||||
|
|
||||||
|
// Convert JPEG to BMP
|
||||||
|
if (!SdMan.openFileForRead("IMG", tempJpgPath.c_str(), tempJpg)) {
|
||||||
|
Serial.printf("[%lu] [IMG] Failed to reopen temp JPEG\n", millis());
|
||||||
|
SdMan.remove(tempJpgPath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile bmpFile;
|
||||||
|
if (!SdMan.openFileForWrite("IMG", cachePath.c_str(), bmpFile)) {
|
||||||
|
Serial.printf("[%lu] [IMG] Failed to create BMP file\n", millis());
|
||||||
|
tempJpg.close();
|
||||||
|
SdMan.remove(tempJpgPath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = JpegToBmpConverter::jpegFileToBmpStream(tempJpg, bmpFile);
|
||||||
|
tempJpg.close();
|
||||||
|
bmpFile.close();
|
||||||
|
SdMan.remove(tempJpgPath.c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
Serial.printf("[%lu] [IMG] Failed to convert JPEG to BMP: %s\n", millis(), imagePath.c_str());
|
||||||
|
SdMan.remove(cachePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (isBmp) {
|
||||||
|
// Extract BMP directly
|
||||||
|
FsFile bmpFile;
|
||||||
|
if (!SdMan.openFileForWrite("IMG", cachePath.c_str(), bmpFile)) {
|
||||||
|
Serial.printf("[%lu] [IMG] Failed to create BMP file\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!epub->readItemContentsToStream(imagePath, bmpFile, 1024)) {
|
||||||
|
Serial.printf("[%lu] [IMG] Failed to extract BMP: %s\n", millis(), imagePath.c_str());
|
||||||
|
bmpFile.close();
|
||||||
|
SdMan.remove(cachePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bmpFile.close();
|
||||||
|
} else {
|
||||||
|
// Unsupported format
|
||||||
|
Serial.printf("[%lu] [IMG] Unsupported image format (not JPEG or BMP): %s\n", millis(), imagePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read image dimensions
|
||||||
|
FsFile imageFile;
|
||||||
|
if (!SdMan.openFileForRead("IMG", cachePath.c_str(), imageFile)) {
|
||||||
|
Serial.printf("[%lu] [IMG] Failed to open cached image: %s\n", millis(), cachePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap bitmap(imageFile);
|
||||||
|
BmpReaderError err = bitmap.parseHeaders();
|
||||||
|
if (err != BmpReaderError::Ok) {
|
||||||
|
Serial.printf("[%lu] [IMG] Failed to parse BMP headers: %s\n", millis(), Bitmap::errorToString(err));
|
||||||
|
imageFile.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = bitmap.getWidth();
|
||||||
|
int height = bitmap.getHeight();
|
||||||
|
imageFile.close();
|
||||||
|
|
||||||
|
// Add image to page
|
||||||
|
addImageToPage(cachePath, width, height);
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [IMG] Successfully processed image: %s (%dx%d)\n", millis(), imagePath.c_str(), width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChapterHtmlSlimParser::addImageToPage(const std::string& cachePath, int width, int height) {
|
||||||
|
// Calculate scaled dimensions to fit viewport
|
||||||
|
ImageBlock imageBlock(cachePath, width, height);
|
||||||
|
int scaledWidth, scaledHeight;
|
||||||
|
imageBlock.getScaledDimensions(viewportWidth, viewportHeight, &scaledWidth, &scaledHeight);
|
||||||
|
|
||||||
|
// Check if image fits on current page
|
||||||
|
if (currentPageNextY + scaledHeight > viewportHeight && currentPageNextY > 0) {
|
||||||
|
// Start new page for image
|
||||||
|
if (currentPage && !currentPage->elements.empty()) {
|
||||||
|
completePageFn(std::move(currentPage));
|
||||||
|
}
|
||||||
|
currentPage.reset(new Page());
|
||||||
|
currentPageNextY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentPage) {
|
||||||
|
currentPage.reset(new Page());
|
||||||
|
currentPageNextY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center image horizontally
|
||||||
|
int xPos = (viewportWidth - scaledWidth) / 2;
|
||||||
|
if (xPos < 0) xPos = 0;
|
||||||
|
|
||||||
|
// Add image to page
|
||||||
|
currentPage->elements.push_back(std::make_shared<PageImage>(cachePath, scaledWidth, scaledHeight, xPos, currentPageNextY));
|
||||||
|
currentPageNextY += scaledHeight;
|
||||||
|
|
||||||
|
// Add some spacing after image
|
||||||
|
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||||
|
currentPageNextY += lineHeight;
|
||||||
|
}
|
||||||
|
|||||||
@ -11,12 +11,16 @@
|
|||||||
|
|
||||||
class Page;
|
class Page;
|
||||||
class GfxRenderer;
|
class GfxRenderer;
|
||||||
|
class Epub;
|
||||||
|
|
||||||
#define MAX_WORD_SIZE 200
|
#define MAX_WORD_SIZE 200
|
||||||
|
|
||||||
class ChapterHtmlSlimParser {
|
class ChapterHtmlSlimParser {
|
||||||
const std::string& filepath;
|
const std::string& filepath;
|
||||||
GfxRenderer& renderer;
|
GfxRenderer& renderer;
|
||||||
|
Epub* epub; // Pointer to epub for image extraction
|
||||||
|
int spineIndex;
|
||||||
|
int imageCounter = 0; // Counter for naming cached images
|
||||||
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
||||||
std::function<void(int)> progressFn; // Progress callback (0-100)
|
std::function<void(int)> progressFn; // Progress callback (0-100)
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
@ -38,19 +42,24 @@ class ChapterHtmlSlimParser {
|
|||||||
|
|
||||||
void startNewTextBlock(TextBlock::BLOCK_STYLE style);
|
void startNewTextBlock(TextBlock::BLOCK_STYLE style);
|
||||||
void makePages();
|
void makePages();
|
||||||
|
void processImage(const char* src);
|
||||||
|
void addImageToPage(const std::string& cachePath, int width, int height);
|
||||||
|
std::string resolveImagePath(const char* src) const;
|
||||||
// XML callbacks
|
// XML callbacks
|
||||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
||||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, Epub* epub, const int spineIndex,
|
||||||
const float lineCompression, const bool extraParagraphSpacing, const int viewportWidth,
|
const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const int viewportHeight,
|
const int viewportWidth, const int viewportHeight,
|
||||||
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
||||||
const std::function<void(int)>& progressFn = nullptr)
|
const std::function<void(int)>& progressFn = nullptr)
|
||||||
: filepath(filepath),
|
: filepath(filepath),
|
||||||
renderer(renderer),
|
renderer(renderer),
|
||||||
|
epub(epub),
|
||||||
|
spineIndex(spineIndex),
|
||||||
fontId(fontId),
|
fontId(fontId),
|
||||||
lineCompression(lineCompression),
|
lineCompression(lineCompression),
|
||||||
extraParagraphSpacing(extraParagraphSpacing),
|
extraParagraphSpacing(extraParagraphSpacing),
|
||||||
|
|||||||
15
shell.nix
Normal file
15
shell.nix
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
|
pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
platformio
|
||||||
|
python3
|
||||||
|
git
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "PlatformIO development environment loaded"
|
||||||
|
echo "Run 'pio run' to build the firmware"
|
||||||
|
echo "Run 'pio run -t upload' to build and flash"
|
||||||
|
'';
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user