Merge pull request #131 from jpirnay/fix-css-text-indent

feat: Honor css defined paragraph intent (and clamp it if necessary)
This commit is contained in:
jpirnay
2026-04-25 11:28:40 +02:00
committed by GitHub
3 changed files with 34 additions and 27 deletions
+21 -22
View File
@@ -291,13 +291,12 @@ std::vector<size_t> 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<int>(static_cast<int>(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<int>(static_cast<int>(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<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
std::vector<int>& splitPrefixWordIndexes,
std::vector<bool>& 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<int>(static_cast<int>(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<int>(static_cast<int>(blockStyle.textIndent), -(pageWidth - 1)), pageWidth - 1)
: 0;
// Calculate total word width for this line, count actual word gaps,
+1 -1
View File
@@ -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
+12 -4
View File
@@ -1,5 +1,6 @@
#pragma once
#include <algorithm>
#include <cstdint>
#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<int16_t>(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