diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index f24550c7..3ad9337d 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -291,13 +291,12 @@ std::vector ParsedText::computeLineBreaks(const GfxRenderer& renderer, c } // Calculate first line indent (only for left/justified text). - // Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on. - // Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies — - // it is structural (positions the bullet/marker), not decorative. + // Explicit CSS text-indent always applies — author intent overrides the extraParagraphSpacing + // toggle. Only the implicit EmSpace fallback in applyParagraphIndent() is gated on it. const int firstLineIndent = - blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) && + blockStyle.textIndentDefined && (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) - ? blockStyle.textIndent + ? std::min(std::max(static_cast(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1) : 0; // Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation. @@ -428,9 +427,9 @@ size_t ParsedText::computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, c } const int firstLineIndent = - lineStartIndex == 0 && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) && + lineStartIndex == 0 && blockStyle.textIndentDefined && (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) - ? blockStyle.textIndent + ? std::min(std::max(static_cast(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1) : 0; const int effectivePageWidth = pageWidth - firstLineIndent; @@ -471,15 +470,17 @@ size_t ParsedText::computeSingleLineBreakNoHyphen(const GfxRenderer& renderer, c } void ParsedText::applyParagraphIndent() { - if (extraParagraphSpacing || words.empty()) { + if (words.empty()) { return; } if (blockStyle.textIndentDefined) { - // CSS text-indent is explicitly set (even if 0) - don't use fallback EmSpace - // The actual indent positioning is handled in extractLine() - } else if (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) { - // No CSS text-indent defined - use EmSpace fallback for visual indent + // CSS text-indent is explicitly set (even if 0) - don't use fallback EmSpace. + // The actual indent positioning is handled in extractLine(). + } else if (!extraParagraphSpacing && + (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)) { + // No CSS text-indent defined - use EmSpace fallback only when extra paragraph spacing is off, + // so paragraphs remain visually distinguishable. words.front().insert(0, "\xe2\x80\x83"); } } @@ -492,13 +493,12 @@ std::vector ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r std::vector& splitPrefixWordIndexes, std::vector& splitInsertedHyphen) { // Calculate first line indent (only for left/justified text). - // Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on. - // Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies — - // it is structural (positions the bullet/marker), not decorative. + // Explicit CSS text-indent always applies — author intent overrides the extraParagraphSpacing + // toggle. Only the implicit EmSpace fallback in applyParagraphIndent() is gated on it. const int firstLineIndent = - blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) && + blockStyle.textIndentDefined && (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) - ? blockStyle.textIndent + ? std::min(std::max(static_cast(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1) : 0; // Pre-compute inter-word gaps to avoid repeated codepoint scanning and renderer @@ -785,14 +785,13 @@ ParsedText::LineProcessResult ParsedText::extractLine( const size_t lineWordCount = lineBreak - lastBreakAt; // Calculate first line indent (only for left/justified text). - // Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on. - // Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies — - // it is structural (positions the bullet/marker), not decorative. + // Explicit CSS text-indent always applies — author intent overrides the extraParagraphSpacing + // toggle. Only the implicit EmSpace fallback in applyParagraphIndent() is gated on it. const bool isFirstLine = breakIndex == 0; const int firstLineIndent = - isFirstLine && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) && + isFirstLine && blockStyle.textIndentDefined && (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) - ? blockStyle.textIndent + ? std::min(std::max(static_cast(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1) : 0; // Calculate total word width for this line, count actual word gaps, diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 157f35dd..d6cdc424 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 = 22; +constexpr uint8_t SECTION_FILE_VERSION = 23; constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION sizeof(int) + // fontId sizeof(float) + // lineCompression diff --git a/lib/Epub/Epub/blocks/BlockStyle.h b/lib/Epub/Epub/blocks/BlockStyle.h index db56f6b7..83869b8c 100644 --- a/lib/Epub/Epub/blocks/BlockStyle.h +++ b/lib/Epub/Epub/blocks/BlockStyle.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "Epub/css/CssStyle.h" @@ -8,6 +9,12 @@ * BlockStyle - Block-level styling properties */ struct BlockStyle { + // Upper bound (in em) for any single side's horizontal margin or padding. + // Some EPUBs apply huge em-based insets to chapter-opener classes; without a + // cap, effectiveWidth collapses to 1-2 words per line and justification dumps + // the remaining space into a single gap. + static constexpr float MAX_HORIZONTAL_INSET_EM = 4.0f; + CssTextAlign alignment = CssTextAlign::Justify; // Spacing (in pixels) @@ -75,16 +82,17 @@ struct BlockStyle { const uint16_t viewportWidth = 0) { BlockStyle blockStyle; const float vw = viewportWidth; + const auto maxHorizontalInsetPx = static_cast(emSize * MAX_HORIZONTAL_INSET_EM); // Resolve all CssLength values to pixels using the current font's em size and viewport width blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize, vw); blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize, vw); - blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize, vw); - blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize, vw); + blockStyle.marginLeft = std::min(cssStyle.marginLeft.toPixelsInt16(emSize, vw), maxHorizontalInsetPx); + blockStyle.marginRight = std::min(cssStyle.marginRight.toPixelsInt16(emSize, vw), maxHorizontalInsetPx); blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize, vw); blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw); - blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize, vw); - blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize, vw); + blockStyle.paddingLeft = std::min(cssStyle.paddingLeft.toPixelsInt16(emSize, vw), maxHorizontalInsetPx); + blockStyle.paddingRight = std::min(cssStyle.paddingRight.toPixelsInt16(emSize, vw), maxHorizontalInsetPx); // For textIndent: if it's a percentage we can't resolve (no viewport width), // leave textIndentDefined=false so the EmSpace fallback in applyParagraphIndent() is used