diff --git a/docs/file-formats.md b/docs/file-formats.md index 2fa0c60b..1b7ca3b8 100644 --- a/docs/file-formats.md +++ b/docs/file-formats.md @@ -61,29 +61,29 @@ struct TocEntry { struct BookBin { // Header u8 version [[comment("Format version"), color("FFD93D")]]; - + // Version validation if (version != EXPECTED_VERSION) { std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION)); } - + u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]]; u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]]; u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]]; - + // Metadata section Metadata metadata [[comment("Book metadata")]]; - + // Validate LUT offset alignment u32 currentOffset = $; if (currentOffset != lutOffset) { std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset)); } - + // Lookup Tables u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]]; u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]]; - + // Data Entries SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]]; TocEntry toc[tocCount] [[comment("Table of contents entries")]]; @@ -104,7 +104,7 @@ if (parsedSize != fileSize) { ## `section.bin` -### Version 8 +### Version 24 ImHex Pattern: @@ -114,7 +114,7 @@ import std.string; import std.core; // === Configuration === -#define EXPECTED_VERSION 8 +#define EXPECTED_VERSION 24 #define MAX_STRING_LENGTH 65535 // === String Structure === @@ -133,8 +133,10 @@ fn format_string(String s) { // === Page Structure === -enum StorageType : u8 { - PageLine = 1 +enum PageElementTag : u8 { + PageLine = 1, + PageImage = 2, + PageHorizontalRule = 3 }; enum WordStyle : u8 { @@ -161,10 +163,29 @@ struct PageLine { BlockStyle blockStyle; }; +struct PageImage { + s16 xPos; + s16 yPos; + String imagePath; + s16 width; + s16 height; +}; + +struct PageHorizontalRule { + s16 xPos; + s16 yPos; + u16 width; + u8 thickness; +}; + struct PageElement { u8 pageElementType; if (pageElementType == 1) { PageLine pageLine [[inline]]; + } else if (pageElementType == 2) { + PageImage pageImage [[inline]]; + } else if (pageElementType == 3) { + PageHorizontalRule horizontalRule [[inline]]; } else { std::error(std::format("Unknown page element type: {}", pageElementType)); } @@ -180,12 +201,12 @@ struct Page { struct SectionBin { // Header u8 version [[comment("Format version"), color("FFD93D")]]; - + // Version validation if (version != EXPECTED_VERSION) { std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION)); } - + // Cache busting parameters s32 fontId; float lineCompression; @@ -194,15 +215,15 @@ struct SectionBin { u16 vieportHeight; u16 pageCount; u32 lutOffset; - + Page page[pageCount]; - + // Validate LUT offset alignment u32 currentOffset = $; if (currentOffset != lutOffset) { std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset)); } - + // Lookup Tables u32 lut[pageCount]; }; diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index ba12589e..5fcda220 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -1,8 +1,11 @@ #include "Page.h" +#include #include #include +#include + void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { block->render(renderer, fontId, xPos + xOffset, yPos + yOffset); } @@ -48,6 +51,47 @@ std::unique_ptr PageImage::deserialize(FsFile& file) { return std::unique_ptr(new PageImage(std::move(ib), xPos, yPos)); } +void PageHorizontalRule::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { + (void)fontId; + if (width == 0 || thickness == 0) { + return; + } + + renderer.drawLine(xPos + xOffset, yPos + yOffset, xPos + xOffset + width - 1, yPos + yOffset, thickness, true); +} + +bool PageHorizontalRule::serialize(FsFile& file) { + serialization::writePod(file, xPos); + serialization::writePod(file, yPos); + serialization::writePod(file, width); + serialization::writePod(file, thickness); + return true; +} + +std::unique_ptr PageHorizontalRule::deserialize(FsFile& file) { + int16_t xPos = 0; + int16_t yPos = 0; + uint16_t width = 0; + uint8_t thickness = 0; + serialization::readPod(file, xPos); + serialization::readPod(file, yPos); + serialization::readPod(file, width); + serialization::readPod(file, thickness); + + if (width == 0 || thickness == 0) { + LOG_ERR("PGE", "Deserialization failed: invalid horizontal rule metadata (width=%u thickness=%u)", width, + thickness); + return nullptr; + } + + auto* rule = new (std::nothrow) PageHorizontalRule(width, thickness, xPos, yPos); + if (!rule) { + LOG_ERR("PGE", "Deserialization failed: could not allocate PageHorizontalRule"); + return nullptr; + } + return std::unique_ptr(rule); +} + void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { for (auto& element : elements) { element->render(renderer, fontId, xOffset, yOffset); @@ -98,6 +142,12 @@ std::unique_ptr Page::deserialize(FsFile& file) { } else if (tag == TAG_PageImage) { auto pi = PageImage::deserialize(file); page->elements.push_back(std::move(pi)); + } else if (tag == TAG_PageHorizontalRule) { + auto rule = PageHorizontalRule::deserialize(file); + if (!rule) { + return nullptr; + } + page->elements.push_back(std::move(rule)); } else { LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag); return nullptr; diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 46814b15..34076433 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -12,7 +12,8 @@ enum PageElementTag : uint8_t { TAG_PageLine = 1, - TAG_PageImage = 2, // New tag + TAG_PageImage = 2, + TAG_PageHorizontalRule = 3, }; // represents something that has been added to a page @@ -55,6 +56,20 @@ class PageImage final : public PageElement { const ImageBlock& getImageBlock() const { return *imageBlock; } }; +class PageHorizontalRule final : public PageElement { + uint16_t width; + uint8_t thickness; + + public: + PageHorizontalRule(uint16_t width, uint8_t thickness, const int16_t xPos, const int16_t yPos) + : PageElement(xPos, yPos), width(width), thickness(thickness) {} + + void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; + bool serialize(FsFile& file) override; + PageElementTag getTag() const override { return TAG_PageHorizontalRule; } + static std::unique_ptr deserialize(FsFile& file); +}; + class Page { public: // the list of block index and line numbers on this page diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 7cbd37e1..30f0ef47 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -10,7 +10,7 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 23; +constexpr uint8_t SECTION_FILE_VERSION = 24; constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) + diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 295f58a7..89184d5d 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -8,7 +8,9 @@ #include #include +#include #include +#include #include "Epub.h" #include "Epub/Page.h" @@ -145,6 +147,68 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) { wordsExtractedInBlock = 0; } +void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) { + if (partWordBufferIndex > 0) { + flushPartWordBuffer(); + } + + if (currentTextBlock) { + const BlockStyle parentBlockStyle = currentTextBlock->getBlockStyle(); + startNewTextBlock(parentBlockStyle); + } + + if (!currentPage) { + currentPage.reset(new (std::nothrow) Page()); + if (!currentPage) { + LOG_ERR("EHP", "Failed to create page for horizontal rule"); + return; + } + currentPageNextY = 0; + } + + const int16_t lineHeight = static_cast(renderer.getLineHeight(fontId) * lineCompression + 0.5f); + const int16_t defaultVerticalSpacing = static_cast(lineHeight / 2); + const int16_t topSpacing = + static_cast((blockStyle.marginTop > 0 ? blockStyle.marginTop : defaultVerticalSpacing) + + (blockStyle.paddingTop > 0 ? blockStyle.paddingTop : 0)); + const int16_t bottomSpacing = + static_cast((blockStyle.marginBottom > 0 ? blockStyle.marginBottom : defaultVerticalSpacing) + + (blockStyle.paddingBottom > 0 ? blockStyle.paddingBottom : 0)); + constexpr uint8_t ruleThickness = 2; + const int16_t availableWidth = + std::max(1, static_cast(viewportWidth - blockStyle.totalHorizontalInset())); + const int16_t width = std::max(1, static_cast(availableWidth / 4)); + const int16_t xPos = static_cast(blockStyle.leftInset() + ((availableWidth - width) / 2)); + const int16_t totalHeight = static_cast(topSpacing + ruleThickness + bottomSpacing); + + if (!currentPage->elements.empty() && currentPageNextY + totalHeight > viewportHeight) { + completePageFn(std::move(currentPage), xpathParagraphIndex, xpathListItemIndex); + completedPageCount++; + currentPage.reset(new (std::nothrow) Page()); + if (!currentPage) { + LOG_ERR("EHP", "Failed to create page after horizontal-rule page break"); + return; + } + currentPageNextY = 0; + } + + currentPageNextY += topSpacing; + + auto pageRule = std::shared_ptr( + new (std::nothrow) PageHorizontalRule(width, ruleThickness, xPos, currentPageNextY)); + if (!pageRule) { + LOG_ERR("EHP", "Failed to create PageHorizontalRule"); + return; + } + currentPage->elements.push_back(pageRule); + currentPageNextY = static_cast(currentPageNextY + ruleThickness + bottomSpacing); + + if (!pendingAnchorId.empty()) { + anchorData.push_back({std::move(pendingAnchorId), static_cast(completedPageCount)}); + pendingAnchorId.clear(); + } +} + void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { auto* self = static_cast(userData); @@ -262,6 +326,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* return; } + if (self->tableDepth == 1 && strcmp(name, "hr") == 0) { + self->depth += 1; + return; + } + if (matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS))) { std::string src; std::string alt; @@ -600,6 +669,25 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle( cssStyle, emSize, static_cast(self->paragraphAlignment), self->viewportWidth); + if (strcmp(name, "hr") == 0) { + auto hrBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Left, self->viewportWidth); + if (!self->embeddedStyle) { + hrBlockStyle.marginLeft = 0; + hrBlockStyle.marginRight = 0; + hrBlockStyle.marginTop = 0; + hrBlockStyle.marginBottom = 0; + hrBlockStyle.paddingLeft = 0; + hrBlockStyle.paddingRight = 0; + hrBlockStyle.paddingTop = 0; + hrBlockStyle.paddingBottom = 0; + hrBlockStyle.textIndentDefined = false; + hrBlockStyle.textIndent = 0; + } + self->emitHorizontalRule(hrBlockStyle); + self->depth += 1; + return; + } + if (matches(name, HEADER_TAGS, std::size(HEADER_TAGS))) { self->currentCssStyle = cssStyle; auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center, self->viewportWidth); diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 623dc858..989184ea 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -91,6 +91,7 @@ class ChapterHtmlSlimParser { void startNewTextBlock(const BlockStyle& blockStyle); void flushPartWordBuffer(); void makePages(); + void emitHorizontalRule(const BlockStyle& blockStyle); // XML callbacks 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);