From 92f1b8b1ae2b079dbae78311f6e3a1442e0cf8e2 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Wed, 17 Jun 2026 00:16:28 +0100 Subject: [PATCH] test(render): cover text-state invalidation around inline graphics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The paragraph render handler invalidates its tracked font/colour after an inline image or shape so the following text span re-emits Tf/rg rather than trusting state the graphic disturbed. ParagraphTextStateDedupTest covered single-style dedup and multi-style re-emit but never paired same-style text across an inline graphic — the exact case the invalidate guards. Add a shape case and an image case (same-style text on both sides of the graphic) that assert two setFont operators. Both use the rich-text builder so the runs keep their declared [text, graphic, text] order. Mutation-checked: removing either invalidate() collapses the count to one and fails the matching case. --- .../pdf/ParagraphTextStateDedupTest.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/test/java/com/demcha/compose/document/backend/fixed/pdf/ParagraphTextStateDedupTest.java b/src/test/java/com/demcha/compose/document/backend/fixed/pdf/ParagraphTextStateDedupTest.java index 64efa399f..f50129656 100644 --- a/src/test/java/com/demcha/compose/document/backend/fixed/pdf/ParagraphTextStateDedupTest.java +++ b/src/test/java/com/demcha/compose/document/backend/fixed/pdf/ParagraphTextStateDedupTest.java @@ -4,12 +4,17 @@ import com.demcha.compose.GraphCompose; import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.image.DocumentImageData; +import com.demcha.compose.document.style.DocumentColor; import org.apache.pdfbox.Loader; import org.apache.pdfbox.contentstream.operator.Operator; import org.apache.pdfbox.pdfparser.PDFStreamParser; import org.apache.pdfbox.pdmodel.PDDocument; import org.junit.jupiter.api.Test; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; @@ -80,6 +85,65 @@ void multiStyleParagraphReEmitsFontOnEachStyleChange() throws Exception { } } + @Test + void sameStyleTextAroundAnInlineShapeReEmitsTheFont() throws Exception { + byte[] pdf; + try (DocumentSession session = GraphCompose.document() + .pageSize(400, 200) + .margin(24, 24, 24, 24) + .create()) { + // Two same-style text runs with an inline shape between them. The + // shape ends the text block and runs its own graphics-state/colour + // ops, so the handler must invalidate the tracked font — otherwise + // the second run trusts the stale state, skips its Tf, and draws + // through whatever the shape left set. Same style, so without the + // invalidate the two runs would dedup to a single setFont. + session.pageFlow(flow -> flow.addParagraph(p -> p.rich(r -> r + .plain("alpha ") + .diamond(8, DocumentColor.rgb(196, 30, 58)) + .plain(" bravo")))); + pdf = session.toPdfBytes(); + } + + try (PDDocument document = Loader.loadPDF(pdf)) { + assertThat(operatorCount(document, "Tf")) + .describedAs("the text span after an inline shape must re-emit setFont") + .isEqualTo(2); + } + } + + @Test + void sameStyleTextAroundAnInlineImageReEmitsTheFont() throws Exception { + DocumentImageData dot = DocumentImageData.fromBytes(onePixelPng()); + byte[] pdf; + try (DocumentSession session = GraphCompose.document() + .pageSize(400, 200) + .margin(24, 24, 24, 24) + .create()) { + // Same guard for the inline-image span: drawImage breaks the text + // block, so the text after it must re-emit its setFont. + session.pageFlow(flow -> flow.addParagraph(p -> p.rich(r -> r + .plain("alpha ") + .image(dot, 8, 8) + .plain(" bravo")))); + pdf = session.toPdfBytes(); + } + + try (PDDocument document = Loader.loadPDF(pdf)) { + assertThat(operatorCount(document, "Tf")) + .describedAs("the text span after an inline image must re-emit setFont") + .isEqualTo(2); + } + } + + private static byte[] onePixelPng() throws Exception { + BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + image.setRGB(0, 0, java.awt.Color.WHITE.getRGB()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ImageIO.write(image, "png", output); + return output.toByteArray(); + } + private static int operatorCount(PDDocument document, String operatorName) throws IOException { int count = 0; for (var page : document.getPages()) {