#pragma once #include #include #include #include #include #include "FootnoteEntry.h" #include "blocks/ImageBlock.h" #include "blocks/TextBlock.h" enum PageElementTag : uint8_t { TAG_PageLine = 1, TAG_PageImage = 2, // New tag }; // represents something that has been added to a page class PageElement { public: int16_t xPos; int16_t yPos; explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {} virtual ~PageElement() = default; virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0; virtual bool serialize(FsFile& file) = 0; virtual PageElementTag getTag() const = 0; // Add type identification }; // a line from a block element class PageLine final : public PageElement { std::shared_ptr block; public: PageLine(std::shared_ptr block, const int16_t xPos, const int16_t yPos) : PageElement(xPos, yPos), block(std::move(block)) {} const std::shared_ptr& getBlock() const { return block; } void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; bool serialize(FsFile& file) override; PageElementTag getTag() const override { return TAG_PageLine; } static std::unique_ptr deserialize(FsFile& file); }; // New PageImage class class PageImage final : public PageElement { std::shared_ptr imageBlock; public: PageImage(std::shared_ptr block, const int16_t xPos, const int16_t yPos) : PageElement(xPos, yPos), imageBlock(std::move(block)) {} 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 deserialize(FsFile& file); const ImageBlock& getImageBlock() const { return *imageBlock; } }; class Page { public: // the list of block index and line numbers on this page std::vector> elements; std::vector footnotes; static constexpr uint16_t MAX_FOOTNOTES_PER_PAGE = 16; void addFootnote(const char* number, const char* href) { if (footnotes.size() >= MAX_FOOTNOTES_PER_PAGE) return; // Cap per-page footnotes FootnoteEntry entry; strncpy(entry.number, number, sizeof(entry.number) - 1); entry.number[sizeof(entry.number) - 1] = '\0'; strncpy(entry.href, href, sizeof(entry.href) - 1); entry.href[sizeof(entry.href) - 1] = '\0'; footnotes.push_back(entry); } void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; bool serialize(FsFile& file) const; static std::unique_ptr deserialize(FsFile& file); // Check if page contains any images (used to force full refresh) bool hasImages() const { return std::any_of(elements.begin(), elements.end(), [](const std::shared_ptr& el) { return el->getTag() == TAG_PageImage; }); } // Get bounding box of all images on the page (union of image rects) // Returns false if no images. Coordinates are relative to page origin. bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const { bool found = false; int16_t minX = INT16_MAX, minY = INT16_MAX, maxX = INT16_MIN, maxY = INT16_MIN; for (const auto& el : elements) { if (el->getTag() == TAG_PageImage) { const auto& img = static_cast(*el); int16_t x = img.xPos; int16_t y = img.yPos; int16_t right = x + img.getImageBlock().getWidth(); int16_t bottom = y + img.getImageBlock().getHeight(); minX = std::min(minX, x); minY = std::min(minY, y); maxX = std::max(maxX, right); maxY = std::max(maxY, bottom); found = true; } } if (found) { outX = minX; outY = minY; outW = maxX - minX; outH = maxY - minY; } return found; } };