diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 499e9437..ec9fef71 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -12,7 +12,7 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 20; +constexpr uint8_t SECTION_FILE_VERSION = 21; constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION sizeof(int) + // fontId sizeof(float) + // lineCompression diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index a7982a53..965d892b 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -176,24 +176,17 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) { if (currentTextBlock) { // already have a text block running and it is empty - just reuse it if (currentTextBlock->isEmpty()) { - // Merge with existing block style to accumulate CSS styling from parent block elements. - // This handles cases like

text

where the - // div's margin should be preserved, even though it has no direct text content. BlockStyle incoming = blockStyle; - const bool brGapPending = currentTextBlock->getBlockStyle().fromBrElement; + const BlockStyle& currentStyle = currentTextBlock->getBlockStyle(); + const bool brGapPending = currentStyle.fromBrElement; if (brGapPending) { // The empty block was created by a
section separator. Inject a full line of // blank space before the following paragraph so the scene/section break is visible. // This only fires when the
block stayed empty (i.e. no inline text was added). const int16_t lineHeight = static_cast(renderer.getLineHeight(fontId) * lineCompression + 0.5f); - incoming.marginTop = static_cast(incoming.marginTop + lineHeight); + incoming.marginTop = static_cast(currentStyle.marginTop + incoming.marginTop + lineHeight); } - - BlockStyle merged = currentTextBlock->getBlockStyle().getCombinedBlockStyle(incoming); - // Preserve only whether the current empty block still represents
separators. - // This lets consecutive
accumulate one line each without leaking the flag to real content blocks. - merged.fromBrElement = blockStyle.fromBrElement; - currentTextBlock->setBlockStyle(merged); + currentTextBlock->setBlockStyle(incoming); if (!pendingAnchorId.empty()) { if (std::find(tocAnchors.begin(), tocAnchors.end(), pendingAnchorId) != tocAnchors.end()) { @@ -475,19 +468,30 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } const bool hasCssHeight = imgStyle.hasImageHeight(); const bool hasCssWidth = imgStyle.hasImageWidth(); + int containerWidth = self->viewportWidth; + if (self->currentTextBlock) { + const int inset = self->currentTextBlock->getBlockStyle().totalHorizontalInset(); + if (inset > 0 && inset < self->viewportWidth) { + containerWidth = self->viewportWidth - inset; + } + } else if (!self->blockStyleStack.empty()) { + const int inset = self->blockStyleStack.back().totalHorizontalInset(); + if (inset > 0 && inset < self->viewportWidth) { + containerWidth = self->viewportWidth - inset; + } + } if (hasCssHeight && hasCssWidth && dims.width > 0 && dims.height > 0) { - // Both CSS height and width set: resolve both, then clamp to viewport preserving requested ratio + // Both CSS height and width set: resolve both, then clamp to the current container preserving ratio. displayHeight = static_cast( imgStyle.imageHeight.toPixels(emSize, static_cast(self->viewportHeight)) + 0.5f); - displayWidth = static_cast( - imgStyle.imageWidth.toPixels(emSize, static_cast(self->viewportWidth)) + 0.5f); + displayWidth = + static_cast(imgStyle.imageWidth.toPixels(emSize, static_cast(containerWidth)) + 0.5f); if (displayHeight < 1) displayHeight = 1; if (displayWidth < 1) displayWidth = 1; - if (displayWidth > self->viewportWidth || displayHeight > self->viewportHeight) { - float scaleX = (displayWidth > self->viewportWidth) - ? static_cast(self->viewportWidth) / displayWidth - : 1.0f; + if (displayWidth > containerWidth || displayHeight > self->viewportHeight) { + float scaleX = + (displayWidth > containerWidth) ? static_cast(containerWidth) / displayWidth : 1.0f; float scaleY = (displayHeight > self->viewportHeight) ? static_cast(self->viewportHeight) / displayHeight : 1.0f; @@ -512,8 +516,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* static_cast(displayHeight * (static_cast(dims.width) / dims.height) + 0.5f); if (displayWidth < 1) displayWidth = 1; } - if (displayWidth > self->viewportWidth) { - displayWidth = self->viewportWidth; + if (displayWidth > containerWidth) { + displayWidth = containerWidth; // Rescale height to preserve aspect ratio when width is clamped displayHeight = static_cast(displayWidth * (static_cast(dims.height) / dims.width) + 0.5f); @@ -522,10 +526,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (displayWidth < 1) displayWidth = 1; LOG_DBG("EHP", "Display size from CSS height: %dx%d", displayWidth, displayHeight); } else if (hasCssWidth && !hasCssHeight && dims.width > 0 && dims.height > 0) { - // Use CSS width (resolve % against viewport width) and derive height from aspect ratio - displayWidth = static_cast( - imgStyle.imageWidth.toPixels(emSize, static_cast(self->viewportWidth)) + 0.5f); - if (displayWidth > self->viewportWidth) displayWidth = self->viewportWidth; + // Use CSS width (resolve % against container width) and derive height from aspect ratio. + displayWidth = + static_cast(imgStyle.imageWidth.toPixels(emSize, static_cast(containerWidth)) + 0.5f); + if (displayWidth > containerWidth) displayWidth = containerWidth; if (displayWidth < 1) displayWidth = 1; displayHeight = static_cast(displayWidth * (static_cast(dims.height) / dims.width) + 0.5f); @@ -539,8 +543,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (displayHeight < 1) displayHeight = 1; LOG_DBG("EHP", "Display size from CSS width: %dx%d", displayWidth, displayHeight); } else { - // Scale to fit viewport while maintaining aspect ratio - int maxWidth = self->viewportWidth; + // Scale to fit the current container while maintaining aspect ratio. + int maxWidth = containerWidth; int maxHeight = self->viewportHeight; float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f; float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f; @@ -654,7 +658,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* // Fallback to alt text if image processing fails if (!alt.empty()) { alt = "[Image: " + alt + "]"; - self->startNewTextBlock(centeredBlockStyle); + const BlockStyle altBlockStyle = self->blockStyleStack.empty() + ? centeredBlockStyle + : self->blockStyleStack.back().getCombinedBlockStyle(centeredBlockStyle); + self->startNewTextBlock(altBlockStyle); self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth); self->depth += 1; self->characterData(userData, alt.c_str(), alt.length()); @@ -768,7 +775,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (self->embeddedStyle && cssStyle.hasTextAlign()) { headerBlockStyle.alignment = cssStyle.textAlign; } - self->startNewTextBlock(headerBlockStyle); + const BlockStyle accumulatedHeaderBlockStyle = self->blockStyleStack.back().getCombinedBlockStyle(headerBlockStyle); + self->blockStyleStack.push_back(accumulatedHeaderBlockStyle); + self->startNewTextBlock(accumulatedHeaderBlockStyle); self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); self->updateEffectiveInlineStyle(); } else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { @@ -780,7 +789,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* blockStyle.alignment = cssStyle.textAlign; blockStyle.textAlignDefined = true; } - self->startNewTextBlock(blockStyle); + const BlockStyle accumulatedBlockStyle = self->blockStyleStack.back().getCombinedBlockStyle(blockStyle); + self->blockStyleStack.push_back(accumulatedBlockStyle); + self->startNewTextBlock(accumulatedBlockStyle); self->updateEffectiveInlineStyle(); self->skipTextUntilDepth = self->depth; @@ -814,7 +825,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* blockStyle.alignment = cssStyle.textAlign; blockStyle.textAlignDefined = true; } - self->startNewTextBlock(blockStyle); + const BlockStyle accumulatedBlockStyle = self->blockStyleStack.back().getCombinedBlockStyle(blockStyle); + self->blockStyleStack.push_back(accumulatedBlockStyle); + self->startNewTextBlock(accumulatedBlockStyle); self->updateEffectiveInlineStyle(); if (strcmp(name, "li") == 0) { @@ -1258,35 +1271,28 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n self->currentCssStyle.reset(); self->updateEffectiveInlineStyle(); - // Reset alignment on empty text blocks to prevent stale alignment from bleeding - // into the next sibling element. This fixes issue #1026 where an empty

(default - // Center) followed by an image-only

causes Center to persist through the chain - // of empty block reuse into subsequent text paragraphs. - // Margins/padding are preserved so parent element spacing still accumulates correctly. - if (self->currentTextBlock && self->currentTextBlock->isEmpty()) { - auto style = self->currentTextBlock->getBlockStyle(); - // Keep alignment only when closing the
separator itself so subsequent text - // within the same block container stays aligned. Reset alignment when closing - // other block tags (e.g. div/p) to avoid leaking centered/right alignment globally. - const bool preserveForBrClose = style.fromBrElement && strcmp(name, "br") == 0; - if (!preserveForBrClose) { - style.textAlignDefined = false; - style.alignment = (self->paragraphAlignment == static_cast(CssTextAlign::None)) - ? CssTextAlign::Justify - : static_cast(self->paragraphAlignment); - self->currentTextBlock->setBlockStyle(style); + if (strcmp(name, "br") != 0 && self->blockStyleStack.size() > 1) { + self->blockStyleStack.pop_back(); + if (self->currentTextBlock && self->currentTextBlock->isEmpty()) { + self->currentTextBlock->setBlockStyle(self->blockStyleStack.back()); } } } } bool ChapterHtmlSlimParser::parseAndBuildPages() { + BlockStyle rootBlockStyle; + rootBlockStyle.alignment = (this->paragraphAlignment == static_cast(CssTextAlign::None)) + ? CssTextAlign::Justify + : static_cast(this->paragraphAlignment); + blockStyleStack.clear(); + blockStyleStack.reserve(8); + blockStyleStack.push_back(rootBlockStyle); + auto paragraphAlignmentBlockStyle = BlockStyle(); paragraphAlignmentBlockStyle.textAlignDefined = true; - // Resolve None sentinel to Justify for initial block (no CSS context yet) - const auto align = (this->paragraphAlignment == static_cast(CssTextAlign::None)) - ? CssTextAlign::Justify - : static_cast(this->paragraphAlignment); + // Resolve None sentinel to Justify for the initial block using the root block style. + const auto align = rootBlockStyle.alignment; paragraphAlignmentBlockStyle.alignment = align; startNewTextBlock(paragraphAlignmentBlockStyle); diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index ecda8da1..f63b7fb3 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -65,6 +65,7 @@ class ChapterHtmlSlimParser { bool hasUnderline = false, underline = false; }; std::vector inlineStyleStack; + std::vector blockStyleStack; CssStyle currentCssStyle; bool effectiveBold = false; bool effectiveItalic = false;