feat: ports hr tag rendering from crossink (#2117)

This commit is contained in:
Julia
2026-05-23 09:47:16 -04:00
committed by GitHub
parent f39ba7037f
commit 7accc607af
6 changed files with 192 additions and 17 deletions
+36 -15
View File
@@ -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];
};
+50
View File
@@ -1,8 +1,11 @@
#include "Page.h"
#include <GfxRenderer.h>
#include <Logging.h>
#include <Serialization.h>
#include <new>
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> PageImage::deserialize(FsFile& file) {
return std::unique_ptr<PageImage>(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> 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<PageHorizontalRule>(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> 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;
+16 -1
View File
@@ -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<PageHorizontalRule> deserialize(FsFile& file);
};
class Page {
public:
// the list of block index and line numbers on this page
+1 -1
View File
@@ -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) +
@@ -8,7 +8,9 @@
#include <XmlParserUtils.h>
#include <expat.h>
#include <algorithm>
#include <iterator>
#include <new>
#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<int16_t>(renderer.getLineHeight(fontId) * lineCompression + 0.5f);
const int16_t defaultVerticalSpacing = static_cast<int16_t>(lineHeight / 2);
const int16_t topSpacing =
static_cast<int16_t>((blockStyle.marginTop > 0 ? blockStyle.marginTop : defaultVerticalSpacing) +
(blockStyle.paddingTop > 0 ? blockStyle.paddingTop : 0));
const int16_t bottomSpacing =
static_cast<int16_t>((blockStyle.marginBottom > 0 ? blockStyle.marginBottom : defaultVerticalSpacing) +
(blockStyle.paddingBottom > 0 ? blockStyle.paddingBottom : 0));
constexpr uint8_t ruleThickness = 2;
const int16_t availableWidth =
std::max<int16_t>(1, static_cast<int16_t>(viewportWidth - blockStyle.totalHorizontalInset()));
const int16_t width = std::max<int16_t>(1, static_cast<int16_t>(availableWidth / 4));
const int16_t xPos = static_cast<int16_t>(blockStyle.leftInset() + ((availableWidth - width) / 2));
const int16_t totalHeight = static_cast<int16_t>(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<PageHorizontalRule>(
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<int16_t>(currentPageNextY + ruleThickness + bottomSpacing);
if (!pendingAnchorId.empty()) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
pendingAnchorId.clear();
}
}
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<ChapterHtmlSlimParser*>(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<CssTextAlign>(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);
@@ -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);