From f8a9f1f07a1e1167a42bc5fb64c766be0529583d Mon Sep 17 00:00:00 2001 From: martin brook Date: Mon, 23 Feb 2026 20:17:37 +0000 Subject: [PATCH] fix: image centering bleed (#1096) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * Fixes #1026 * Added a reproducer chapter to the epub generator for this case * The fix is in endElement — when leaving a block/header element that had an empty text block, reset the alignment to the user's default so it doesn't bleed into the next sibling. This preserves accumulated margins from parent elements while preventing stale alignment from carrying across. ## Additional Context ### Before fix ![20260222_210029262](https://github.com/user-attachments/assets/263e4608-18cf-418b-871a-1c9a71822bdf) ![20260222_210040995](https://github.com/user-attachments/assets/9f0fdea1-5abf-4f1c-b35d-d35c8309456a) ![20260222_210052640](https://github.com/user-attachments/assets/b77dbadc-f347-400b-994a-17d0f5f073d8) ### After fix ![20260222_211037007](https://github.com/user-attachments/assets/294e15b3-ee40-4c21-8f5b-bd6b40d43d8d) ![20260222_211045139](https://github.com/user-attachments/assets/74107cf9-08a2-4737-be7f-ed0b5648ca6f) --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY **_ --- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 14 +++++++++++ scripts/generate_test_epub.py | 22 ++++++++++++++++++ test/epubs/test_jpeg_images.epub | Bin 322106 -> 322942 bytes test/epubs/test_png_images.epub | Bin 97163 -> 97996 bytes 4 files changed, 36 insertions(+) diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 90bf8fee1..d502933d1 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -789,6 +789,20 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n if (headerOrBlockTag) { 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(); + style.textAlignDefined = false; + style.alignment = (self->paragraphAlignment == static_cast(CssTextAlign::None)) + ? CssTextAlign::Justify + : static_cast(self->paragraphAlignment); + self->currentTextBlock->setBlockStyle(style); + } } } diff --git a/scripts/generate_test_epub.py b/scripts/generate_test_epub.py index 0d465e109..e585420c6 100644 --- a/scripts/generate_test_epub.py +++ b/scripts/generate_test_epub.py @@ -607,6 +607,17 @@ def main(): Cache test 2

Navigate back to Page A - it should load faster from cache.

"""), [('cache_test_2.jpg', images['cache_test_2.jpg'])]), + ("9. Alignment Bleed", make_chapter("Image Centering Bleed Test", """ +

Tests that image centering does not bleed into following text blocks (issue #1026).

+

Set Paragraph Alignment to Justify and Embedded Style to OFF before testing.

+

All paragraphs below the images should be justified, not centered.

+

+

Test image

+
+

FIRST PARAGRAPH after image. This paragraph follows an empty heading and an image-only paragraph. With the bug present, this text appears centered instead of justified because the empty heading's default Center alignment bleeds through the chain of empty text blocks. Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod tempor.

+

SECOND PARAGRAPH in the same div. This paragraph should always be justified because the first paragraph's text block was flushed. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia.

+
+"""), []), # centering_test.jpg already included by chapter 4 ] create_epub(OUTPUT_DIR / 'test_jpeg_images.epub', 'JPEG Image Tests', jpeg_chapters) @@ -661,6 +672,17 @@ def main(): Cache test 2

Navigate back to Page A - it should load faster from cache.

"""), [('cache_test_2.png', images['cache_test_2.png'])]), + ("9. Alignment Bleed", make_chapter("Image Centering Bleed Test", """ +

Tests that image centering does not bleed into following text blocks (issue #1026).

+

Set Paragraph Alignment to Justify and Embedded Style to OFF before testing.

+

All paragraphs below the images should be justified, not centered.

+

+

Test image

+
+

FIRST PARAGRAPH after image. This paragraph follows an empty heading and an image-only paragraph. With the bug present, this text appears centered instead of justified because the empty heading's default Center alignment bleeds through the chain of empty text blocks. Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod tempor.

+

SECOND PARAGRAPH in the same div. This paragraph should always be justified because the first paragraph's text block was flushed. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia.

+
+"""), []), # centering_test.png already included by chapter 4 ] create_epub(OUTPUT_DIR / 'test_png_images.epub', 'PNG Image Tests', png_chapters) diff --git a/test/epubs/test_jpeg_images.epub b/test/epubs/test_jpeg_images.epub index 3f2b1f77606fce58d111ee9bf2f61a737b0df6f1..a99251f06497a11a11e9b80dd92da6d661b89b43 100644 GIT binary patch delta 2304 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 delta 1393 zcmX@}m$mylD{p`|Gm8iV2&`D-F_G7t8AQ3?15ukb7=swWj9R8sf?$T1hzdlcP}EEY zEV9aw4I;AHXul-yibWnV34NtkE-)}KC{KQvE;4zt&qJ`fa^F}f5M%Q7^nW0F^NkF? zA~56SEhUHve0N@CgG9FfbZ0aJ3vD;@WV|i}7UPLwl!u7v#WKDX28+oRGAc6if$U`4 zU^(FfBLl->j_G=(jH>k~9@O(sh_$|3vdiPTT7%YZA&JKrTeaGbGof?dj2+%jc1LSbHmSb$=?qt(VX>K z!{(HQ42Wv)+MYg`;d z;;o*$q-$<6N|X0WnS=3}&jiWCi3&-@o_& zuSEI8WsS!*!X+=d7vE&NuB%(1ZngEs`SrR}A3OWxH@rN1phvV)Tf_5V#NOxS0r|Ya z1=E@DJhh2Ac2=v#;r7emgmC5?S9v0`92{oueV}|R`mo5IuMFk3%-gF@^C$fNBy!~} zLw%_+^VgJZiym{lpPSD0rHo7< zXVX>wFvF5A2LmVx*!Uc(_F-gTkYQ$EsOM*3VDNW!3JBKEODxl?$SBFpnR@Abev^R+ z`-k22N2A|oADy4Tl~#QD%_E;@o8Ck$^b*eTT)F7~U%5?kdvxBoRMyV_Cb+Mr&Qh#k z+O{CK<0=bknSz$91^*J5eE#hxLzT}?r>Fl^fZ`Tmp~i`SNGZPTOSj_MKH|*t`6_&2{ysd?F`43(?%vV*QkB=9;6CvMq-< zT4Za_x%;iUCVf-(CFTcFa~4`wK3li6r_)$7^VN>Y>?aRpsBc|8fpvdq@D}bEF|CJ8 zMcUsDJ&OaaGMR7n&DoQg?px+{`!s7@&)xb1jqQxBXX~q2y_5S7l}>clXBPrE@ug}LIjCjZ~KaC`8a&?P%t z)~=DUDcv}6`@^}l`?k;7+WCNIa?8P_+)W0CzNhLL0=yZSM3~_PM@)X=Y8?;{ggF?d zzpP+X0cFwY0+o!8U`9wKqdk~0t&-6V%(xE|QLJM00*mBTLDg)iV)O)y{DFzsRYTQu zS3}jDt!DH9tC6UIibT~g`hrDP!9?EFK-HMnLe*5(LPd_$GTJME(hZ{MU|&7gP~L17k=DBYIK`@MdKLDG+54Wmv(=z;L#dk%55$0N6K2 A;{X5v