From 4d289fc17b00036951319265a9ec51d0757f5fb1 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 6 Jul 2026 10:29:07 +0100 Subject: [PATCH] test: remove the legacy ECS test layer and its harness The Entity-Component-System render/layout pipeline is dead on the canonical path (DocumentSession -> LayoutCompiler -> LayoutGraph -> FixedLayoutBackend); it was exercised only by a test harness and the suites driving it. Remove the harness (EngineComposerHarness + the engine-assembly builders), the ecs render-pipeline tests, the qa integration suites and root ECS unit tests that depend on them, and the layout-snapshot resources they alone consumed. Every feature they touched is already covered by a canonical DocumentSession test. Two suites tested live code through the harness, so their coverage is preserved harness-free: barcode rendering gains a canonical BarcodeRenderTest, and the shared image source/metadata cache (ImageSourceCache, on the canonical image path) gains a harness-free ImageDataCacheTest next to the cache in core. render-pdf no longer publishes a tests-classifier jar (its only consumer was the harness); qa drops that dependency. The core tests-classifier jar stays -- a few render-pdf backend tests still use its shared visual-test helper. --- qa/pom.xml | 13 +- .../document/api/BarcodeRenderTest.java | 38 + .../components/ComponentBuilderTest.java | 189 -- .../content/ImageDataCacheTest.java | 99 - .../EngineComposerHarnessContractTest.java | 63 - ...gineComposerHarnessLayoutSnapshotTest.java | 178 - ...mposerHarnessMutationInvalidationTest.java | 69 - .../core/NewFeaturesIntegrationTest.java | 476 --- .../integration/BlockTextIntegrationTest.java | 227 -- .../integration/CircleIntegrationTest.java | 163 - .../FeatureShowcaseRenderTest.java | 376 --- .../integration/ImageIntegrationTest.java | 252 -- .../LargeContainerIntegrationTest.java | 88 - .../integration/LineIntegrationTest.java | 296 -- .../PageBreakerIntegrationTest.java | 382 --- .../RepositoryShowcaseRenderTest.java | 349 -- .../SingleLineTextIntegrationTest.java | 84 - .../TableColSpanIntegrationTest.java | 250 -- .../TableLayoutIntegrationTest.java | 58 - .../TableMultilineIntegrationTest.java | 67 - .../TablePaginationIntegrationTest.java | 75 - .../TableStylingIntegrationTest.java | 59 - .../TextPaginationIntegrationTest.java | 65 - .../render/RenderHandlerRegistryTest.java | 56 - .../render/guides/GuidesRendererTest.java | 158 - .../font/FontLibraryIntegrationTest.java | 99 - .../layout/LayoutSnapshotExtractorTest.java | 287 -- .../integration/container_split.json | 107 - .../features/barcode_showcase_render.json | 557 ---- .../features/document_features_paginated.json | 797 ----- .../integration/massive_text.json | 137 - .../repository_showcase_render.json | 647 ---- .../integration/table_pagination_test.json | 2927 ----------------- render-pdf/pom.xml | 36 +- .../render/pdf/ecs/PdfRenderSessionTest.java | 97 - .../PdfRenderingSystemECSDispatchTest.java | 136 - .../PdfRenderingSystemECSImageCacheTest.java | 102 - .../handlers/PdfLinkRenderHandlerTest.java | 54 - .../PdfTableRowRenderHandlerTest.java | 176 - .../testsupport/EngineComposerHarness.java | 576 ---- .../engine/assembly/BarcodeBuilder.java | 165 - .../engine/assembly/BarcodeBuilderTest.java | 231 -- .../engine/assembly/BlockTextBuilder.java | 816 ----- .../engine/assembly/BlockTextBuilderTest.java | 209 -- .../engine/assembly/CircleBuilder.java | 45 - .../engine/assembly/ComponentBuilder.java | 232 -- .../assembly/DisplayUrlTextBuilder.java | 16 - .../engine/assembly/DividerBuilder.java | 113 - .../engine/assembly/ElementBuilder.java | 52 - .../engine/assembly/HContainerBuilder.java | 46 - .../engine/assembly/ImageBuilder.java | 125 - .../engine/assembly/ImageBuilderTest.java | 160 - .../engine/assembly/LineBuilder.java | 52 - .../engine/assembly/LineBuilderTest.java | 42 - .../engine/assembly/LinkBuilder.java | 36 - .../engine/assembly/ModuleBuilder.java | 97 - .../engine/assembly/PageBreakBuilder.java | 39 - .../engine/assembly/RectangleBuilder.java | 22 - .../engine/assembly/RowBuilder.java | 38 - .../engine/assembly/TableBuilder.java | 557 ---- .../engine/assembly/TableBuilderTest.java | 466 --- .../engine/assembly/TextBuilder.java | 111 - .../engine/assembly/TextBuilderTest.java | 95 - .../engine/assembly/VContainerBuilder.java | 38 - .../engine/assembly/container/Box.java | 8 - .../assembly/container/BuildEntity.java | 29 - .../assembly/container/ContainerBuilder.java | 83 - .../engine/assembly/container/EmptyBox.java | 85 - .../assembly/container/EntityBuilderBase.java | 75 - .../assembly/container/EntityCreator.java | 59 - .../engine/assembly/container/Layout.java | 100 - .../assembly/container/ShapeBuilderBase.java | 114 - .../engine/assembly/container/StackAxis.java | 11 - .../assembly/container/package-info.java | 7 - .../content/ImageDataCacheTest.java | 83 + .../engine/core/SystemRegistryTest.java | 64 - .../ParentContainerUpdaterTest.java | 98 - .../engine/render/EntityRenderOrderTest.java | 80 - 78 files changed, 126 insertions(+), 15438 deletions(-) create mode 100644 qa/src/test/java/com/demcha/compose/document/api/BarcodeRenderTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/components/ComponentBuilderTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/components/content/ImageDataCacheTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessContractTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessLayoutSnapshotTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessMutationInvalidationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/core/NewFeaturesIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/BlockTextIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/CircleIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/FeatureShowcaseRenderTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/ImageIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/LargeContainerIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/LineIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/PageBreakerIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/RepositoryShowcaseRenderTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/SingleLineTextIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/TableColSpanIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/TableLayoutIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/TableMultilineIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/TablePaginationIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/TableStylingIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/integration/TextPaginationIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/render/RenderHandlerRegistryTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/engine/render/guides/GuidesRendererTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java delete mode 100644 qa/src/test/java/com/demcha/testing/layout/LayoutSnapshotExtractorTest.java delete mode 100644 qa/src/test/resources/layout-snapshots/integration/container_split.json delete mode 100644 qa/src/test/resources/layout-snapshots/integration/features/barcode_showcase_render.json delete mode 100644 qa/src/test/resources/layout-snapshots/integration/features/document_features_paginated.json delete mode 100644 qa/src/test/resources/layout-snapshots/integration/massive_text.json delete mode 100644 qa/src/test/resources/layout-snapshots/integration/repository_showcase_render.json delete mode 100644 qa/src/test/resources/layout-snapshots/integration/table_pagination_test.json delete mode 100644 render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderSessionTest.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderingSystemECSDispatchTest.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderingSystemECSImageCacheTest.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfLinkRenderHandlerTest.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfTableRowRenderHandlerTest.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/EngineComposerHarness.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BarcodeBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BarcodeBuilderTest.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilderTest.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/CircleBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ComponentBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/DisplayUrlTextBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/DividerBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ElementBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/HContainerBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ImageBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ImageBuilderTest.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LineBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LineBuilderTest.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LinkBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ModuleBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/PageBreakBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/RectangleBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/RowBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilderTest.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/VContainerBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/Box.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/BuildEntity.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/ContainerBuilder.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EmptyBox.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EntityBuilderBase.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EntityCreator.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/Layout.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/ShapeBuilderBase.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/StackAxis.java delete mode 100644 render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/package-info.java create mode 100644 src/test/java/com/demcha/compose/engine/components/content/ImageDataCacheTest.java delete mode 100644 src/test/java/com/demcha/compose/engine/core/SystemRegistryTest.java delete mode 100644 src/test/java/com/demcha/compose/engine/pagination/ParentContainerUpdaterTest.java delete mode 100644 src/test/java/com/demcha/compose/engine/render/EntityRenderOrderTest.java diff --git a/qa/pom.xml b/qa/pom.xml index 08aefe8d7..67a0f56fe 100644 --- a/qa/pom.xml +++ b/qa/pom.xml @@ -77,23 +77,14 @@ ${project.version} test - + io.github.demchaav graph-compose-render-pdf ${project.version} test - - io.github.demchaav - graph-compose-render-pdf - ${project.version} - test-jar - tests - test - diff --git a/qa/src/test/java/com/demcha/compose/document/api/BarcodeRenderTest.java b/qa/src/test/java/com/demcha/compose/document/api/BarcodeRenderTest.java new file mode 100644 index 000000000..e1db30874 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/api/BarcodeRenderTest.java @@ -0,0 +1,38 @@ +package com.demcha.compose.document.api; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.layout.LayoutGraph; +import com.demcha.compose.document.style.DocumentInsets; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * End-to-end coverage for the canonical barcode DSL ({@code addBarcode}): a QR + * code and a linear CODE128 both lay out on a single page and render through the + * PDF backend without error. Guards barcode rendering on the canonical path. + */ +class BarcodeRenderTest { + + @Test + void barcodesLayOutOnOnePageAndRenderToPdf() throws Exception { + try (DocumentSession session = GraphCompose.document() + .pageSize(240, 200) + .margin(DocumentInsets.of(12)) + .create()) { + session.pageFlow() + .addBarcode(b -> b.qrCode().data("GC-2026-001").size(80, 80)) + .addBarcode(b -> b.code128().data("GC-2026-001").size(160, 40)) + .build(); + + LayoutGraph graph = session.layoutGraph(); + assertThat(graph.totalPages()).isEqualTo(1); + assertThat(graph.nodes()).isNotEmpty(); + + byte[] pdf = session.toPdfBytes(); + assertThat(new String(pdf, 0, 5, StandardCharsets.US_ASCII)).isEqualTo("%PDF-"); + } + } +} diff --git a/qa/src/test/java/com/demcha/compose/engine/components/ComponentBuilderTest.java b/qa/src/test/java/com/demcha/compose/engine/components/ComponentBuilderTest.java deleted file mode 100644 index 52c4a0281..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/components/ComponentBuilderTest.java +++ /dev/null @@ -1,189 +0,0 @@ -package com.demcha.compose.engine.components; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.testsupport.engine.assembly.CircleBuilder; -import com.demcha.compose.testsupport.engine.assembly.LineBuilder; -import com.demcha.compose.engine.components.content.shape.LinePath; -import com.demcha.compose.engine.components.renderable.Circle; -import com.demcha.compose.engine.components.renderable.Line; -import com.demcha.compose.engine.components.renderable.Module; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.compose.testsupport.EngineComposerHarness; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; - -import static org.assertj.core.api.Assertions.assertThat; - -class ComponentBuilderTest { - - @Test - void shouldCreateCircleFromComponentBuilderFactory() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - var entity = composer.componentBuilder() - .circle() - .size(40, 30) - .build(); - - assertThat(entity.hasAssignable(Circle.class)).isTrue(); - assertThat(entity.getComponent(ContentSize.class)) - .hasValueSatisfying(size -> { - assertThat(size.width()).isEqualTo(40); - assertThat(size.height()).isEqualTo(30); - }); - } - } - - @Test - void shouldCreateLineFromComponentBuilderFactory() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - var entity = composer.componentBuilder() - .line() - .size(120, 8) - .build(); - - assertThat(entity.hasAssignable(Line.class)).isTrue(); - assertThat(entity.getComponent(LinePath.class)).hasValue(LinePath.horizontal()); - assertThat(entity.getComponent(ContentSize.class)) - .hasValueSatisfying(size -> { - assertThat(size.width()).isEqualTo(120); - assertThat(size.height()).isEqualTo(8); - }); - } - } - - @Test - void shouldCreateModuleUsingCanvasOverload() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - var entity = composer.componentBuilder() - .moduleBuilder(Align.middle(4), composer.canvas()) - .build(); - - assertThat(entity.hasAssignable(Module.class)).isTrue(); - assertThat(entity.getComponent(ContentSize.class)).isPresent(); - } - } - - @Test - void shouldCreateModuleUsingContentSizeOverload() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - var entity = composer.componentBuilder() - .moduleBuilder(Align.middle(4), new ContentSize(240, 120)) - .build(); - - assertThat(entity.hasAssignable(Module.class)).isTrue(); - assertThat(entity.getComponent(ContentSize.class)).isPresent(); - } - } - - @Test - void rootModuleShouldUseCanvasWidthMinusOwnHorizontalMargin() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .margin(24, 24, 24, 24) - .create()) { - var cb = composer.componentBuilder(); - var module = cb.moduleBuilder(Align.middle(4)) - .anchor(Anchor.topLeft()) - .margin(new Margin(0, 12, 0, 8)) - .addChild(cb.rectangle().size(60, 20).build()) - .build(); - - composer.toBytes(); - - assertThat(module.getComponent(ContentSize.class)) - .hasValueSatisfying(size -> { - assertThat(size.width()).isEqualTo(composer.canvas().innerWidth() - 20); - assertThat(size.height()).isGreaterThan(0); - }); - } - } - - @Test - void nestedModuleShouldUseParentInnerWidthInsteadOfWidestChild() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - var cb = composer.componentBuilder(); - var module = cb.moduleBuilder(Align.left(4)) - .anchor(Anchor.topLeft()) - .margin(new Margin(0, 10, 0, 10)) - .addChild(cb.rectangle().size(220, 20).build()) - .build(); - - cb.vContainer(Align.left(0)) - .size(200, 0) - .anchor(Anchor.topLeft()) - .addChild(module) - .build(); - - composer.toBytes(); - - assertThat(module.getComponent(ContentSize.class)) - .hasValueSatisfying(size -> { - assertThat(size.width()).isEqualTo(180); - assertThat(size.height()).isGreaterThan(0); - }); - } - } - - @Test - void moduleShouldGrowVerticallyWithoutGrowingHorizontally() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - var cb = composer.componentBuilder(); - var module = cb.moduleBuilder(Align.middle(4), new ContentSize(240, 10)) - .anchor(Anchor.topLeft()) - .addChild(cb.rectangle().size(100, 30).build()) - .addChild(cb.rectangle().size(120, 40).build()) - .build(); - - composer.toBytes(); - - assertThat(module.getComponent(ContentSize.class)) - .hasValueSatisfying(size -> { - assertThat(size.width()).isEqualTo(240); - assertThat(size.height()).isEqualTo(74); - }); - } - } - - @Test - void oversizedChildShouldNotForceModuleToExpandHorizontally() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - var cb = composer.componentBuilder(); - var module = cb.moduleBuilder(Align.middle(4), new ContentSize(180, 0)) - .anchor(Anchor.topLeft()) - .addChild(cb.rectangle().size(320, 20).build()) - .build(); - - composer.toBytes(); - - assertThat(module.getComponent(ContentSize.class)) - .hasValueSatisfying(size -> assertThat(size.width()).isEqualTo(180)); - } - } - - @Test - void circleBuilderShouldStayLeafLikeAndNotExposeRectangleOnlyShapeMethods() { - var methodNames = Arrays.stream(CircleBuilder.class.getMethods()) - .map(method -> method.getName()) - .toList(); - - assertThat(methodNames) - .contains("fillColor", "stroke", "size", "padding", "margin", "anchor") - .doesNotContain("cornerRadius", "rectangle"); - } - - @Test - void lineBuilderShouldStayLeafLikeAndExposeLineSpecificPathMethods() { - var methodNames = Arrays.stream(LineBuilder.class.getMethods()) - .map(method -> method.getName()) - .toList(); - - assertThat(methodNames) - .contains("stroke", "path", "horizontal", "vertical", "diagonalAscending", "diagonalDescending", - "size", "padding", "margin", "anchor") - .doesNotContain("fillColor", "cornerRadius", "rectangle"); - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/components/content/ImageDataCacheTest.java b/qa/src/test/java/com/demcha/compose/engine/components/content/ImageDataCacheTest.java deleted file mode 100644 index 65f37018f..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/components/content/ImageDataCacheTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.demcha.compose.engine.components.content; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.testsupport.engine.assembly.ImageBuilder; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.testsupport.EngineComposerHarness; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -class ImageDataCacheTest { - - @TempDir - Path tempDir; - - @BeforeEach - void setUp() { - ImageSourceCache.clearForTests(); - } - - @AfterEach - void tearDown() { - ImageSourceCache.clearForTests(); - } - - @Test - void shouldReuseCachedBytesForRepeatedPathLoads() throws Exception { - Path imagePath = tempDir.resolve("cached-source.png"); - Files.write(imagePath, createPngBytes(120, 60, new Color(24, 92, 160))); - - ImageData firstLoad = ImageData.create(imagePath); - - Files.write(imagePath, createPngBytes(48, 24, new Color(182, 44, 64))); - - ImageData secondLoad = ImageData.create(imagePath); - - assertThat(ImageSourceCache.sourceCacheSize()).isEqualTo(1); - assertThat(ImageSourceCache.metadataCacheSize()).isEqualTo(1); - assertThat(firstLoad.getFingerprint()).isEqualTo(secondLoad.getFingerprint()); - assertThat(secondLoad.getMetadata().width()).isEqualTo(120); - assertThat(secondLoad.getMetadata().height()).isEqualTo(60); - } - - @Test - void shouldReuseMetadataCacheAndAvoidBuilderRedecode() throws Exception { - byte[] bytes = createPngBytes(200, 100, new Color(42, 52, 110)); - - ImageData first = ImageData.create(bytes); - ImageData second = ImageData.create(bytes); - - assertThat(first.getFingerprint()).isEqualTo(second.getFingerprint()); - assertThat(ImageSourceCache.metadataCacheSize()).isEqualTo(1); - assertThat(ImageSourceCache.metadataDecodeCount()).isEqualTo(1); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - ImageBuilder builder = composer.componentBuilder() - .image() - .image(first) - .fitToBounds(100, 100); - - ContentSize contentSize = builder.build().getComponent(ContentSize.class).orElseThrow(); - - assertThat(contentSize.width()).isEqualTo(100); - assertThat(contentSize.height()).isEqualTo(50); - } - assertThat(ImageSourceCache.metadataDecodeCount()).isEqualTo(1); - } - - private byte[] createPngBytes(int width, int height, Color color) { - try { - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - Graphics2D graphics = image.createGraphics(); - try { - graphics.setColor(color); - graphics.fillRect(0, 0, width, height); - graphics.setColor(Color.WHITE); - graphics.drawRect(0, 0, width - 1, height - 1); - } finally { - graphics.dispose(); - } - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ImageIO.write(image, "png", outputStream); - return outputStream.toByteArray(); - } catch (Exception e) { - throw new IllegalStateException("Failed to generate PNG for cache test", e); - } - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessContractTest.java b/qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessContractTest.java deleted file mode 100644 index 68c4aa7a8..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessContractTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.demcha.compose.engine.core; - -import com.demcha.compose.testsupport.EngineComposerHarness; - -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.content.text.TextStyle; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -class EngineComposerHarnessContractTest { - - @TempDir - Path tempDir; - - @Test - void shouldExposeCanvasAndExportBytesThroughCommonComposerContract() throws Exception { - byte[] pdfBytes; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - assertThat(composer.canvas().innerWidth()).isPositive(); - - composer.componentBuilder() - .text() - .textWithAutoSize("Common composer contract") - .textStyle(TextStyle.DEFAULT_STYLE) - .anchor(Anchor.topLeft()) - .build(); - - pdfBytes = composer.toBytes(); - } - - assertThat(pdfBytes).isNotEmpty(); - assertThat(new String(pdfBytes, 0, 4, StandardCharsets.US_ASCII)).isEqualTo("%PDF"); - } - - @Test - void shouldWritePdfFileWhenBuildIsCalledThroughCommonComposerContract() throws Exception { - Path outputFile = tempDir.resolve("common-composer-build.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile).create()) { - composer.componentBuilder() - .text() - .textWithAutoSize("Build to file") - .textStyle(TextStyle.DEFAULT_STYLE) - .anchor(Anchor.topLeft()) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists().isRegularFile().isNotEmptyFile(); - try (PDDocument saved = Loader.loadPDF(outputFile.toFile())) { - assertThat(saved.getNumberOfPages()).isGreaterThan(0); - } - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessLayoutSnapshotTest.java b/qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessLayoutSnapshotTest.java deleted file mode 100644 index 5056fd287..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessLayoutSnapshotTest.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.demcha.compose.engine.core; - -import com.demcha.compose.testsupport.EngineComposerHarness; - -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.content.text.TextIndentStrategy; -import com.demcha.compose.engine.components.content.text.BlockTextData; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.core.EntityName; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.layout.LayoutSystem; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Field; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -class EngineComposerHarnessLayoutSnapshotTest { - - @Test - void shouldNotShiftBlockTextWhenBuildFollowsLayoutSnapshot() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(PDRectangle.A4) - .margin(18, 18, 18, 18) - .markdown(true) - .create()) { - composeBlockTextProbe(composer); - - composer.layoutSnapshot(); - double lineXAfterSnapshot = firstLineX(composer, "ProbeBlock"); - double lineWidthAfterSnapshot = firstLineWidth(composer, "ProbeBlock"); - - composer.build(); - double lineXAfterBuild = firstLineX(composer, "ProbeBlock"); - double lineWidthAfterBuild = firstLineWidth(composer, "ProbeBlock"); - - assertThat(lineXAfterBuild).isEqualTo(lineXAfterSnapshot); - assertThat(lineWidthAfterBuild).isEqualTo(lineWidthAfterSnapshot); - } - } - - @Test - void shouldResetLayersAndDepthStateAcrossRepeatedLayoutPasses() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(PDRectangle.A4) - .margin(18, 18, 18, 18) - .markdown(true) - .create()) { - composeLayerProbe(composer); - - EntityManager entityManager = entityManager(composer); - LayoutSystem layoutSystem = layoutSystem(composer); - - layoutSystem.process(entityManager); - List firstLayerOrder = flattenedLayerOrder(entityManager); - LinkedHashMap firstDepths = new LinkedHashMap<>(entityManager.getDepthById()); - - layoutSystem.process(entityManager); - List secondLayerOrder = flattenedLayerOrder(entityManager); - - assertThat(secondLayerOrder).containsExactlyElementsOf(firstLayerOrder); - assertThat(secondLayerOrder).doesNotHaveDuplicates(); - assertThat(secondLayerOrder).hasSize(entityManager.getEntities().size()); - assertThat(entityManager.getDepthById()).containsExactlyEntriesOf(firstDepths); - } - } - - private void composeBlockTextProbe(EngineComposerHarness composer) { - ComponentBuilder cb = composer.componentBuilder(); - - cb.vContainer(Align.left(8)) - .entityName("ProbeRoot") - .anchor(Anchor.topLeft()) - .addChild(cb.blockText(Align.left(2), TextStyle.DEFAULT_STYLE) - .entityName("ProbeBlock") - .size(composer.canvas().innerWidth() - 40, 2) - .padding(Padding.of(12)) - .bulletOffset("• ") - .strategy(TextIndentStrategy.ALL_LINES) - .text( - java.util.List.of("Focused on platform engineering, document generation, backend integration, and resilient delivery."), - TextStyle.DEFAULT_STYLE, - null, - null) - .anchor(Anchor.topLeft()) - .build()) - .build(); - } - - private void composeLayerProbe(EngineComposerHarness composer) { - ComponentBuilder cb = composer.componentBuilder(); - - Entity left = cb.rectangle() - .entityName("LeftProbe") - .size(48, 24) - .anchor(Anchor.topLeft()) - .build(); - Entity right = cb.rectangle() - .entityName("RightProbe") - .size(48, 24) - .anchor(Anchor.topLeft()) - .build(); - - Entity row = cb.hContainer(Align.left(4)) - .entityName("ProbeRow") - .anchor(Anchor.topLeft()) - .addChild(left) - .addChild(right) - .build(); - - cb.vContainer(Align.left(6)) - .entityName("ProbeRoot") - .anchor(Anchor.topLeft()) - .addChild(row) - .addChild(cb.blockText(Align.left(2), TextStyle.DEFAULT_STYLE) - .entityName("ProbeFooter") - .size(composer.canvas().innerWidth() - 40, 2) - .padding(Padding.of(8)) - .text( - java.util.List.of("Repeated layout passes should not duplicate layer membership or depth state."), - TextStyle.DEFAULT_STYLE, - null, - null) - .anchor(Anchor.topLeft()) - .build()) - .build(); - } - - private double firstLineX(EngineComposerHarness composer, String entityName) throws Exception { - return firstLine(composer, entityName).x(); - } - - private double firstLineWidth(EngineComposerHarness composer, String entityName) throws Exception { - return firstLine(composer, entityName).lineWidth(); - } - - private com.demcha.compose.engine.components.content.text.LineTextData firstLine(EngineComposerHarness composer, String entityName) throws Exception { - EntityManager entityManager = entityManager(composer); - - Entity blockEntity = entityManager.getEntities().values().stream() - .filter(entity -> entity.getComponent(EntityName.class) - .map(EntityName::value) - .filter(entityName::equals) - .isPresent()) - .findFirst() - .orElseThrow(() -> new AssertionError("Missing entity " + entityName)); - - BlockTextData textData = blockEntity.getComponent(BlockTextData.class) - .orElseThrow(() -> new AssertionError("Missing BlockTextData for " + entityName)); - - return textData.lines().get(0); - } - - private EntityManager entityManager(EngineComposerHarness composer) throws Exception { - Field entityManagerField = EngineComposerHarness.class.getDeclaredField("entityManager"); - entityManagerField.setAccessible(true); - return (EntityManager) entityManagerField.get(composer); - } - - private LayoutSystem layoutSystem(EngineComposerHarness composer) throws Exception { - Field layoutSystemField = EngineComposerHarness.class.getDeclaredField("layoutSystem"); - layoutSystemField.setAccessible(true); - return (LayoutSystem) layoutSystemField.get(composer); - } - - private List flattenedLayerOrder(EntityManager entityManager) { - return entityManager.getLayers().values().stream() - .flatMap(List::stream) - .toList(); - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessMutationInvalidationTest.java b/qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessMutationInvalidationTest.java deleted file mode 100644 index 7f37150ba..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessMutationInvalidationTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.demcha.compose.engine.core; - -import com.demcha.compose.testsupport.EngineComposerHarness; - -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.Margin; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class EngineComposerHarnessMutationInvalidationTest { - - @Test - void shouldReLayoutWhenEntityIsAddedAfterSnapshotBeforeRender() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(new PDRectangle(200, 200)) - .margin(10, 10, 10, 10) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - cb.rectangle() - .entityName("First") - .size(80, 30) - .anchor(Anchor.topLeft()) - .build(); - - composer.layoutSnapshot(); - - Entity second = cb.rectangle() - .entityName("Second") - .size(80, 30) - .anchor(Anchor.topLeft()) - .build(); - - composer.toPDDocument(); - - assertThat(second.getComponent(Placement.class)).isPresent(); - } - } - - @Test - void shouldReLayoutWhenCanvasMarginChangesAfterSnapshot() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(new PDRectangle(200, 200)) - .margin(10, 10, 10, 10) - .create()) { - - Entity entity = composer.componentBuilder() - .rectangle() - .entityName("Probe") - .size(80, 30) - .anchor(Anchor.topLeft()) - .build(); - - composer.layoutSnapshot(); - double xBefore = entity.getComponent(Placement.class).orElseThrow().x(); - - composer.margin(Margin.of(20)); - composer.toPDDocument(); - - double xAfter = entity.getComponent(Placement.class).orElseThrow().x(); - assertThat(xAfter).isGreaterThan(xBefore); - } - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/core/NewFeaturesIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/core/NewFeaturesIntegrationTest.java deleted file mode 100644 index f5389d209..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/core/NewFeaturesIntegrationTest.java +++ /dev/null @@ -1,476 +0,0 @@ -package com.demcha.compose.engine.core; - -import com.demcha.compose.testsupport.EngineComposerHarness; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.engine.components.content.barcode.BarcodeType; -import com.demcha.compose.engine.components.content.header_footer.HeaderFooterConfig; -import com.demcha.compose.engine.components.content.header_footer.HeaderFooterZone; -import com.demcha.compose.engine.components.content.metadata.DocumentMetadata; -import com.demcha.compose.engine.components.content.protection.PdfProtectionConfig; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.content.watermark.WatermarkConfig; -import com.demcha.compose.engine.components.content.watermark.WatermarkLayer; -import com.demcha.compose.engine.components.content.watermark.WatermarkPosition; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.ComponentColor; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDDocumentInformation; -import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.awt.Color; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Visual integration tests for all new document-level features: - * watermark, header/footer, bookmarks, metadata, protection, - * page breaks, dividers, and barcodes working together. - */ -class NewFeaturesIntegrationTest { - - @TempDir - Path tempDir; - - // ===== Watermark ===== - - @Test - void shouldRenderTextWatermarkOnEveryPage() throws Exception { - Path outputFile = tempDir.resolve("watermark-text.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .margin(40, 40, 40, 40) - .create()) { - - composer.watermark("DRAFT"); - - var cb = composer.componentBuilder(); - cb.text() - .textWithAutoSize("Page 1 — Watermark test document") - .textStyle(TextStyle.DEFAULT_STYLE) - .anchor(Anchor.topLeft()) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists().isNotEmptyFile(); - try (PDDocument saved = Loader.loadPDF(outputFile.toFile())) { - assertThat(saved.getNumberOfPages()).isGreaterThan(0); - } - } - - @Test - void shouldRenderCustomWatermarkWithFullConfig() throws Exception { - Path outputFile = tempDir.resolve("watermark-custom.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .margin(40, 40, 40, 40) - .create()) { - - composer.watermark(WatermarkConfig.builder() - .text("CONFIDENTIAL") - .fontSize(60) - .rotation(30) - .opacity(0.10f) - .color(Color.RED) - .layer(WatermarkLayer.BEHIND_CONTENT) - .position(WatermarkPosition.CENTER) - .build()); - - var cb = composer.componentBuilder(); - cb.text() - .textWithAutoSize("Custom watermark test — text with red color behind content") - .textStyle(TextStyle.DEFAULT_STYLE) - .anchor(Anchor.topLeft()) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists().isNotEmptyFile(); - } - - // ===== Header / Footer ===== - - @Test - void shouldRenderHeaderAndFooterWithPageNumbers() throws Exception { - Path outputFile = tempDir.resolve("header-footer.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .margin(40, 40, 60, 40) - .create()) { - - composer.header("GraphCompose Inc.", "Invoice #12345", "{date}"); - composer.footer(null, "Page {page} of {pages}", null); - - var cb = composer.componentBuilder(); - cb.text() - .textWithAutoSize("Main document content between header and footer") - .textStyle(TextStyle.DEFAULT_STYLE) - .anchor(Anchor.topLeft()) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists().isNotEmptyFile(); - try (PDDocument saved = Loader.loadPDF(outputFile.toFile())) { - assertThat(saved.getNumberOfPages()).isGreaterThan(0); - } - } - - @Test - void shouldRenderFooterWithSeparatorLine() throws Exception { - Path outputFile = tempDir.resolve("footer-separator.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .margin(40, 40, 50, 40) - .create()) { - - composer.footer(HeaderFooterConfig.builder() - .leftText("Ref: DOC-2026") - .centerText("— {page} —") - .rightText("{date}") - .fontSize(8) - .textColor(Color.GRAY) - .showSeparator(true) - .separatorColor(new Color(200, 200, 200)) - .separatorThickness(0.5f) - .build()); - - var cb = composer.componentBuilder(); - cb.text() - .textWithAutoSize("Document with custom footer separator") - .textStyle(TextStyle.DEFAULT_STYLE) - .anchor(Anchor.topLeft()) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists().isNotEmptyFile(); - } - - // ===== Bookmarks ===== - - @Test - void shouldCreateBookmarkOutlineInPdf() throws Exception { - byte[] pdfBytes; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .margin(40, 40, 40, 40) - .create()) { - var cb = composer.componentBuilder(); - - cb.moduleBuilder(Align.middle(8), composer.canvas()) - .bookmark("Chapter 1 — Introduction") - .anchor(Anchor.topLeft()) - .addChild( - cb.text() - .textWithAutoSize("Introduction content") - .textStyle(TextStyle.DEFAULT_STYLE) - .build() - ) - .build(); - - cb.moduleBuilder(Align.middle(8), composer.canvas()) - .bookmark("Chapter 2 — Details") - .anchor(Anchor.topLeft()) - .addChild( - cb.text() - .textWithAutoSize("Details content") - .textStyle(TextStyle.DEFAULT_STYLE) - .build() - ) - .build(); - - pdfBytes = composer.toBytes(); - } - - assertThat(pdfBytes).isNotEmpty(); - assertThat(new String(pdfBytes, 0, 4, StandardCharsets.US_ASCII)).isEqualTo("%PDF"); - - try (PDDocument doc = Loader.loadPDF(pdfBytes)) { - PDDocumentOutline outline = doc.getDocumentCatalog().getDocumentOutline(); - assertThat(outline).isNotNull(); - // Verify that outline has children (bookmarks were created) - assertThat(outline.getFirstChild()).isNotNull(); - } - } - - @Test - void shouldCreateNestedBookmarks() throws Exception { - byte[] pdfBytes; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .margin(40, 40, 40, 40) - .create()) { - var cb = composer.componentBuilder(); - - cb.moduleBuilder(Align.middle(4), composer.canvas()) - .bookmark("Part 1") - .anchor(Anchor.topLeft()) - .addChild( - cb.moduleBuilder(Align.middle(4)) - .bookmark("Section 1.1", 1) - .addChild( - cb.text() - .textWithAutoSize("Nested section content") - .textStyle(TextStyle.DEFAULT_STYLE) - .build() - ) - .build() - ) - .build(); - - pdfBytes = composer.toBytes(); - } - - try (PDDocument doc = Loader.loadPDF(pdfBytes)) { - PDDocumentOutline outline = doc.getDocumentCatalog().getDocumentOutline(); - assertThat(outline).isNotNull(); - // Verify at least one bookmark was created with a nested child - var firstChild = outline.getFirstChild(); - assertThat(firstChild).isNotNull(); - } - } - - // ===== Document Metadata ===== - - @Test - void shouldSetDocumentMetadata() throws Exception { - byte[] pdfBytes; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - composer.metadata(DocumentMetadata.builder() - .title("Test Invoice") - .author("GraphCompose Test") - .subject("Financial Document") - .keywords("test, invoice, pdf") - .build()); - - var cb = composer.componentBuilder(); - cb.text() - .textWithAutoSize("Metadata test") - .textStyle(TextStyle.DEFAULT_STYLE) - .anchor(Anchor.topLeft()) - .build(); - - pdfBytes = composer.toBytes(); - } - - try (PDDocument doc = Loader.loadPDF(pdfBytes)) { - PDDocumentInformation info = doc.getDocumentInformation(); - assertThat(info.getTitle()).isEqualTo("Test Invoice"); - assertThat(info.getAuthor()).isEqualTo("GraphCompose Test"); - assertThat(info.getSubject()).isEqualTo("Financial Document"); - assertThat(info.getKeywords()).isEqualTo("test, invoice, pdf"); - } - } - - // ===== PDF Protection ===== - - @Test - void shouldEncryptPdfWithProtection() throws Exception { - Path outputFile = tempDir.resolve("protected.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .margin(40, 40, 40, 40) - .create()) { - - composer.protect(PdfProtectionConfig.builder() - .ownerPassword("admin123") - .userPassword("") - .canPrint(true) - .canCopyContent(false) - .canModify(false) - .keyLength(128) - .build()); - - var cb = composer.componentBuilder(); - cb.text() - .textWithAutoSize("This document is protected from copying and editing") - .textStyle(TextStyle.DEFAULT_STYLE) - .anchor(Anchor.topLeft()) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists().isNotEmptyFile(); - // Encrypted PDF should still be loadable - try (PDDocument saved = Loader.loadPDF(outputFile.toFile(), "")) { - assertThat(saved.getNumberOfPages()).isGreaterThan(0); - } - } - - // ===== Page Break ===== - - @Test - void pageBreakShouldCreateEntitySuccessfully() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .margin(40, 40, 40, 40) - .create()) { - var cb = composer.componentBuilder(); - - var entity = cb.pageBreak().build(); - - assertThat(entity.hasAssignable( - com.demcha.compose.engine.components.renderable.PageBreakComponent.class)) - .isTrue(); - assertThat(entity.getComponent( - com.demcha.compose.engine.components.geometry.ContentSize.class)) - .hasValueSatisfying(size -> { - assertThat(size.width()).isEqualTo(0); - assertThat(size.height()).isEqualTo(1); - }); - } - } - - // ===== Divider ===== - - @Test - void dividerShouldCreateLineEntity() throws Exception { - Path outputFile = tempDir.resolve("divider.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .margin(40, 40, 40, 40) - .create()) { - var cb = composer.componentBuilder(); - - var module = cb.moduleBuilder(Align.middle(4), composer.canvas()) - .anchor(Anchor.topLeft()) - .addChild( - cb.text() - .textWithAutoSize("Above divider") - .textStyle(TextStyle.DEFAULT_STYLE) - .build() - ) - .addChild( - cb.divider() - .width(composer.canvas().innerWidth()) - .thickness(1.5) - .color(ComponentColor.GRAY) - .verticalSpacing(10) - .build() - ) - .addChild( - cb.text() - .textWithAutoSize("Below divider") - .textStyle(TextStyle.DEFAULT_STYLE) - .build() - ) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists().isNotEmptyFile(); - } - - // ===== Full Feature Showcase ===== - - @Test - void shouldRenderFullShowcaseWithAllNewFeatures() throws Exception { - Path outputFile = tempDir.resolve("full-showcase.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .margin(40, 40, 60, 40) - .create()) { - - // Document metadata - composer.metadata(DocumentMetadata.builder() - .title("GraphCompose Feature Showcase") - .author("GraphCompose Test Suite") - .subject("Integration Test") - .keywords("barcode, watermark, bookmark, header, footer") - .build()); - - // Watermark - composer.watermark(WatermarkConfig.builder() - .text("SAMPLE") - .fontSize(72) - .rotation(45) - .opacity(0.08f) - .color(Color.LIGHT_GRAY) - .build()); - - // Header and footer - composer.header("GraphCompose", "Feature Showcase", "{date}"); - composer.footer(null, "Page {page} of {pages}", null); - - var cb = composer.componentBuilder(); - - // Section 1: Barcode showcase (bookmarked) - cb.moduleBuilder(Align.middle(8), composer.canvas()) - .bookmark("1. Barcodes") - .anchor(Anchor.topLeft()) - .addChild( - cb.text() - .textWithAutoSize("Barcode Examples") - .textStyle(TextStyle.DEFAULT_STYLE) - .build() - ) - .addChild( - cb.divider() - .width(composer.canvas().innerWidth()) - .thickness(1) - .color(ComponentColor.LIGHT_GRAY) - .build() - ) - .addChild( - cb.barcode() - .data("https://github.com/DemchaAV/GraphCompose") - .qrCode() - .size(120, 120) - .build() - ) - .addChild( - cb.barcode() - .data("SHIP-998877665544") - .code128() - .size(220, 60) - .build() - ) - .build(); - - // Section 2: Text section (bookmarked) - cb.moduleBuilder(Align.middle(8), composer.canvas()) - .bookmark("2. Document Features") - .anchor(Anchor.topLeft()) - .addChild( - cb.text() - .textWithAutoSize("This section demonstrates watermarks, headers, footers, and bookmarks") - .textStyle(TextStyle.DEFAULT_STYLE) - .build() - ) - .build(); - - composer.build(); - } - - // Validate output - assertThat(outputFile).exists().isRegularFile().isNotEmptyFile(); - - try (PDDocument saved = Loader.loadPDF(outputFile.toFile())) { - assertThat(saved.getNumberOfPages()).isGreaterThan(0); - - // Verify metadata - PDDocumentInformation info = saved.getDocumentInformation(); - assertThat(info.getTitle()).isEqualTo("GraphCompose Feature Showcase"); - assertThat(info.getAuthor()).isEqualTo("GraphCompose Test Suite"); - - // Verify bookmarks exist - PDDocumentOutline outline = saved.getDocumentCatalog().getDocumentOutline(); - assertThat(outline).isNotNull(); - assertThat(outline.getFirstChild()).isNotNull(); - } - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/BlockTextIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/BlockTextIntegrationTest.java deleted file mode 100644 index ce3a7e9fe..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/BlockTextIntegrationTest.java +++ /dev/null @@ -1,227 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory; -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.content.text.TextIndentStrategy; -import com.demcha.compose.engine.components.content.text.BlockTextData; -import com.demcha.compose.engine.components.content.text.LineTextData; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.compose.engine.render.pdf.PdfFont; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.within; - -/** - * Integration test for BlockTextBuilder with markdown and bullet offsets. - */ -class BlockTextIntegrationTest { - - @Test - void shouldRenderBlockTextWithBulletOffset() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("block_text_bullet_test", "guides", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(15, 10, 15, 15) - .markdown(true) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - - cb.blockText(Align.left(5), TextStyle.DEFAULT_STYLE) - .strategy(TextIndentStrategy.ALL_LINES) - .size(400, 2) - .anchor(Anchor.center()) - .bulletOffset("• ") - .padding(Padding.of(5)) - .margin(Margin.of(5)) - .text( - List.of("**CVRewriter (AI-Powered API Service)** – *Portfolio Project* " + - "Developed a full-stack application centred around a **Spring Boot REST API**."), - TextStyle.DEFAULT_STYLE, - Padding.of(5), - Margin.of(5)) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - } - - @Test - void shouldRenderBlockTextWithWhitespaceIndent() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("block_text_whitespace_test", "guides", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(15, 10, 15, 15) - .markdown(true) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - cb.blockText(Align.left(5), TextStyle.DEFAULT_STYLE) - .strategy(TextIndentStrategy.FIRST_LINE) - .size(400, 2) - .anchor(Anchor.center()) - .bulletOffset(" ") // 4 spaces indent - .padding(Padding.of(5)) - .margin(Margin.of(5)) - .text( - List.of("This is a paragraph with first-line indent. " + - "The subsequent lines should not have the same indent."), - TextStyle.DEFAULT_STYLE, - Padding.of(5), - Margin.of(5)) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - } - - @Test - void shouldRenderBlockTextWithMarkdownFormatting() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("block_text_markdown_test", "clean", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .markdown(true) - .guideLines(false) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - - cb.blockText(Align.left(3), TextStyle.DEFAULT_STYLE) - .size(500, 2) - .anchor(Anchor.topLeft()) - .padding(Padding.of(10)) - .margin(Margin.of(10)) - .text( - List.of("**Bold text** and *italic text* combined with normal text.\n" + - "This tests markdown parsing in **BlockTextBuilder**."), - TextStyle.DEFAULT_STYLE, - Padding.zero(), - Margin.zero()) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - } - - @Test - void shouldIncreaseVerticalGapAroundMarkdownHeading() throws Exception { - TextStyle style = TextStyle.DEFAULT_STYLE; - Entity entity; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .markdown(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - entity = cb.blockText(Align.left(2), style) - .size(360, 2) - .anchor(Anchor.topLeft()) - .padding(Padding.zero()) - .margin(Margin.zero()) - .text(List.of("First line\n# Heading\nThird line"), style, Padding.zero(), Margin.zero()) - .build(); - - composer.build(); - } - - BlockTextData blockTextData = entity.getComponent(BlockTextData.class).orElseThrow(); - assertThat(blockTextData.lines()).hasSize(3); - assertThat(blockTextData.lines().get(1).hasCachedLineMetrics()).isTrue(); - assertThat(blockTextData.lines().get(1).lineMetrics().lineHeight()) - .isGreaterThan(blockTextData.lines().get(0).lineMetrics().lineHeight()); - - double gapBeforeHeading = blockTextData.lines().get(0).y() - blockTextData.lines().get(1).y(); - double gapAfterHeading = blockTextData.lines().get(1).y() - blockTextData.lines().get(2).y(); - - double baseStep; - try (PDDocument fontDocument = new PDDocument()) { - PdfFont font = (PdfFont) PdfFontLibraryFactory.library(fontDocument) - .getFont(style.fontName(), PdfFont.class) - .orElseThrow(); - baseStep = font.getLineHeight(style) + 2.0; - } - - assertThat(gapBeforeHeading).isGreaterThan(baseStep); - assertThat(gapAfterHeading).isGreaterThan(baseStep); - } - - @Test - void shouldRespectOwnPaddingWhenPositioningBlockTextLines() throws Exception { - TextStyle style = TextStyle.DEFAULT_STYLE; - Padding padding = Padding.of(8); - Entity entity; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .markdown(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - entity = cb.blockText(Align.left(2), style) - .size(360, 2) - .anchor(Anchor.topLeft()) - .padding(padding) - .margin(Margin.zero()) - .text(List.of("First line\nSecond line\nThird line"), style, Padding.zero(), Margin.zero()) - .build(); - - composer.build(); - } - - BlockTextData blockTextData = entity.getComponent(BlockTextData.class).orElseThrow(); - Placement placement = entity.getComponent(Placement.class).orElseThrow(); - - double baselineOffsetFromBottom; - try (PDDocument fontDocument = new PDDocument()) { - PdfFont font = (PdfFont) PdfFontLibraryFactory.library(fontDocument) - .getFont(style.fontName(), PdfFont.class) - .orElseThrow(); - baselineOffsetFromBottom = font.verticalMetrics(style).baselineOffsetFromBottom(); - } - - LineTextData firstLine = blockTextData.lines().get(0); - LineTextData lastLine = blockTextData.lines().get(blockTextData.lines().size() - 1); - - assertThat(firstLine.hasCachedLineWidth()).isTrue(); - assertThat(firstLine.hasCachedLineMetrics()).isTrue(); - assertThat(firstLine.hasCachedBaselineOffset()).isTrue(); - assertThat(lastLine.baselineOffset()).isCloseTo(baselineOffsetFromBottom, within(0.001)); - assertThat(firstLine.x()).isCloseTo(placement.x() + padding.left(), within(0.5)); - assertThat(lastLine.y() - lastLine.baselineOffset()) - .isCloseTo(placement.y() + padding.bottom(), within(0.5)); - } -} - diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/CircleIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/CircleIntegrationTest.java deleted file mode 100644 index bad2a0f01..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/CircleIntegrationTest.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.awt.Color; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.nio.file.Path; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -class CircleIntegrationTest { - - @Test - void shouldRenderSingleCircleWithoutGuides() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("circle_single_clean", "clean", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(false) - .create()) { - - composer.componentBuilder() - .circle() - .size(180, 180) - .fillColor(ComponentColor.ROYAL_BLUE) - .padding(Padding.of(8)) - .margin(Margin.of(15)) - .anchor(Anchor.topCenter()) - .build(); - - composer.build(); - } - - assertPdfExists(outputFile, 1); - } - - @Test - void shouldRenderSingleCircleWithGuides() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("circle_single_guides", "guides", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(true) - .create()) { - - composer.componentBuilder() - .circle() - .size(150, 150) - .fillColor(ComponentColor.LIGHT_GRAY) - .stroke(new Stroke(ComponentColor.ROYAL_BLUE, 2)) - .padding(Padding.of(10)) - .margin(Margin.of(15)) - .anchor(Anchor.topCenter()) - .build(); - - composer.build(); - } - - assertPdfExists(outputFile, 1); - } - - @Test - void shouldPaginateCircleColumnAcrossPages() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("circle_pagination_test", "guides", "integration"); - List circles; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) -// .margin(20, 20, 20, 20) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - circles = createColoredCircles(cb); - - var containerBuilder = cb.vContainer(Align.top(12)) - .entityName("CirclePaginationColumn") - .anchor(Anchor.topCenter()) - .margin(Margin.of(20)); - - for (Entity circle : circles) { - containerBuilder.addChild(circle); - } - containerBuilder.build(); - - composer.build(); - } - - assertPdfExists(outputFile, 2); - - int pageCount; - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - pageCount = document.getNumberOfPages(); - assertThat(pageCount).isGreaterThan(1); - } - - Set occupiedPages = new HashSet<>(); - for (Entity circle : circles) { - Placement placement = circle.getComponent(Placement.class).orElseThrow(); - assertThat(placement.startPage()).isEqualTo(placement.endPage()); - assertThat(placement.startPage()).isGreaterThanOrEqualTo(0); - assertThat(placement.endPage()).isLessThan(pageCount); - occupiedPages.add(placement.startPage()); - } - - assertThat(occupiedPages) - .as("Circles should be distributed across more than one page") - .hasSizeGreaterThan(1); - } - - private void assertPdfExists(Path outputFile, int minPages) throws Exception { - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - assertThat(document.getNumberOfPages()).isGreaterThanOrEqualTo(minPages); - } - } - - private List createColoredCircles(ComponentBuilder cb) { - List circles = new ArrayList<>(); - List colors = List.of( - ComponentColor.ROYAL_BLUE, - ComponentColor.LIGHT_BLUE, - ComponentColor.ORANGE, - ComponentColor.DARK_GREEN, - ComponentColor.PURPLE); - - for (int i = 0; i < colors.size(); i++) { - Entity circle = cb.circle() - .size(150, 150) - .fillColor(colors.get(i)) - .stroke(new Stroke(ComponentColor.DARK_BLUE, 2)) - .padding(Padding.of(6)) - .margin(Margin.of(10)) - .anchor(Anchor.center()) - .entityName("Circle" + i) - .build(); - circles.add(circle); - } - return circles; - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/FeatureShowcaseRenderTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/FeatureShowcaseRenderTest.java deleted file mode 100644 index bf2e3a87e..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/FeatureShowcaseRenderTest.java +++ /dev/null @@ -1,376 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.content.header_footer.HeaderFooterConfig; -import com.demcha.compose.engine.components.content.metadata.DocumentMetadata; -import com.demcha.compose.engine.components.content.text.TextDecoration; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.content.watermark.WatermarkConfig; -import com.demcha.compose.engine.components.content.watermark.WatermarkLayer; -import com.demcha.compose.engine.components.content.watermark.WatermarkPosition; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.testing.VisualTestOutputs; -import com.demcha.compose.testing.layout.LayoutSnapshotAssertions; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDDocumentInformation; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; -import org.junit.jupiter.api.Test; - -import java.awt.Color; -import java.nio.file.Path; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Human-readable visual entry points for document-platform features that should - * be easy to inspect in generated PDFs. - * - *

These tests intentionally write artifacts under {@code target/visual-tests} - * so contributors can quickly inspect barcode output, page breaks, and - * document-level chrome while engine changes evolve.

- */ -class FeatureShowcaseRenderTest { - - private static final Color TITLE_COLOR = new Color(18, 40, 74); - private static final Color SECTION_COLOR = new Color(28, 83, 135); - private static final Color BODY_COLOR = new Color(58, 69, 84); - private static final TextStyle TITLE_STYLE = TextStyle.builder() - .size(22) - .decoration(TextDecoration.BOLD) - .color(TITLE_COLOR) - .build(); - private static final TextStyle SECTION_STYLE = TextStyle.builder() - .size(13) - .decoration(TextDecoration.BOLD) - .color(SECTION_COLOR) - .build(); - private static final TextStyle BODY_STYLE = TextStyle.builder() - .size(9.5) - .decoration(TextDecoration.DEFAULT) - .color(BODY_COLOR) - .build(); - private static final String REPOSITORY_URL = "https://github.com/DemchaAV/GraphCompose"; - - @Test - void shouldRenderBarcodeGalleryWithoutGuideLines() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("barcode_showcase_render", "clean", "integration", "features"); - - renderBarcodeGallery(outputFile, false); - - assertPdfLooksValid(outputFile, 1); - } - - @Test - void shouldRenderBarcodeGalleryWithGuideLines() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("barcode_showcase_render_guides", "guides", "integration", "features"); - - renderBarcodeGallery(outputFile, true); - - assertPdfLooksValid(outputFile, 1); - } - - @Test - void shouldRenderPaginatedDocumentFeaturesWithoutGuideLines() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("document_features_paginated", "clean", "integration", "features"); - - renderPaginatedDocumentFeatures(outputFile, false); - - assertPaginatedFeaturePdf(outputFile); - } - - @Test - void shouldRenderPaginatedDocumentFeaturesWithGuideLines() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("document_features_paginated_guides", "guides", "integration", "features"); - - renderPaginatedDocumentFeatures(outputFile, true); - - assertPaginatedFeaturePdf(outputFile); - } - - private void renderBarcodeGallery(Path outputFile, boolean guideLines) throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(24, 24, 24, 24) - .guideLines(guideLines) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - double contentWidth = composer.canvas().innerWidth(); - - cb.vContainer(Align.middle(12)) - .entityName("BarcodeShowcaseRoot") - .size(contentWidth, 0) - .anchor(Anchor.topLeft()) - .addChild(title(cb, "Barcode and QR Showcase", "BarcodeShowcaseTitle")) - .addChild(body(cb, - "This artifact is meant for manual inspection. It keeps the composition simple so contributors can quickly verify how QR, Code 128, and EAN-13 output look in the real PDF renderer.", - contentWidth, - "BarcodeShowcaseLead")) - .addChild(barcodeModule( - composer, - cb, - "QR Code", - cb.barcode() - .entityName("BarcodeQrSample") - .data(REPOSITORY_URL) - .qrCode() - .size(118, 118) - .foreground(new Color(22, 77, 122)) - .anchor(Anchor.topCenter()) - .build(), - "This QR now points directly to the GraphCompose repository so the showcase PDF doubles as a shareable project handoff artifact.", - "BarcodeQrModule")) - .addChild(barcodeModule( - composer, - cb, - "Code 128 Shipping Label", - cb.barcode() - .entityName("BarcodeCode128Sample") - .data("SHIP-998877665544-ALPHA") - .code128() - .size(320, 72) - .quietZone(4) - .anchor(Anchor.topCenter()) - .build(), - "Code 128 is the practical workhorse for logistics and warehouse labels where dense alphanumeric payloads matter.", - "BarcodeCode128Module")) - .addChild(barcodeModule( - composer, - cb, - "EAN-13 Retail Code", - cb.barcode() - .entityName("BarcodeEan13Sample") - .data("4006381333931") - .ean13() - .size(240, 96) - .background(new Color(252, 252, 252)) - .quietZone(2) - .anchor(Anchor.topCenter()) - .build(), - "EAN-13 gives a familiar retail-style output so library users can validate product-code scenarios without writing custom PDFBox code.", - "BarcodeEan13Module")) - .build(); - - if (!guideLines) { - LayoutSnapshotAssertions.assertMatches(composer.layoutSnapshot(), "integration/features/barcode_showcase_render"); - } - composer.build(); - } - } - - private void renderPaginatedDocumentFeatures(Path outputFile, boolean guideLines) throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(28, 28, 42, 28) - .markdown(true) - .guideLines(guideLines) - .create()) { - - composer.metadata(DocumentMetadata.builder() - .title("GraphCompose Document Features Showcase") - .author("GraphCompose integration tests") - .subject("Visual verification for pagination and document features") - .keywords("graphcompose, pagination, barcode, watermark, header, footer") - .build()); - composer.watermark(WatermarkConfig.builder() - .text("PREVIEW") - .fontSize(82) - .rotation(38) - .opacity(0.08f) - .color(new Color(54, 92, 131)) - .layer(WatermarkLayer.BEHIND_CONTENT) - .position(WatermarkPosition.CENTER) - .build()); - composer.header("GraphCompose", "Document Features Showcase", "{date}"); - composer.footer(HeaderFooterConfig.builder() - .leftText("integration/features") - .centerText("Page {page} of {pages}") - .rightText("visual smoke") - .showSeparator(true) - .build()); - - ComponentBuilder cb = composer.componentBuilder(); - double contentWidth = composer.canvas().innerWidth(); - - var root = cb.vContainer(Align.middle(12)) - .entityName("DocumentFeaturesRoot") - .size(contentWidth, 0) - .anchor(Anchor.topLeft()); - - root.addChild(title(cb, "Paginated Document Features", "DocumentFeaturesTitle")); - root.addChild(body(cb, - "This document combines visible PDF chrome with layout-sensitive content. The goal is to give contributors a single artifact where they can inspect header/footer placement, watermark layering, explicit page breaks, bookmarks, and natural multi-page flow.", - contentWidth, - "DocumentFeaturesLead")); - - root.addChild(cb.moduleBuilder(Align.middle(8), composer.canvas()) - .entityName("OverviewModule") - .bookmark("1. Overview") - .anchor(Anchor.topLeft()) - .addChild(sectionTitle(cb, "Overview", "OverviewHeading")) - .addChild(body(cb, - "The first section keeps a compact summary plus a QR code so the document shows that barcode entities can live inside an otherwise text-heavy report composition.", - contentWidth, - "OverviewBody")) - .addChild(cb.divider() - .entityName("OverviewDivider") - .width(contentWidth) - .thickness(1) - .color(ComponentColor.LIGHT_GRAY) - .verticalSpacing(6) - .build()) - .addChild(cb.barcode() - .entityName("DocumentFeaturesQr") - .data(REPOSITORY_URL) - .qrCode() - .size(104, 104) - .foreground(new Color(18, 40, 74)) - .anchor(Anchor.topCenter()) - .build()) - .build()); - - root.addChild(cb.pageBreak() - .entityName("ForcedPageBreakAfterOverview") - .build()); - - root.addChild(cb.moduleBuilder(Align.middle(8), composer.canvas()) - .entityName("ManualPageBreakModule") - .bookmark("2. Manual Page Break") - .anchor(Anchor.topLeft()) - .addChild(sectionTitle(cb, "Manual Page Break", "ManualPageBreakHeading")) - .addChild(body(cb, - "This section starts on a fresh page because the previous child in the root flow is an explicit page-break entity. That makes the artifact useful for validating both the visual result and the pagination contract.", - contentWidth, - "ManualPageBreakBody")) - .addChild(cb.divider() - .entityName("ManualPageBreakDivider") - .width(contentWidth) - .thickness(1.2) - .color(ComponentColor.GRAY) - .verticalSpacing(8) - .build()) - .addChild(body(cb, - "Below the divider we intentionally switch back to ordinary flowing content so the transition from forced page break into normal pagination stays visible in one PDF.", - contentWidth, - "ManualPageBreakFollowUp")) - .build()); - - var narrative = cb.moduleBuilder(Align.middle(8), composer.canvas()) - .entityName("NarrativePaginationModule") - .bookmark("3. Natural Pagination") - .anchor(Anchor.topLeft()) - .addChild(sectionTitle(cb, "Natural Pagination", "NarrativePaginationHeading")) - .addChild(body(cb, - "The paragraphs below are intentionally long so the layout engine has to continue onto later pages without losing spacing rhythm between sections.", - contentWidth, - "NarrativePaginationLead")); - - for (int i = 1; i <= 9; i++) { - narrative.addChild(body( - cb, - repeatedNarrativeParagraph(i), - contentWidth, - "NarrativeParagraph" + i)); - } - - root.addChild(narrative.build()); - root.build(); - - if (!guideLines) { - LayoutSnapshotAssertions.assertMatches(composer.layoutSnapshot(), "integration/features/document_features_paginated"); - } - composer.build(); - } - } - - private Entity barcodeModule(EngineComposerHarness composer, - ComponentBuilder cb, - String heading, - Entity barcode, - String description, - String entityName) { - double contentWidth = composer.canvas().innerWidth(); - return cb.moduleBuilder(Align.middle(8), composer.canvas()) - .entityName(entityName) - .anchor(Anchor.topLeft()) - .addChild(sectionTitle(cb, heading, entityName + "Heading")) - .addChild(cb.divider() - .entityName(entityName + "Divider") - .width(contentWidth) - .thickness(1) - .color(ComponentColor.LIGHT_GRAY) - .verticalSpacing(6) - .build()) - .addChild(barcode) - .addChild(body(cb, description, contentWidth, entityName + "Body")) - .build(); - } - - private Entity title(ComponentBuilder cb, String text, String entityName) { - return cb.text() - .entityName(entityName) - .textWithAutoSize(text) - .textStyle(TITLE_STYLE) - .anchor(Anchor.topLeft()) - .margin(Margin.bottom(4)) - .build(); - } - - private Entity sectionTitle(ComponentBuilder cb, String text, String entityName) { - return cb.text() - .entityName(entityName) - .textWithAutoSize(text) - .textStyle(SECTION_STYLE) - .anchor(Anchor.topLeft()) - .build(); - } - - private Entity body(ComponentBuilder cb, String text, double width, String entityName) { - return cb.blockText(Align.left(4), BODY_STYLE) - .entityName(entityName) - .size(width, 2) - .anchor(Anchor.topLeft()) - .padding(Padding.of(4)) - .margin(Margin.bottom(2)) - .text(List.of(text), BODY_STYLE, Padding.zero(), Margin.zero()) - .build(); - } - - private void assertPdfLooksValid(Path outputFile, int minimumPages) throws Exception { - assertThat(outputFile).exists().isRegularFile().isNotEmptyFile(); - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - assertThat(document.getNumberOfPages()).isGreaterThanOrEqualTo(minimumPages); - } - } - - private void assertPaginatedFeaturePdf(Path outputFile) throws Exception { - assertThat(outputFile).exists().isRegularFile().isNotEmptyFile(); - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - assertThat(document.getNumberOfPages()).isGreaterThanOrEqualTo(3); - - PDDocumentInformation info = document.getDocumentInformation(); - assertThat(info.getTitle()).isEqualTo("GraphCompose Document Features Showcase"); - - PDDocumentOutline outline = document.getDocumentCatalog().getDocumentOutline(); - assertThat(outline).isNotNull(); - assertThat(outline.getFirstChild()).isNotNull(); - } - } - - private String repeatedNarrativeParagraph(int index) { - String lead = "Paragraph %d documents a realistic report-style body where the same layout primitives need to stay readable even as content keeps flowing. ".formatted(index); - String detail = "GraphCompose should preserve vertical rhythm, carry the module across page boundaries, and keep the surrounding document chrome visually stable while the text continues. "; - String closing = "This makes the artifact useful both for debugging pagination and for quickly checking whether a renderer-side change altered the final PDF appearance."; - return lead + detail.repeat(5) + closing; - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/ImageIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/ImageIntegrationTest.java deleted file mode 100644 index 469067a4f..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/ImageIntegrationTest.java +++ /dev/null @@ -1,252 +0,0 @@ -package com.demcha.compose.engine.integration; -import com.demcha.compose.qa.RepoPaths; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -class ImageIntegrationTest { - - private static final Path COVER_ASSET = RepoPaths.resolve("assets", "GraphComposeCover.png"); - - @Test - void shouldRenderSingleImageWithGuides() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("image_single_guides", "guides", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(true) - .create()) { - - composer.componentBuilder() - .image() - .image(createPngBytes(320, 180)) - .fitToBounds(320, 220) - .padding(Padding.of(10)) - .margin(Margin.of(15)) - .anchor(Anchor.topCenter()) - .build(); - - composer.build(); - } - - assertPdfExists(outputFile, 1); - } - - @Test - void shouldRenderSingleImageWithoutGuides() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("image_single_clean", "clean", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(false) - .create()) { - - composer.componentBuilder() - .image() - .image(createPngBytes(320, 180)) - .fitToBounds(320, 220) - .padding(Padding.of(10)) - .margin(Margin.of(15)) - .anchor(Anchor.topCenter()) - .build(); - - composer.build(); - } - - assertPdfExists(outputFile, 1); - } - - @Test - void shouldRenderImageInsideVContainer() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("image_vcontainer_pagination", "guides", "integration"); - Placement imagePlacement; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - - Entity spacer = cb.rectangle() - .size(500, 700) - .fillColor(ComponentColor.LIGHT_GRAY) - .margin(Margin.of(5)) - .build(); - - Entity image = cb.image() - .image(createPngBytes(320, 180)) - .fitToBounds(320, 220) - .padding(Padding.of(10)) - .margin(Margin.of(10)) - .anchor(Anchor.center()) - .build(); - - cb.vContainer(Align.middle(10)) - .entityName("ImageStack") - .anchor(Anchor.topCenter()) - .margin(Margin.of(10)) - .addChild(spacer) - .addChild(image) - .build(); - - composer.build(); - imagePlacement = image.getComponent(Placement.class).orElseThrow(); - } - - assertPdfExists(outputFile, 2); - assertThat(imagePlacement.startPage()).isEqualTo(imagePlacement.endPage()); - assertThat(imagePlacement.startPage()).isGreaterThan(0); - } - - @Test - void shouldRenderFifteenCoverImagesInCenteredVContainer() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("image_vcontainer_cover_column", "guides", "integration"); - assertThat(COVER_ASSET).exists(); - - Entity[] images = new Entity[15]; - Placement[] placements = new Placement[15]; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - var container = cb.vContainer(Align.middle(10)) - .entityName("CoverImageColumn") - .anchor(Anchor.topCenter()) - .margin(Margin.of(10)); - - for (int i = 0; i < placements.length; i++) { - Entity image = cb.image() - .image(COVER_ASSET) - .scale(0.22) - .padding(Padding.of(6)) - .margin(Margin.of(8)) - .anchor(Anchor.center()) - .entityName("CoverImage" + i) - .build(); - container.addChild(image); - images[i] = image; - } - - container.build(); - composer.build(); - - for (int i = 0; i < placements.length; i++) { - placements[i] = images[i].getComponent(Placement.class).orElseThrow(); - } - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - assertThat(document.getNumberOfPages()).isGreaterThanOrEqualTo(3); - } - - for (Placement placement : placements) { - assertThat(placement.startPage()).isEqualTo(placement.endPage()); - } - } - - @Test - void shouldReuseOriginalAndCreateOnlyNeededVariantsInComposer() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - - cb.vContainer(Align.middle(10)) - .entityName("MixedImageSizes") - .anchor(Anchor.topCenter()) - .margin(Margin.of(10)) - .addChild(cb.image() - .image(createPngBytes(200, 100)) - .fitToBounds(150, 80) - .padding(Padding.of(6)) - .margin(Margin.of(8)) - .anchor(Anchor.center()) - .build()) - .addChild(cb.image() - .image(createPngBytes(200, 100)) - .fitToBounds(40, 20) - .padding(Padding.of(6)) - .margin(Margin.of(8)) - .anchor(Anchor.center()) - .build()) - .addChild(cb.image() - .image(createPngBytes(200, 100)) - .fitToBounds(50, 25) - .padding(Padding.of(6)) - .margin(Margin.of(8)) - .anchor(Anchor.center()) - .build()) - .build(); - - composer.toPDDocument(); - } - } - - private void assertPdfExists(Path outputFile, int minPages) throws Exception { - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - assertThat(document.getNumberOfPages()).isGreaterThanOrEqualTo(minPages); - } - } - - private byte[] createPngBytes(int width, int height) { - try { - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - Graphics2D graphics = image.createGraphics(); - try { - graphics.setColor(new Color(31, 45, 72)); - graphics.fillRect(0, 0, width, height); - graphics.setColor(new Color(255, 196, 61)); - graphics.fillRoundRect(width / 8, height / 6, width * 3 / 4, height * 2 / 3, 18, 18); - graphics.setColor(Color.WHITE); - graphics.drawRect(0, 0, width - 1, height - 1); - graphics.drawLine(0, 0, width - 1, height - 1); - } finally { - graphics.dispose(); - } - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ImageIO.write(image, "png", outputStream); - return outputStream.toByteArray(); - } catch (Exception e) { - throw new IllegalStateException("Failed to create test PNG", e); - } - } -} - diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/LargeContainerIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/LargeContainerIntegrationTest.java deleted file mode 100644 index a3be62a1c..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/LargeContainerIntegrationTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration test for large containers that span multiple pages. - */ -class LargeContainerIntegrationTest { - - @Test - void shouldRenderLargeContainerAcrossMultiplePages() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("large_container_test", "guides", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(10, 10, 10, 10) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - - // Container height 2100 is larger than A4 page (~842 pts) - cb.vContainer(Align.middle(2)) - .size(500, 2100) - .anchor(Anchor.topCenter()) - .margin(Margin.of(10)) - .padding(Padding.of(5)) - .entityName("LargeContainer") - .build(); - - composer.build(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - - try (PDDocument doc = Loader.loadPDF(outputFile.toFile())) { - // A4 height is ~842pt, container is 2100pt, so expect at least 3 pages - assertThat(doc.getNumberOfPages()).isGreaterThanOrEqualTo(3); - } - } - - @Test - void shouldRenderSmallContainerOnSinglePage() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("small_container_test", "guides", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - - cb.vContainer(Align.middle(5)) - .size(400, 200) - .anchor(Anchor.topCenter()) - .margin(Margin.of(10)) - .padding(Padding.of(5)) - .entityName("SmallContainer") - .build(); - - composer.build(); - } - - assertThat(outputFile).exists(); - - try (PDDocument doc = Loader.loadPDF(outputFile.toFile())) { - assertThat(doc.getNumberOfPages()).isGreaterThanOrEqualTo(1); - } - } -} - diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/LineIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/LineIntegrationTest.java deleted file mode 100644 index 65c27af3a..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/LineIntegrationTest.java +++ /dev/null @@ -1,296 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.content.shape.LinePath; -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.rendering.PDFRenderer; -import org.junit.jupiter.api.Test; - -import java.awt.Color; -import java.awt.image.BufferedImage; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -class LineIntegrationTest { - private static final float RENDER_DPI = 144.0f; - private static final double RENDER_SCALE = RENDER_DPI / 72.0; - - @Test - void shouldRenderLineVariantsWithoutGuides() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("line_variants", "clean", "integration"); - - Entity horizontal; - Entity vertical; - Entity diagonal; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .guideLines(false) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - - horizontal = cb.line() - .horizontal() - .size(220, 20) - .padding(Padding.of(6)) - .stroke(new Stroke(ComponentColor.ROYAL_BLUE, 4.0)) - .anchor(Anchor.topLeft()) - .build(); - - vertical = cb.line() - .vertical() - .size(20, 120) - .padding(Padding.of(6)) - .stroke(new Stroke(ComponentColor.ORANGE, 4.0)) - .anchor(Anchor.topLeft()) - .build(); - - diagonal = cb.line() - .diagonalDescending() - .size(180, 70) - .padding(Padding.of(8)) - .stroke(new Stroke(ComponentColor.DARK_GREEN, 3.0)) - .anchor(Anchor.topLeft()) - .build(); - - cb.vContainer(Align.left(18)) - .entityName("LineGallery") - .anchor(Anchor.topLeft()) - .margin(Margin.of(24)) - .addChild(horizontal) - .addChild(vertical) - .addChild(diagonal) - .build(); - - composer.build(); - } - - assertPdfExists(outputFile, 1); - - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - BufferedImage image = new PDFRenderer(document).renderImageWithDPI(0, RENDER_DPI); - - assertLineIsVisible(image, document, horizontal); - assertLineIsVisible(image, document, vertical); - assertLineIsVisible(image, document, diagonal); - } - } - - @Test - void shouldRenderSingleLineWithGuideLines() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("line_single_guides", "guides", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .guideLines(true) - .create()) { - - composer.componentBuilder() - .line() - .diagonalAscending() - .size(180, 80) - .padding(Padding.of(8)) - .margin(Margin.of(16)) - .stroke(new Stroke(ComponentColor.ROYAL_BLUE, 4.0)) - .anchor(Anchor.topCenter()) - .build(); - - composer.build(); - } - - assertPdfExists(outputFile, 1); - } - - @Test - void shouldRenderLineVariantsWithGuideLines() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("line_variants_guides", "guides", "integration"); - List lines = new ArrayList<>(); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - var container = cb.vContainer(Align.left(18)) - .entityName("LineGuideGallery") - .anchor(Anchor.topLeft()) - .margin(Margin.of(24)); - - lines.add(cb.line() - .horizontal() - .size(220, 20) - .padding(Padding.of(6)) - .stroke(new Stroke(ComponentColor.ROYAL_BLUE, 4.0)) - .anchor(Anchor.topLeft()) - .build()); - - lines.add(cb.line() - .vertical() - .size(20, 120) - .padding(Padding.of(6)) - .stroke(new Stroke(ComponentColor.ORANGE, 4.0)) - .anchor(Anchor.topLeft()) - .build()); - - lines.add(cb.line() - .diagonalDescending() - .size(180, 70) - .padding(Padding.of(8)) - .stroke(new Stroke(ComponentColor.DARK_GREEN, 3.0)) - .anchor(Anchor.topLeft()) - .build()); - - for (Entity line : lines) { - container.addChild(line); - } - - container.build(); - composer.build(); - } - - assertPdfExists(outputFile, 1); - - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - BufferedImage image = new PDFRenderer(document).renderImageWithDPI(0, RENDER_DPI); - - for (Entity line : lines) { - assertLineIsVisible(image, document, line); - assertThat(line.getComponent(Placement.class)).isPresent(); - } - } - } - - @Test - void shouldPaginateLineColumnAcrossPagesWithGuideLines() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("line_pagination", "guides", "integration"); - List lines = new ArrayList<>(); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - var containerBuilder = cb.vContainer(Align.middle(10)) - .entityName("LinePaginationColumn") - .anchor(Anchor.topCenter()) - .margin(Margin.of(20)); - - for (int i = 0; i < 32; i++) { - Entity line = cb.line() - .size(420, 16) - .padding(Padding.of(6)) - .stroke(new Stroke(i % 2 == 0 ? ComponentColor.ROYAL_BLUE : ComponentColor.DARK_BLUE, 3.0)) - .margin(Margin.of(8)) - .anchor(Anchor.center()) - .entityName("Line" + i) - .build(); - lines.add(line); - containerBuilder.addChild(line); - } - - containerBuilder.build(); - composer.build(); - } - - assertPdfExists(outputFile, 2); - - int pageCount; - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - pageCount = document.getNumberOfPages(); - assertThat(pageCount).isGreaterThan(1); - } - - Set occupiedPages = new HashSet<>(); - for (Entity line : lines) { - Placement placement = line.getComponent(Placement.class).orElseThrow(); - assertThat(placement.startPage()).isEqualTo(placement.endPage()); - assertThat(placement.startPage()).isGreaterThanOrEqualTo(0); - assertThat(placement.endPage()).isLessThan(pageCount); - occupiedPages.add(placement.startPage()); - } - - assertThat(occupiedPages) - .as("Lines should be distributed across more than one page") - .hasSizeGreaterThan(1); - } - - private void assertPdfExists(Path outputFile, int minPages) throws Exception { - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - assertThat(document.getNumberOfPages()).isGreaterThanOrEqualTo(minPages); - } - } - - private void assertLineIsVisible(BufferedImage image, PDDocument document, Entity entity) { - Placement placement = entity.getComponent(Placement.class).orElseThrow(); - Padding padding = entity.getComponent(Padding.class).orElse(Padding.zero()); - LinePath path = entity.getComponent(LinePath.class).orElseThrow(); - Stroke stroke = entity.getComponent(Stroke.class).orElseThrow(); - - double drawableX = placement.x() + padding.left(); - double drawableY = placement.y() + padding.bottom(); - double drawableWidth = Math.max(0.0, placement.width() - padding.horizontal()); - double drawableHeight = Math.max(0.0, placement.height() - padding.vertical()); - - int hits = 0; - for (double t : new double[]{0.05, 0.25, 0.5, 0.75, 0.95}) { - double sampleX = interpolate(drawableX + drawableWidth * path.startX(), drawableX + drawableWidth * path.endX(), t); - double sampleY = interpolate(drawableY + drawableHeight * path.startY(), drawableY + drawableHeight * path.endY(), t); - - if (hasColorNear(image, sampleX, sampleY, stroke.strokeColor().color(), - Math.max(2, (int) Math.ceil(stroke.width() * RENDER_SCALE)))) { - hits++; - } - } - - assertThat(hits) - .as("Expected visible line pixels for %s", entity) - .isGreaterThanOrEqualTo(4); - } - - private boolean hasColorNear(BufferedImage image, - double pdfX, - double pdfY, - Color expected, - int radius) { - int centerX = (int) Math.round(pdfX * RENDER_SCALE); - int centerY = image.getHeight() - 1 - (int) Math.round(pdfY * RENDER_SCALE); - - for (int y = Math.max(0, centerY - radius); y <= Math.min(image.getHeight() - 1, centerY + radius); y++) { - for (int x = Math.max(0, centerX - radius); x <= Math.min(image.getWidth() - 1, centerX + radius); x++) { - if (isCloseColor(new Color(image.getRGB(x, y), true), expected, 35)) { - return true; - } - } - } - return false; - } - - private boolean isCloseColor(Color actual, Color expected, int tolerancePerChannel) { - return Math.abs(actual.getRed() - expected.getRed()) <= tolerancePerChannel - && Math.abs(actual.getGreen() - expected.getGreen()) <= tolerancePerChannel - && Math.abs(actual.getBlue() - expected.getBlue()) <= tolerancePerChannel; - } - - private double interpolate(double start, double end, double t) { - return start + ((end - start) * t); - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/PageBreakerIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/PageBreakerIntegrationTest.java deleted file mode 100644 index 1573aa175..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/PageBreakerIntegrationTest.java +++ /dev/null @@ -1,382 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.content.text.BlockTextData; -import com.demcha.compose.engine.components.content.text.LineTextData; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.compose.font.FontName; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.awt.*; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration test for page breaking functionality. - * Tests that large content correctly spans multiple pages. - */ -class PageBreakerIntegrationTest { - - @Test - void shouldBreakTextAcrossPages() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("page_breaker_text_test", "guides", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - List textBlocks = createLargeTextContent(cb, 95, 3); - - var containerBuilder = cb.vContainer(Align.middle(5)) - .entityName("MainContainer") - .anchor(Anchor.topCenter()) - .margin(Margin.of(50)); - - for (Entity block : textBlocks) { - containerBuilder.addChild(block); - } - containerBuilder.build(); - - composer.build(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - - // Verify multiple pages were created - try (PDDocument doc = Loader.loadPDF(outputFile.toFile())) { - assertThat(doc.getNumberOfPages()).isGreaterThan(1); - } - } - - @Test - void shouldPreserveCachedLineMeasurementsAcrossPageBreakCopies() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("page_breaker_block_text_cache_test", "clean", "integration"); - Entity blockText; - List widthsBeforeLayout; - List baselinesBeforeLayout; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .markdown(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - blockText = cb.blockText(Align.left(3), TextStyle.builder() - .fontName(FontName.HELVETICA) - .size(9) - .color(ComponentColor.BLACK) - .build()) - .size(320, 2) - .anchor(Anchor.topLeft()) - .padding(Padding.of(6)) - .margin(Margin.of(8)) - .entityName("PageBreakCacheProbe") - .text(List.of(createNestedMarkdownHeadingText(22)), - TextStyle.builder() - .fontName(FontName.HELVETICA) - .size(9) - .color(ComponentColor.BLACK) - .build(), - Padding.zero(), - Margin.zero()) - .build(); - - BlockTextData beforeLayout = blockText.getComponent(BlockTextData.class).orElseThrow(); - widthsBeforeLayout = beforeLayout.lines().stream() - .map(LineTextData::lineWidth) - .collect(Collectors.toList()); - baselinesBeforeLayout = beforeLayout.lines().stream() - .map(LineTextData::baselineOffset) - .collect(Collectors.toList()); - - composer.build(); - } - - BlockTextData afterLayout = blockText.getComponent(BlockTextData.class).orElseThrow(); - assertThat(afterLayout.lines()).isNotEmpty(); - assertThat(afterLayout.lines()).allSatisfy(line -> { - assertThat(line.hasCachedLineWidth()).isTrue(); - assertThat(line.hasCachedLineMetrics()).isTrue(); - assertThat(line.hasCachedBaselineOffset()).isTrue(); - }); - assertThat(afterLayout.lines().stream().map(LineTextData::lineWidth).toList()) - .containsExactlyElementsOf(widthsBeforeLayout); - assertThat(afterLayout.lines().stream().map(LineTextData::baselineOffset).toList()) - .containsExactlyElementsOf(baselinesBeforeLayout); - assertThat(afterLayout.lines().stream().map(LineTextData::page).distinct().count()).isGreaterThan(1); - - try (PDDocument doc = Loader.loadPDF(outputFile.toFile())) { - assertThat(doc.getNumberOfPages()).isGreaterThan(1); - } - } - - @Test - void shouldBreakColoredRectanglesAcrossPages() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("page_breaker_rectangles_test", "guides", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - List rectangles = createColoredRectangles(cb); - - var containerBuilder = cb.vContainer(Align.middle(5)) - .entityName("MainContainer") - .anchor(Anchor.topCenter()) - .margin(Margin.of(50)); - - for (Entity rect : rectangles) { - containerBuilder.addChild(rect); - } - containerBuilder.build(); - - composer.build(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - - try (PDDocument doc = Loader.loadPDF(outputFile.toFile())) { - assertThat(doc.getNumberOfPages()).isGreaterThan(1); - } - } - - @Test - void shouldBreakNestedContainersWithMarkdownHeadingsAcrossPages() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf( - "page_breaker_nested_container_markdown_heading_test", - "guides", - "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(18, 18, 18, 18) - .markdown(true) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - - Entity title = cb.text() - .textWithAutoSize("Nested Container Markdown Heading Pagination Preview") - .textStyle(TextStyle.builder() - .fontName(FontName.HELVETICA) - .size(12) - .decoration(com.demcha.compose.engine.components.content.text.TextDecoration.BOLD) - .color(ComponentColor.BLACK) - .build()) - .anchor(Anchor.topLeft()) - .padding(Padding.zero()) - .margin(Margin.zero()) - .entityName("NestedPreviewTitle") - .build(); - - Entity markdownBlock = cb.blockText(Align.left(2), TextStyle.builder() - .fontName(FontName.HELVETICA) - .size(9) - .color(ComponentColor.BLACK) - .build()) - .size(320, 2) - .anchor(Anchor.topLeft()) - .padding(Padding.of(6)) - .margin(Margin.of(4)) - .entityName("NestedMarkdownHeadingBlock") - .text(List.of(createNestedMarkdownHeadingText(20)), - TextStyle.builder() - .fontName(FontName.HELVETICA) - .size(9) - .color(ComponentColor.BLACK) - .build(), - Padding.zero(), - Margin.zero()) - .build(); - - Entity innerContainer = cb.vContainer(Align.middle(8)) - .entityName("InnerMarkdownContainer") - .anchor(Anchor.topLeft()) - .padding(Padding.of(12)) - .margin(Margin.of(8)) - .addChild(markdownBlock) - .build(); - - cb.vContainer(Align.middle(12)) - .entityName("OuterPreviewContainer") - .anchor(Anchor.topCenter()) - .padding(Padding.of(10)) - .margin(Margin.of(24)) - .addChild(title) - .addChild(innerContainer) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - - try (PDDocument doc = Loader.loadPDF(outputFile.toFile())) { - assertThat(doc.getNumberOfPages()).isGreaterThan(1); - } - } - - @Test - void shouldExpandParentPlacementAfterLeafMovesToNextPage() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf( - "page_breaker_parent_after_leaf_shift_test", - "guides", - "integration"); - - Placement parentPlacement; - Placement leafPlacement; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(true) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - - Entity spacer = cb.rectangle() - .size(320, 680) - .fillColor(ComponentColor.LIGHT_GRAY) - .margin(Margin.of(8)) - .entityName("PaginationSpacer") - .build(); - - Entity shiftedLeaf = cb.rectangle() - .size(320, 220) - .fillColor(ComponentColor.ROYAL_BLUE) - .stroke(new Stroke(ComponentColor.DARK_BLUE, 2)) - .margin(Margin.of(8)) - .entityName("ShiftedLeaf") - .build(); - - Entity container = cb.vContainer(Align.middle(10)) - .entityName("ShiftedLeafContainer") - .anchor(Anchor.topCenter()) - .margin(Margin.of(24)) - .addChild(spacer) - .addChild(shiftedLeaf) - .build(); - - composer.build(); - - parentPlacement = container.getComponent(Placement.class).orElseThrow(); - leafPlacement = shiftedLeaf.getComponent(Placement.class).orElseThrow(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - - try (PDDocument doc = Loader.loadPDF(outputFile.toFile())) { - assertThat(doc.getNumberOfPages()).isGreaterThan(1); - } - - assertThat(leafPlacement.startPage()).isEqualTo(leafPlacement.endPage()); - assertThat(leafPlacement.startPage()).isGreaterThan(0); - - int parentFirstPage = Math.min(parentPlacement.startPage(), parentPlacement.endPage()); - int parentLastPage = Math.max(parentPlacement.startPage(), parentPlacement.endPage()); - - assertThat(parentFirstPage).isEqualTo(0); - assertThat(parentLastPage).isGreaterThan(parentFirstPage); - assertThat(leafPlacement.startPage()).isBetween(parentFirstPage, parentLastPage); - } - - private List createLargeTextContent(ComponentBuilder cb, int rows, double spacing) { - List data = new ArrayList<>(); - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i <= rows; i++) { - sb.append(i).append(". Test text line - We will see how we break our text into pages\n"); - - if (i > 0 && i % 20 == 0) { - data.add(createBlockText(cb, sb.toString(), 302, spacing, "textBlock" + i / 20)); - sb = new StringBuilder(); - } - if (i == rows && !sb.isEmpty()) { - data.add(createBlockText(cb, sb.toString(), 302, spacing, "textBlockFinal")); - } - } - return data; - } - - private Entity createBlockText(ComponentBuilder cb, String text, double width, double spacing, String name) { - return cb.blockText(Align.middle(spacing), TextStyle.builder() - .fontName(FontName.HELVETICA) - .size(9) - .color(ComponentColor.BLACK) - .build()) - .size(width, 2) - .anchor(Anchor.center()) - .padding(Padding.of(5)) - .margin(Margin.of(5)) - .entityName(name) - .text(List.of(text), TextStyle.DEFAULT_STYLE, Padding.of(5), Margin.of(5)) - .build(); - } - - private List createColoredRectangles(ComponentBuilder cb) { - List data = new ArrayList<>(); - List colors = List.of(Color.BLUE, Color.DARK_GRAY, Color.GREEN, Color.RED, Color.YELLOW); - - for (int i = 0; i < 5; i++) { - Entity rect = cb.rectangle() - .size(300, 300) - .stroke(new Stroke(ComponentColor.PURPLE, 2)) - .fillColor(colors.get(i)) - .entityName("Rectangle" + i) - .build(); - data.add(rect); - } - return data; - } - - private String createNestedMarkdownHeadingText(int sections) { - String paragraph = "This paragraph lives inside a nested container and is intentionally long so the page breaker " + - "has to keep recalculating line positions while markdown headings change the local line metrics. " + - "We want to see that the heading breathes a bit more than the body text and that the container still " + - "flows correctly across page boundaries without collapsing the reserved height. "; - - StringBuilder sb = new StringBuilder(); - for (int i = 1; i <= sections; i++) { - sb.append("# Section ").append(i).append('\n'); - sb.append(paragraph.repeat(3)).append('\n'); - sb.append("Continuation line for section ").append(i) - .append(" with **bold text** and *italic text* so markdown token styles stay mixed inside the same block.") - .append('\n'); - sb.append("Another continuation line for section ").append(i) - .append(" to force wrapping inside the inner container and expose page splitting more clearly.") - .append('\n'); - } - return sb.toString(); - } -} - diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/RepositoryShowcaseRenderTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/RepositoryShowcaseRenderTest.java deleted file mode 100644 index 7c9b5df20..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/RepositoryShowcaseRenderTest.java +++ /dev/null @@ -1,349 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.font.FontName; -import com.demcha.compose.engine.components.content.text.TextIndentStrategy; -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.content.table.TableCellLayoutStyle; -import com.demcha.compose.engine.components.content.table.TableColumnLayout; -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.content.text.TextDecoration; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.compose.testing.layout.LayoutSnapshotAssertions; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.awt.Color; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -class RepositoryShowcaseRenderTest { - private static final Path COVER_ASSET = locateRepoAsset(Path.of("assets", "GraphComposeCover.png")); - - /** - * Resolves a repository-root asset independently of which module runs the - * test. The suite moved from the engine module (working dir = repo root) to - * the qa reactor tail (working dir = {@code qa/}); walking up from the - * working directory until the relative path resolves keeps the cover image - * reachable from either location. - */ - private static Path locateRepoAsset(Path relative) { - for (Path dir = Path.of("").toAbsolutePath(); dir != null; dir = dir.getParent()) { - Path candidate = dir.resolve(relative); - if (Files.exists(candidate)) { - return candidate; - } - } - return relative; - } - private static final Color TITLE_COLOR = new Color(18, 40, 74); - private static final Color ACCENT_COLOR = new Color(37, 128, 197); - private static final Color MUTED_COLOR = new Color(86, 98, 120); - private static final Color HEADER_FILL = new Color(226, 238, 250); - private static final Color FIRST_COLUMN_FILL = new Color(245, 249, 253); - private static final Color TABLE_BORDER = new Color(172, 188, 205); - private static final Color TABLE_BORDER_STRONG = new Color(136, 157, 181); - - @Test - void shouldRenderRepositoryShowcaseOnSinglePageWithoutGuideLines() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("repository_showcase_render", "clean", "integration"); - - renderRepositoryShowcase(outputFile, false); - - assertSinglePagePdfLooksValid(outputFile); - } - - @Test - void shouldRenderRepositoryShowcaseOnSinglePageWithGuideLines() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("repository_showcase_render_guides", "guides", "integration"); - - renderRepositoryShowcase(outputFile, true); - - assertSinglePagePdfLooksValid(outputFile); - } - - private void renderRepositoryShowcase(Path outputFile, boolean guideLines) throws Exception { - assertThat(COVER_ASSET).exists(); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(16, 16, 16, 16) - .markdown(true) - .guideLines(guideLines) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - double contentWidth = composer.canvas().innerWidth(); - - Entity title = text(cb, - "GraphCompose", - TextStyle.builder() - .fontName(FontName.POPPINS) - .size(33) - .decoration(TextDecoration.BOLD) - .color(TITLE_COLOR) - .build(), - Anchor.topCenter()); - - Entity heroImage = cb.image() - .image(COVER_ASSET) - .fitToBounds(contentWidth - 12, 158) - .padding(Padding.of(4)) - .anchor(Anchor.topCenter()) - .build(); - - Entity subtitle = text(cb, - "A declarative layout engine for programmatic document generation", - TextStyle.builder() - .fontName(FontName.IBM_PLEX_SERIF) - .size(11.5) - .decoration(TextDecoration.DEFAULT) - .color(MUTED_COLOR) - .build(), - Anchor.topCenter()); - - Entity intro = paragraph( - cb, - List.of( - "This render test reuses the visual rhythm of `README.md`, but rebuilds the page in a clean light style using only **GraphCompose** primitives. " + - "The composition intentionally shows centered layout, a cover image from `assets`, wrapped markdown text, accent lines, and a styled table on the same document." - ), - contentWidth - 18, - 9.7, - Anchor.topCenter(), - TextIndentStrategy.FIRST_LINE); - - Entity separatorTop = separator(cb, contentWidth * 0.9); - Entity sectionWhat = text(cb, "What GraphCompose is", sectionStyle(), Anchor.topLeft()); - Entity overview = bulletParagraph( - cb, - List.of( - "**Builders** create `Entity` trees instead of drawing directly to absolute PDF coordinates.", - "**Layout systems** resolve size and placement before rendering starts.", - "**Rendering systems** convert the final geometry into PDF output." - ), - contentWidth - 12); - - Entity sectionBenchmark = text(cb, "Comparative Benchmark", sectionStyle(), Anchor.topLeft()); - Entity benchmarkLead = paragraph( - cb, - List.of("A compact benchmark table keeps the `README.md` spirit while also demonstrating `table()` with fills, borders, padding, and aligned columns."), - contentWidth - 10, - 9.0, - Anchor.topLeft(), - TextIndentStrategy.FIRST_LINE); - Entity featuresTable = showcaseTable(cb, contentWidth); - Entity sectionLines = text(cb, "Primitive Showcase", sectionStyle(), Anchor.topLeft()); - Entity lineGallery = lineGallery(cb); - Entity closingNote = paragraph( - cb, - List.of( - "The same page can be rendered with guide lines enabled, so this test works both as a polished sample and as a practical visual debug artifact while `GraphCompose` evolves." - ), - contentWidth - 12, - 8.9, - Anchor.topLeft(), - TextIndentStrategy.FIRST_LINE); - - cb.vContainer(Align.middle(9)) - .entityName("RepositoryShowcaseRoot") - .anchor(Anchor.topCenter()) - .margin(Margin.of(4)) - .addChild(title) - .addChild(heroImage) - .addChild(subtitle) - .addChild(intro) - .addChild(separatorTop) - .addChild(sectionWhat) - .addChild(overview) - .addChild(sectionBenchmark) - .addChild(benchmarkLead) - .addChild(featuresTable) - .addChild(sectionLines) - .addChild(lineGallery) - .addChild(closingNote) - .build(); - - if (!guideLines) { - LayoutSnapshotAssertions.assertMatches(composer.layoutSnapshot(), "repository_showcase_render", "integration"); - } - composer.build(); - } - } - - private Entity showcaseTable(ComponentBuilder cb, double contentWidth) { - TextStyle bodyStyle = TextStyle.builder() - .fontName(FontName.HELVETICA) - .size(8.8) - .decoration(TextDecoration.DEFAULT) - .color(TITLE_COLOR) - .build(); - - TextStyle headerStyle = TextStyle.builder() - .fontName(FontName.POPPINS) - .size(8.9) - .decoration(TextDecoration.BOLD) - .color(TITLE_COLOR) - .build(); - - return cb.table() - .entityName("RepositoryCapabilitiesTable") - .anchor(Anchor.topCenter()) - .columns( - TableColumnLayout.fixed(144), - TableColumnLayout.fixed(112), - TableColumnLayout.fixed(112)) - .width(contentWidth - 6) - .defaultCellStyle(TableCellLayoutStyle.builder() - .padding(Padding.of(5)) - .fillColor(ComponentColor.WHITE) - .stroke(new Stroke(TABLE_BORDER, 1.1)) - .textStyle(bodyStyle) - .textAnchor(Anchor.centerLeft()) - .build()) - .rowStyle(0, TableCellLayoutStyle.builder() - .fillColor(HEADER_FILL) - .textStyle(headerStyle) - .build()) - .columnStyle(0, TableCellLayoutStyle.builder() - .fillColor(FIRST_COLUMN_FILL) - .stroke(new Stroke(TABLE_BORDER_STRONG, 1.5)) - .build()) - .columnStyle(1, TableCellLayoutStyle.builder() - .textAnchor(Anchor.center()) - .build()) - .columnStyle(2, TableCellLayoutStyle.builder() - .textAnchor(Anchor.center()) - .build()) - .row("Library", "Avg Time (ms)", "Avg Heap (MB)") - .row("GraphCompose", "2.45", "0.16") - .row("iText 5 (Old)", "1.57", "0.16") - .row("JasperReports", "4.45", "0.19") - .build(); - } - - private Entity lineGallery(ComponentBuilder cb) { - Entity horizontal = cb.line() - .horizontal() - .size(120, 14) - .padding(Padding.of(3)) - .stroke(new Stroke(new Color(38, 91, 168), 2.5)) - .anchor(Anchor.center()) - .build(); - - Entity vertical = cb.line() - .vertical() - .size(14, 46) - .padding(Padding.of(3)) - .stroke(new Stroke(new Color(204, 121, 47), 2.5)) - .anchor(Anchor.center()) - .build(); - - Entity diagonal = cb.line() - .diagonalAscending() - .size(104, 38) - .padding(Padding.of(4)) - .stroke(new Stroke(new Color(50, 135, 94), 2.5)) - .anchor(Anchor.center()) - .build(); - - return cb.hContainer(Align.middle(18)) - .entityName("RepositoryShowcaseLineGallery") - .anchor(Anchor.topCenter()) - .margin(Margin.top(2)) - .addChild(horizontal) - .addChild(vertical) - .addChild(diagonal) - .build(); - } - - private Entity bulletParagraph(ComponentBuilder cb, List lines, double width) { - TextStyle style = TextStyle.builder() - .fontName(FontName.HELVETICA) - .size(9.4) - .decoration(TextDecoration.DEFAULT) - .color(TITLE_COLOR) - .build(); - - return cb.blockText(Align.left(5), style) - .size(width, 2) - .padding(0, 0, 0, 0) - .strategy(TextIndentStrategy.ALL_LINES) - .bulletOffset("•") - .text(lines, style, null, null) - .anchor(Anchor.topLeft()) - .build(); - } - - private Entity paragraph(ComponentBuilder cb, - List lines, - double width, - double fontSize, - Anchor anchor, - TextIndentStrategy strategy) { - TextStyle style = TextStyle.builder() - .fontName(FontName.HELVETICA) - .size(fontSize) - .decoration(TextDecoration.DEFAULT) - .color(MUTED_COLOR) - .build(); - - return cb.blockText(Align.left(4), style) - .size(width, 2) - .padding(0, 0, 0, 0) - .strategy(strategy) - .text(lines, style, null, null) - .anchor(anchor) - .build(); - } - - private Entity separator(ComponentBuilder cb, double width) { - return cb.line() - .horizontal() - .size(width, 10) - .padding(Padding.of(2)) - .stroke(new Stroke(ACCENT_COLOR, 1.9)) - .anchor(Anchor.topCenter()) - .build(); - } - - private Entity text(ComponentBuilder cb, String value, TextStyle style, Anchor anchor) { - return cb.text() - .textWithAutoSize(value) - .textStyle(style) - .anchor(anchor) - .build(); - } - - private TextStyle sectionStyle() { - return TextStyle.builder() - .fontName(FontName.POPPINS) - .size(12.2) - .decoration(TextDecoration.BOLD) - .color(TITLE_COLOR) - .build(); - } - - private void assertSinglePagePdfLooksValid(Path outputFile) throws Exception { - assertThat(outputFile).exists(); - assertThat(outputFile).isRegularFile(); - assertThat(outputFile).isNotEmptyFile(); - - try (PDDocument saved = Loader.loadPDF(outputFile.toFile())) { - assertThat(saved.getNumberOfPages()).isEqualTo(1); - } - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/SingleLineTextIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/SingleLineTextIntegrationTest.java deleted file mode 100644 index eae373195..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/SingleLineTextIntegrationTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.compose.engine.render.pdf.PdfFont; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.text.PDFTextStripper; -import org.apache.pdfbox.text.TextPosition; -import org.junit.jupiter.api.Test; - -import java.io.IOException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.within; - -class SingleLineTextIntegrationTest { - - @Test - void shouldRenderAutosizedSingleLineTextAtPaddingAdjustedBaseline() throws Exception { - TextStyle style = TextStyle.DEFAULT_STYLE; - Padding padding = Padding.of(5); - Margin margin = Margin.of(5); - Entity textEntity; - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(PDRectangle.A4) - .create()) { - - textEntity = composer.componentBuilder() - .text() - .textWithAutoSize("In-memory PDF") - .padding(padding) - .margin(margin) - .textStyle(style) - .anchor(Anchor.topLeft()) - .build(); - - PDDocument document = composer.toPDDocument(); - TextPosition firstGlyph = firstVisibleGlyph(document); - Placement placement = textEntity.getComponent(Placement.class).orElseThrow(); - PdfFont.VerticalMetrics metrics; - try (PDDocument fontDocument = new PDDocument()) { - PdfFont pdfFont = (PdfFont) PdfFontLibraryFactory.library(fontDocument) - .getFont(style.fontName(), PdfFont.class) - .orElseThrow(); - metrics = pdfFont.verticalMetrics(style); - } - - assertThat(firstGlyph).isNotNull(); - assertThat(firstGlyph.getUnicode()).startsWith("I"); - assertThat((double) firstGlyph.getTextMatrix().getTranslateX()) - .isCloseTo(placement.x() + padding.left(), within(0.5)); - assertThat((double) firstGlyph.getTextMatrix().getTranslateY()) - .isCloseTo(placement.y() + padding.bottom() + metrics.baselineOffsetFromBottom(), within(0.5)); - } - } - - private TextPosition firstVisibleGlyph(PDDocument document) throws IOException { - final TextPosition[] first = new TextPosition[1]; - - PDFTextStripper stripper = new PDFTextStripper() { - @Override - protected void processTextPosition(TextPosition text) { - if (first[0] == null && text.getUnicode() != null && !text.getUnicode().isBlank()) { - first[0] = text; - } - super.processTextPosition(text); - } - }; - - stripper.getText(document); - - assertThat(first[0]).as("first rendered glyph").isNotNull(); - return first[0]; - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/TableColSpanIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/TableColSpanIntegrationTest.java deleted file mode 100644 index 2ac7798f0..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/TableColSpanIntegrationTest.java +++ /dev/null @@ -1,250 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.engine.components.content.shape.Side; -import com.demcha.compose.engine.components.content.table.TableCellContent; -import com.demcha.compose.engine.components.content.table.TableCellLayoutStyle; -import com.demcha.compose.engine.components.content.table.TableColumnLayout; -import com.demcha.compose.engine.components.content.table.TableLayoutData; -import com.demcha.compose.engine.components.content.table.TableResolvedCell; -import com.demcha.compose.engine.components.content.table.TableRowData; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.within; - -class TableColSpanIntegrationTest { - - private static final double EPS = 1e-6; - - @Test - void shouldCollapseTotalsRowIntoSpannedLabelAndValue() throws Exception { - Path output = VisualTestOutputs.preparePdf("table_colspan_totals", "clean", "integration"); - - try (EngineComposerHarness composer = EngineComposerHarness.pdf(output) - .pageSize(PDRectangle.A4) - .margin(24, 24, 24, 24) - .guideLines(false) - .create()) { - - Entity table = composer.componentBuilder() - .table() - .entityName("ColSpanTotals") - .anchor(Anchor.topCenter()) - .margin(Margin.of(12)) - .columns( - TableColumnLayout.fixed(150), - TableColumnLayout.fixed(80), - TableColumnLayout.fixed(80), - TableColumnLayout.fixed(100)) - .row("Item", "Qty", "Unit", "Amount") - .row("Coffee beans", "12", "$15.00", "$180.00") - .row("Filters", "4", "$5.00", "$20.00") - .row( - TableCellContent.text("Total").withColSpan(3).withStyle(TableCellLayoutStyle.builder() - .fillColor(ComponentColor.LIGHT_GRAY) - .textAnchor(Anchor.centerRight()) - .build()), - TableCellContent.text("$200.00").withStyle(TableCellLayoutStyle.builder() - .fillColor(ComponentColor.LIGHT_GRAY) - .textAnchor(Anchor.centerRight()) - .build())) - .build(); - - TableLayoutData layoutData = table.getComponent(TableLayoutData.class).orElseThrow(); - assertThat(layoutData.columnCount()).isEqualTo(4); - assertThat(layoutData.rowCount()).isEqualTo(4); - - TableRowData totalsRow = layoutData.rowEntities().get(3).getComponent(TableRowData.class).orElseThrow(); - assertThat(totalsRow.cells()).hasSize(2); - - TableResolvedCell label = totalsRow.cells().get(0); - assertThat(label.x()).isEqualTo(0.0, within(EPS)); - assertThat(label.width()).isEqualTo(150.0 + 80.0 + 80.0, within(EPS)); - - TableResolvedCell amount = totalsRow.cells().get(1); - assertThat(amount.x()).isEqualTo(150.0 + 80.0 + 80.0, within(EPS)); - assertThat(amount.width()).isEqualTo(100.0, within(EPS)); - - composer.build(); - } - - assertThat(output).exists().isNotEmptyFile(); - } - - @Test - void shouldRenderHeaderGroupWithSpannedMiddleCell() throws Exception { - Path output = VisualTestOutputs.preparePdf("table_colspan_header_group", "clean", "integration"); - - try (EngineComposerHarness composer = EngineComposerHarness.pdf(output) - .pageSize(PDRectangle.A4) - .margin(24, 24, 24, 24) - .guideLines(false) - .create()) { - - Entity table = composer.componentBuilder() - .table() - .entityName("ColSpanHeader") - .anchor(Anchor.topCenter()) - .margin(Margin.of(12)) - .columns( - TableColumnLayout.fixed(120), - TableColumnLayout.fixed(90), - TableColumnLayout.fixed(90), - TableColumnLayout.fixed(120)) - .row( - TableCellContent.text("Region"), - TableCellContent.text("Sales").withColSpan(2), - TableCellContent.text("Owner")) - .row("North", "Q1: $10k", "Q2: $14k", "Mark") - .row("South", "Q1: $8k", "Q2: $11k", "Anna") - .build(); - - TableLayoutData layoutData = table.getComponent(TableLayoutData.class).orElseThrow(); - assertThat(layoutData.columnCount()).isEqualTo(4); - - TableRowData header = layoutData.rowEntities().get(0).getComponent(TableRowData.class).orElseThrow(); - assertThat(header.cells()).hasSize(3); - - TableResolvedCell region = header.cells().get(0); - assertThat(region.x()).isEqualTo(0.0, within(EPS)); - assertThat(region.width()).isEqualTo(120.0, within(EPS)); - - TableResolvedCell sales = header.cells().get(1); - assertThat(sales.x()).isEqualTo(120.0, within(EPS)); - assertThat(sales.width()).isEqualTo(90.0 + 90.0, within(EPS)); - - TableResolvedCell owner = header.cells().get(2); - assertThat(owner.x()).isEqualTo(120.0 + 90.0 + 90.0, within(EPS)); - assertThat(owner.width()).isEqualTo(120.0, within(EPS)); - - TableRowData dataRow = layoutData.rowEntities().get(1).getComponent(TableRowData.class).orElseThrow(); - assertThat(dataRow.cells()).hasSize(4); - - composer.build(); - } - - assertThat(output).exists().isNotEmptyFile(); - } - - @Test - void shouldRenderFullWidthSectionDividerRow() throws Exception { - Path output = VisualTestOutputs.preparePdf("table_colspan_section", "clean", "integration"); - - try (EngineComposerHarness composer = EngineComposerHarness.pdf(output) - .pageSize(PDRectangle.A4) - .margin(24, 24, 24, 24) - .guideLines(false) - .create()) { - - Entity table = composer.componentBuilder() - .table() - .entityName("ColSpanSection") - .anchor(Anchor.topCenter()) - .margin(Margin.of(12)) - .columns( - TableColumnLayout.fixed(110), - TableColumnLayout.fixed(110), - TableColumnLayout.fixed(110)) - .row("Phase", "Owner", "Status") - .row("Discovery", "Mark", "Done") - .row( - TableCellContent.text("— PHASE 2 —").withColSpan(3).withStyle(TableCellLayoutStyle.builder() - .fillColor(ComponentColor.LIGHT_GRAY) - .textAnchor(Anchor.center()) - .build())) - .row("Implementation", "Anna", "In progress") - .build(); - - TableLayoutData layoutData = table.getComponent(TableLayoutData.class).orElseThrow(); - assertThat(layoutData.rowCount()).isEqualTo(4); - - TableRowData divider = layoutData.rowEntities().get(2).getComponent(TableRowData.class).orElseThrow(); - assertThat(divider.cells()).hasSize(1); - - TableResolvedCell only = divider.cells().get(0); - assertThat(only.x()).isEqualTo(0.0, within(EPS)); - assertThat(only.width()).isEqualTo(110.0 * 3, within(EPS)); - assertThat(only.borderSides()).contains(Side.LEFT, Side.RIGHT); - - composer.build(); - } - - assertThat(output).exists().isNotEmptyFile(); - } - - @Test - void shouldFailWhenColSpanSumDoesNotMatchColumnCount() throws Exception { - try (EngineComposerHarness composer = EngineComposerHarness.pdf().create()) { - assertThatThrownBy(() -> composer.componentBuilder() - .table() - .entityName("BrokenSpan") - .columns(TableColumnLayout.auto(), TableColumnLayout.auto(), TableColumnLayout.auto()) - .row("A", "B", "C") - .row( - TableCellContent.text("Wrong").withColSpan(2), - TableCellContent.text("X"), - TableCellContent.text("Y")) - .build()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("colSpan sum"); - } - } - - @Test - void shouldFailWhenColSpanIsZeroOrNegative() throws Exception { - assertThatThrownBy(() -> TableCellContent.text("X").withColSpan(0)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("colSpan"); - - assertThatThrownBy(() -> TableCellContent.text("X").withColSpan(-1)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("colSpan"); - } - - @Test - void shouldDistributeDeficitToAutoColumnsWhenSpannedCellNeedsMoreWidth() throws Exception { - try (EngineComposerHarness composer = EngineComposerHarness.pdf().create()) { - Entity table = composer.componentBuilder() - .table() - .entityName("ColSpanDeficit") - .columns(TableColumnLayout.fixed(40), TableColumnLayout.auto(), TableColumnLayout.auto()) - .row("ID", "First", "Second") - .row( - TableCellContent.text("X"), - TableCellContent.text("This is a very long spanned label that needs more room").withColSpan(2)) - .build(); - - TableLayoutData layoutData = table.getComponent(TableLayoutData.class).orElseThrow(); - assertThat(layoutData.columnWidths().get(0)).isEqualTo(40.0, within(EPS)); - - double col1Width = layoutData.columnWidths().get(1); - double col2Width = layoutData.columnWidths().get(2); - assertThat(col1Width + col2Width).isGreaterThan(50.0); - } - } - - @Test - void shouldFailWhenSpannedCellNeedsExtraWidthButAllSpannedColumnsAreFixed() throws Exception { - try (EngineComposerHarness composer = EngineComposerHarness.pdf().create()) { - assertThatThrownBy(() -> composer.componentBuilder() - .table() - .entityName("ColSpanFixedDeficit") - .columns(TableColumnLayout.fixed(20), TableColumnLayout.fixed(20)) - .row("a", "b") - .row(TableCellContent.text("Way too long for forty points combined").withColSpan(2)) - .build()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("fixed"); - } - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/TableLayoutIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/TableLayoutIntegrationTest.java deleted file mode 100644 index 681c18b5f..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/TableLayoutIntegrationTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.engine.components.content.table.TableCellLayoutStyle; -import com.demcha.compose.engine.components.content.table.TableColumnLayout; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -class TableLayoutIntegrationTest { - - @Test - void shouldRenderTableWithNegotiatedColumns() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("table_layout_test", "clean", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(false) - .create()) { - - composer.componentBuilder() - .table() - .entityName("RoadmapTable") - .anchor(Anchor.topCenter()) - .margin(Margin.of(16)) - .columns( - TableColumnLayout.fixed(90), - TableColumnLayout.auto(), - TableColumnLayout.auto() - ) - .defaultCellStyle(TableCellLayoutStyle.builder() - .fillColor(ComponentColor.WHITE) - .build()) - .columnStyle(0, TableCellLayoutStyle.builder() - .fillColor(ComponentColor.LIGHT_GRAY) - .build()) - .row("Q1", "Foundations", "Layout engine baseline") - .row("Q2", "Tables", "Negotiated widths and row pagination") - .row("Q3", "Templates", "Resume and letter refinements") - .row("Q4", "Exports", "Renderer stabilization") - .build(); - - composer.build(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/TableMultilineIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/TableMultilineIntegrationTest.java deleted file mode 100644 index d20538092..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/TableMultilineIntegrationTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.engine.components.content.table.TableCellContent; -import com.demcha.compose.engine.components.content.table.TableCellLayoutStyle; -import com.demcha.compose.engine.components.content.table.TableColumnLayout; -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -class TableMultilineIntegrationTest { - - @Test - void shouldRenderMultilineCellsWithCellOverridesAndVerticalAnchors() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("table_multiline_cells", "clean", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(false) - .create()) { - - composer.componentBuilder() - .table() - .entityName("ShiftTable") - .anchor(Anchor.topCenter()) - .margin(Margin.of(16)) - .columns( - TableColumnLayout.fixed(120), - TableColumnLayout.fixed(180), - TableColumnLayout.fixed(180) - ) - .width(480) - .defaultCellStyle(TableCellLayoutStyle.builder() - .padding(new Padding(6, 8, 6, 8)) - .fillColor(ComponentColor.WHITE) - .stroke(new Stroke(ComponentColor.BLACK, 1.0)) - .build()) - .row( - TableCellContent.text("Mark"), - TableCellContent.lines("09:00 17:00", "Clean crushed ice").withStyle(TableCellLayoutStyle.builder() - .fillColor(ComponentColor.LIGHT_GRAY) - .textAnchor(Anchor.topLeft()) - .build()), - TableCellContent.lines("12:00 20:00", "Training").withStyle(TableCellLayoutStyle.builder() - .fillColor(ComponentColor.LIGHT_BLUE) - .textAnchor(Anchor.centerLeft()) - .build())) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/TablePaginationIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/TablePaginationIntegrationTest.java deleted file mode 100644 index f1102118d..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/TablePaginationIntegrationTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.testsupport.engine.assembly.TableBuilder; -import com.demcha.compose.engine.components.content.table.TableCellContent; -import com.demcha.compose.engine.components.content.table.TableColumnLayout; -import com.demcha.compose.engine.components.content.table.TableLayoutData; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.compose.testing.layout.LayoutSnapshotAssertions; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -class TablePaginationIntegrationTest { - - @Test - void shouldKeepEachRowOnSinglePageWhenTableSpansMultiplePages() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("table_pagination_test", "clean", "integration"); - - Entity table; - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(false) - .create()) { - - TableBuilder builder = composer.componentBuilder() - .table() - .entityName("PaginationTable") - .anchor(Anchor.topCenter()) - .margin(Margin.of(10)) - .columns(TableColumnLayout.fixed(72), TableColumnLayout.auto(), TableColumnLayout.auto()) - .width(540) - .row("ID", "Category", "Description"); - - for (int i = 0; i < 95; i++) { - builder.row( - TableCellContent.text(String.valueOf(i + 1)), - TableCellContent.text("Category " + (i % 7)), - TableCellContent.lines( - "Single line value " + i + " for negotiated table pagination", - "Backup slot " + i + " 18:00-22:00") - ); - } - - table = builder.build(); - LayoutSnapshotAssertions.assertMatches(composer.layoutSnapshot(), "table_pagination_test", "integration"); - composer.build(); - - for (Entity row : table.getComponent(TableLayoutData.class).orElseThrow().rowEntities()) { - Placement placement = row.getComponent(Placement.class) - .orElseThrow(); - - assertThat(placement.startPage()).isEqualTo(placement.endPage()); - } - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - assertThat(document.getNumberOfPages()).isGreaterThan(1); - } - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/TableStylingIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/TableStylingIntegrationTest.java deleted file mode 100644 index b851a2801..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/TableStylingIntegrationTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.engine.components.content.table.TableCellLayoutStyle; -import com.demcha.compose.engine.components.content.table.TableColumnLayout; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -class TableStylingIntegrationTest { - - @Test - void shouldRenderScopedTableStyles() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("table_styling_test", "clean", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(24, 24, 24, 24) - .guideLines(false) - .create()) { - - composer.componentBuilder() - .table() - .entityName("StylingTable") - .anchor(Anchor.topCenter()) - .margin(Margin.of(12)) - .columns(TableColumnLayout.fixed(110), TableColumnLayout.auto(), TableColumnLayout.auto()) - .defaultCellStyle(TableCellLayoutStyle.builder() - .fillColor(ComponentColor.WHITE) - .padding(Padding.of(6)) - .build()) - .columnStyle(0, TableCellLayoutStyle.builder() - .fillColor(ComponentColor.LIGHT_GRAY) - .build()) - .rowStyle(0, TableCellLayoutStyle.builder() - .fillColor(ComponentColor.LIGHT_BLUE) - .padding(Padding.of(10)) - .build()) - .row("Role", "Owner", "Status") - .row("Engine", "GraphCompose", "Stable") - .row("Feature", "Table Builder", "In progress") - .build(); - - composer.build(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/integration/TextPaginationIntegrationTest.java b/qa/src/test/java/com/demcha/compose/engine/integration/TextPaginationIntegrationTest.java deleted file mode 100644 index 33659d800..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/integration/TextPaginationIntegrationTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.demcha.compose.engine.integration; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.testing.VisualTestOutputs; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -class TextPaginationIntegrationTest { - - private static final int TEXT_COMPONENTS_COUNT = 72; - - @Test - void shouldFillThreePagesWithText() throws Exception { - Path outputFile = VisualTestOutputs.preparePdf("text_pagination_three_pages", "clean", "integration"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .pageSize(PDRectangle.A4) - .margin(20, 20, 20, 20) - .guideLines(false) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - var container = cb.vContainer(Align.middle(4)) - .entityName("TextPaginationContainer") - .anchor(Anchor.topCenter()) - .margin(Margin.of(10)); - - for (int i = 0; i < TEXT_COMPONENTS_COUNT; i++) { - container.addChild(cb.text() - .textWithAutoSize(repeatedText(i)) - .textStyle(TextStyle.DEFAULT_STYLE) - .anchor(Anchor.center()) - .margin(Margin.of(4)) - .build()); - } - - container.build(); - composer.build(); - } - - assertThat(outputFile).exists(); - assertThat(outputFile).isNotEmptyFile(); - - try (PDDocument document = Loader.loadPDF(outputFile.toFile())) { - assertThat(document.getNumberOfPages()).isEqualTo(3); - } - } - - private String repeatedText(int index) { - return "TextComponentTest TextComponentTest TextComponentTest #" + index; - } -} - diff --git a/qa/src/test/java/com/demcha/compose/engine/render/RenderHandlerRegistryTest.java b/qa/src/test/java/com/demcha/compose/engine/render/RenderHandlerRegistryTest.java deleted file mode 100644 index 586ce7b41..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/render/RenderHandlerRegistryTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.demcha.compose.engine.render; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.render.pdf.ecs.PdfRenderingSystemECS; -import com.demcha.compose.engine.render.Render; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class RenderHandlerRegistryTest { - - @Test - void shouldPreferDirectHandlerOverParentTypeHandler() { - RenderHandlerRegistry registry = new RenderHandlerRegistry(); - ParentRenderHandler parent = new ParentRenderHandler(); - ChildRenderHandler child = new ChildRenderHandler(); - - registry.register(parent); - registry.register(child); - - var resolved = registry.find(new ChildRender()); - - assertThat(resolved).containsSame(child); - } - - private static class ParentRender implements Render { - } - - private static final class ChildRender extends ParentRender { - } - - private static final class ParentRenderHandler implements RenderHandler { - @Override - public Class renderType() { - return ParentRender.class; - } - - @Override - public boolean render(EntityManager manager, Entity entity, ParentRender renderComponent, PdfRenderingSystemECS renderingSystem, boolean guideLines) { - return true; - } - } - - private static final class ChildRenderHandler implements RenderHandler { - @Override - public Class renderType() { - return ChildRender.class; - } - - @Override - public boolean render(EntityManager manager, Entity entity, ChildRender renderComponent, PdfRenderingSystemECS renderingSystem, boolean guideLines) { - return true; - } - } -} diff --git a/qa/src/test/java/com/demcha/compose/engine/render/guides/GuidesRendererTest.java b/qa/src/test/java/com/demcha/compose/engine/render/guides/GuidesRendererTest.java deleted file mode 100644 index ef77eca06..000000000 --- a/qa/src/test/java/com/demcha/compose/engine/render/guides/GuidesRendererTest.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.demcha.compose.engine.render.guides; - -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.mock.FactoryClasses; -import com.demcha.mock.FactoryPresets; -import com.demcha.mock.data.CanvasData; -import com.demcha.mock.data.SizeData; -import com.demcha.compose.engine.render.pdf.ecs.PdfGuidesRenderer; -import com.demcha.compose.engine.render.pdf.ecs.PdfRenderingSystemECS; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; // Use standard AssertJ -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.mockito.Mockito.*; - -class GuidesRendererTest { - - private final PdfRenderingSystemECS renderingSystemMock = mock(PdfRenderingSystemECS.class); - // Spy is used to test the real breakCoordinate logic while mocking dependencies - private final PdfGuidesRenderer guidesRenderer = spy(new PdfGuidesRenderer(renderingSystemMock)); - - @BeforeEach - void setUp() { - doReturn(renderingSystemMock).when(guidesRenderer).renderingSystem(); - } - - // Helper Record for assertions - record ExpectedSegment(double y, double height, int page) {} - - static Stream provideBreakScenarios() { - return Stream.of( - - Arguments.of( - "Split across 3 Pages (Complex Loop)", - FactoryPresets.OBJ_TALL_5_12, // H=12 - FactoryPresets.CANVAS_SPLIT_5_M1, // H=5, T=2, B=2 - 2.0, // Start Y on Page 2 - 3, // pages - 3, // Expected Segments (40 + 80 + 30 = 150) - List.of( - // Segment 1: Page 2 (Tail) -> Space available: (100-10)-50 = 40 - new ExpectedSegment(2.0, 3.0, 2), - - // Segment 2: Page 1 (Full Page) -> Space available: 80. - // 110 remaining is > 80, so it takes full 80. - // Note: Y position depends on your logic: - // If logic uses margin.bottom() for next page start, check this value. - // Assuming standard top-down flow usually starts at margin.top(). - // Based on your code: yOnPage = canvas.margin().bottom(); (Y=10) - new ExpectedSegment(0.0, 5.0, 1), - - // Segment 3: Page 0 (Head) -> Remaining: 150 - 40 - 80 = 30. - new ExpectedSegment(0.0, 4.0, 0) - ) - ), - Arguments.of( - "Split across 3 Pages (Complex Loop)", - FactoryPresets.OBJ_TALL_5_6, // H=12 - FactoryPresets.CANVAS_SPLIT_5_M1, // H=5, T=2, B=2 - 2.0, // Start Y on Page 2 - 2, // pages - 2, // Expected Segments (40 + 80 + 30 = 150) - List.of( - // Segment 1: Page 2 (Tail) -> Space available: (100-10)-50 = 40 - new ExpectedSegment(2.0, 3.0, 1), - - // Segment 2: Page 1 (Full Page) -> Space available: 80. - // 110 remaining is > 80, so it takes full 80. - // Note: Y position depends on your logic: - // If logic uses margin.bottom() for next page start, check this value. - // Assuming standard top-down flow usually starts at margin.top(). - // Based on your code: yOnPage = canvas.margin().bottom(); (Y=10) - new ExpectedSegment(0.0, 3.0, 0) - - ) - ), - - // --- SCENARIO 3: Empty Optional --- - Arguments.of( - "Empty Context Returns Empty List", - null, - FactoryPresets.CANVAS_A4_STANDARD, - 0.0, - 1, - 0, - List.of() - ) - ); - } - - @MethodSource("provideBreakScenarios") - @ParameterizedTest(name = "{0}") - void testBreakCoordinate( - String name, - SizeData sizeData, - CanvasData canvasData, - double startY, - int pagesInput, - int expectedListSize, - List expectedSegments - ) { - // 1. Setup Mocks - Canvas canvas = FactoryClasses.canvasMock(canvasData); - when(renderingSystemMock.canvas()).thenReturn(canvas); - - Optional contextOpt; - if (sizeData == null) { - contextOpt = Optional.empty(); - } else { - // We only care about Y and Height for this test - var ctx = new RenderCoordinateContext( - 0, startY, sizeData.width(), sizeData.height(), - 0, 0, null, null - ); - contextOpt = Optional.of(ctx); - } - - // 2. Execution - List result = guidesRenderer.breakCoordinate(contextOpt, pagesInput); - - // 3. Verification - assertAll("Breaking Logic Verification", - () -> assertThat(result) - .as("Result size mismatch") - .hasSize(expectedListSize), - - () -> { - // Check up to the smaller size to avoid IndexOutOfBounds if sizes differ - int checks = Math.min(result.size(), expectedSegments.size()); - for (int i = 0; i < checks; i++) { - var actual = result.get(i); - var expected = expectedSegments.get(i); - final int index = i; - - assertAll("Segment " + i, - () -> assertThat(actual.y()) - .as("Y Position (Seg %d)", index) - .isEqualTo(expected.y()), - () -> assertThat(actual.height()) - .as("Height (Seg %d)", index) - .isEqualTo(expected.height()), - () -> assertThat(expected.page()) - .as("Page Index (Seg %d)", index) - .isEqualTo(actual.endPage()) - ); - } - } - ); - } -} diff --git a/qa/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java b/qa/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java deleted file mode 100644 index 345818d2c..000000000 --- a/qa/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.demcha.compose.font; -import com.demcha.compose.qa.RepoPaths; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.compose.engine.render.pdf.PdfFont; -import com.demcha.compose.engine.render.word.WordFont; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class FontLibraryIntegrationTest { - - @Test - void shouldExposeBundledGoogleFontsInEngineComposerHarness() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - assertThat(composer.availableFonts()) - .contains(FontName.HELVETICA, FontName.LATO, FontName.IBM_PLEX_SERIF, FontName.ZILLA_SLAB, - FontName.KANIT, FontName.VOLKHOV, FontName.ANDIKA, FontName.JETBRAINS_MONO); - } - - try (PDDocument document = new PDDocument()) { - FontLibrary fonts = PdfFontLibraryFactory.library(document); - assertThat(fonts.getFont(FontName.LATO, PdfFont.class)).isPresent(); - assertThat(fonts.getFont(FontName.LATO, WordFont.class)).isPresent(); - assertThat(fonts.getFont(FontName.KANIT, PdfFont.class)).isPresent(); - assertThat(fonts.getFont(FontName.KANIT, WordFont.class)).isPresent(); - assertThat(fonts.getFont(FontName.JETBRAINS_MONO, PdfFont.class)).isPresent(); - assertThat(fonts.getFont(FontName.JETBRAINS_MONO, WordFont.class)).isPresent(); - } - } - - @Test - void shouldRegisterCustomFontFamilyFromFilePaths() throws Exception { - FontName customFamily = FontName.of("Brand Sans"); - // The bundled fonts moved to the graph-compose-fonts module in v1.8.0. - // This test exercises file-based registration, so it reads the real TTFs - // from that module's resources (engine test cwd is the repo root). - Path fontsRoot = RepoPaths.resolve("fonts", "src", "main", "resources", "fonts", "google", "lato"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .registerFontFamily( - customFamily, - fontsRoot.resolve("Lato-Regular.ttf"), - fontsRoot.resolve("Lato-Bold.ttf"), - fontsRoot.resolve("Lato-Italic.ttf"), - fontsRoot.resolve("Lato-BoldItalic.ttf")) - .create()) { - - assertThat(composer.availableFonts()).contains(customFamily); - } - - FontFamilyDefinition customDefinition = FontFamilyDefinition.files(customFamily, fontsRoot.resolve("Lato-Regular.ttf")) - .boldPath(fontsRoot.resolve("Lato-Bold.ttf")) - .italicPath(fontsRoot.resolve("Lato-Italic.ttf")) - .boldItalicPath(fontsRoot.resolve("Lato-BoldItalic.ttf")) - .build(); - - try (PDDocument document = new PDDocument()) { - FontLibrary fonts = PdfFontLibraryFactory.library(document, List.of(customDefinition)); - assertThat(fonts.getFont(customFamily, PdfFont.class)).isPresent(); - assertThat(fonts.getFont(customFamily, WordFont.class)).isPresent(); - } - } - - @Test - void shouldMaterializeStandard14FontsWithoutBundledFontsArtifact() { - // The standard-14 families embed nothing and need no PDF document and no - // graph-compose-fonts jar — this is the baseline an engine-only consumer - // (no bundled fonts on the classpath) still gets. - FontLibrary standard = PdfFontLibraryFactory.standardLibrary(); - - assertThat(standard.getFont(FontName.HELVETICA, PdfFont.class)).isPresent(); - assertThat(standard.getFont(FontName.TIMES_ROMAN, PdfFont.class)).isPresent(); - assertThat(standard.getFont(FontName.COURIER, PdfFont.class)).isPresent(); - } - - @Test - void shouldGiveActionableErrorWhenBundledFontResourceMissing() { - // Simulates the bundled fonts being absent (graph-compose-fonts not on - // the classpath): a fonts/google/... resource that cannot be found must - // point the caller at the companion artifact, not just print a raw path. - FontFamilyDefinition missing = FontFamilyDefinition - .classpath(FontName.of("Missing Family"), "fonts/google/__missing__/Missing-Regular.ttf") - .build(); - FontFamilyDefinition.FontBinarySource regular = missing.fontSourceSet().orElseThrow().regular(); - - assertThatThrownBy(regular::openStream) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("graph-compose-fonts") - .hasMessageContaining("fonts/google/__missing__/Missing-Regular.ttf"); - } -} diff --git a/qa/src/test/java/com/demcha/testing/layout/LayoutSnapshotExtractorTest.java b/qa/src/test/java/com/demcha/testing/layout/LayoutSnapshotExtractorTest.java deleted file mode 100644 index 0201e536c..000000000 --- a/qa/src/test/java/com/demcha/testing/layout/LayoutSnapshotExtractorTest.java +++ /dev/null @@ -1,287 +0,0 @@ -package com.demcha.testing.layout; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.core.EntityName; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.Layer; -import com.demcha.compose.engine.components.layout.ParentComponent; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.compose.engine.debug.LayoutSnapshotExtractor; -import com.demcha.compose.engine.debug.LayoutNodeSnapshot; -import com.demcha.compose.engine.debug.LayoutSnapshot; -import com.demcha.compose.testing.layout.LayoutSnapshotJson; -import com.demcha.compose.engine.render.pdf.ecs.PdfCanvas; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -class LayoutSnapshotExtractorTest { - - @Test - void shouldUseStablePathsAndFallbackNamesForUnnamedSiblings() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(PDRectangle.A4) - .margin(11.1114f, 9.9999f, 8.8888f, 7.7777f) - .create()) { - - ComponentBuilder cb = composer.componentBuilder(); - Entity left = cb.rectangle() - .size(40, 20) - .build(); - Entity right = cb.rectangle() - .size(50, 20) - .margin(Margin.of(1.1114)) - .padding(Padding.of(2.2226)) - .build(); - - cb.vContainer(Align.left(4)) - .entityName("RootFlow") - .anchor(Anchor.topLeft()) - .addChild(left) - .addChild(right) - .build(); - - LayoutSnapshot snapshot = composer.layoutSnapshot(); - - assertThat(snapshot.canvas().margin().top()).isEqualTo(11.111); - assertThat(snapshot.nodes()) - .extracting(LayoutNodeSnapshot::path) - .containsExactly( - "RootFlow[0]", - "RootFlow[0]/Rectangle[0]", - "RootFlow[0]/Rectangle[1]"); - assertThat(snapshot.nodes().get(2).margin().top()).isEqualTo(1.111); - assertThat(snapshot.nodes().get(2).padding().top()).isEqualTo(2.223); - } - } - - @Test - void shouldPreserveDeterministicDepthLayerAndTraversalOrder() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - ComponentBuilder cb = composer.componentBuilder(); - - Entity first = cb.rectangle().size(24, 12).build(); - Entity second = cb.rectangle().size(24, 12).build(); - Entity row = cb.hContainer(Align.left(3)) - .entityName("ContentRow") - .anchor(Anchor.topLeft()) - .addChild(first) - .addChild(second) - .build(); - - cb.vContainer(Align.left(4)) - .entityName("Root") - .anchor(Anchor.topLeft()) - .addChild(row) - .build(); - - LayoutSnapshot snapshot = composer.layoutSnapshot(); - - assertThat(snapshot.nodes()) - .extracting(LayoutNodeSnapshot::path) - .containsExactly( - "Root[0]", - "Root[0]/ContentRow[0]", - "Root[0]/ContentRow[0]/Rectangle[0]", - "Root[0]/ContentRow[0]/Rectangle[1]"); - assertThat(snapshot.nodes()) - .extracting(LayoutNodeSnapshot::layer) - .containsExactly(1, 2, 3, 3); - assertThat(snapshot.nodes()) - .extracting(LayoutNodeSnapshot::depth) - .containsExactly(1, 2, 3, 3); - } - } - - @Test - void shouldProduceByteStableJsonAcrossEquivalentCompositions() throws Exception { - String firstJson = renderSnapshotJsonForStableDocument(); - String secondJson = renderSnapshotJsonForStableDocument(); - - assertThat(secondJson).isEqualTo(firstJson); - } - - @Test - void shouldKeepStableRootOrderWhenRootsShareSameCoordinates() throws Exception { - String firstJson = renderSnapshotJsonForEqualCoordinateRoots(); - String secondJson = renderSnapshotJsonForEqualCoordinateRoots(); - - assertThat(secondJson).isEqualTo(firstJson); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(PDRectangle.A4) - .margin(18, 18, 18, 18) - .create()) { - ComponentBuilder cb = composer.componentBuilder(); - cb.rectangle() - .size(80, 24) - .anchor(Anchor.topLeft()) - .build(); - cb.rectangle() - .size(80, 24) - .anchor(Anchor.topLeft()) - .build(); - - LayoutSnapshot snapshot = composer.layoutSnapshot(); - assertThat(snapshot.nodes()) - .extracting(LayoutNodeSnapshot::path) - .containsExactly("Rectangle[0]", "Rectangle[1]"); - } - } - - @Test - void shouldCaptureTotalPagesFromPlacementSpan() { - EntityManager entityManager = new EntityManager(false); - Entity entity = new Entity(); - entity.addComponent(new EntityName("PagedNode")); - entity.addComponent(new ContentSize(160, 640)); - entity.addComponent(new ComputedPosition(20, -300)); - entity.addComponent(new Placement(20, 300, 160, 640, 2, 0)); - entity.addComponent(new Layer(1)); - entityManager.putEntity(entity); - - Canvas canvas = new PdfCanvas(PDRectangle.A4, 0, 0); - canvas.addMargin(Margin.of(24)); - - LayoutSnapshot snapshot = LayoutSnapshotExtractor.extract(entityManager, canvas); - - assertThat(snapshot.totalPages()).isEqualTo(3); - assertThat(snapshot.nodes()).hasSize(1); - assertThat(snapshot.nodes().get(0).startPage()).isEqualTo(2); - assertThat(snapshot.nodes().get(0).endPage()).isEqualTo(0); - } - - @Test - void shouldUseCanonicalTraversalWhenParentComponentExistsButChildrenListIsMissingEntry() { - EntityManager entityManager = new EntityManager(false); - - Entity parent = new Entity(); - parent.addComponent(new EntityName("Root")); - parent.addComponent(new ContentSize(120, 80)); - parent.addComponent(new ComputedPosition(0, 0)); - parent.addComponent(new Placement(0, 0, 120, 80, 0, 0)); - parent.addComponent(new Layer(1)); - entityManager.putEntity(parent); - - Entity child = new Entity(); - child.addComponent(new EntityName("RecoveredChild")); - child.addComponent(new ParentComponent(parent.getUuid())); - child.addComponent(new ContentSize(40, 20)); - child.addComponent(new ComputedPosition(5, 5)); - child.addComponent(new Placement(5, 5, 40, 20, 0, 0)); - child.addComponent(new Layer(2)); - entityManager.putEntity(child); - - Canvas canvas = new PdfCanvas(PDRectangle.A4, 0, 0); - LayoutSnapshot snapshot = LayoutSnapshotExtractor.extract(entityManager, canvas); - - assertThat(snapshot.nodes()) - .extracting(LayoutNodeSnapshot::path) - .containsExactly("Root[0]", "Root[0]/RecoveredChild[0]"); - } - - @Test - void shouldIgnoreStaleChildrenListEntriesAndFollowCanonicalParentComponentTraversal() { - EntityManager entityManager = new EntityManager(false); - - Entity staleParent = new Entity(); - staleParent.addComponent(new EntityName("StaleParent")); - staleParent.addComponent(new ContentSize(120, 80)); - staleParent.addComponent(new ComputedPosition(0, 0)); - staleParent.addComponent(new Placement(0, 0, 120, 80, 0, 0)); - staleParent.addComponent(new Layer(1)); - entityManager.putEntity(staleParent); - - Entity canonicalParent = new Entity(); - canonicalParent.addComponent(new EntityName("CanonicalParent")); - canonicalParent.addComponent(new ContentSize(120, 80)); - canonicalParent.addComponent(new ComputedPosition(140, 0)); - canonicalParent.addComponent(new Placement(140, 0, 120, 80, 0, 0)); - canonicalParent.addComponent(new Layer(1)); - entityManager.putEntity(canonicalParent); - - Entity child = new Entity(); - child.addComponent(new EntityName("Child")); - child.addComponent(new ParentComponent(canonicalParent.getUuid())); - child.addComponent(new ContentSize(40, 20)); - child.addComponent(new ComputedPosition(145, 5)); - child.addComponent(new Placement(145, 5, 40, 20, 0, 0)); - child.addComponent(new Layer(2)); - entityManager.putEntity(child); - - staleParent.getChildren().add(child.getUuid()); - staleParent.getChildren().add(UUID.randomUUID()); - - Canvas canvas = new PdfCanvas(PDRectangle.A4, 0, 0); - LayoutSnapshot snapshot = LayoutSnapshotExtractor.extract(entityManager, canvas); - - assertThat(snapshot.nodes()) - .extracting(LayoutNodeSnapshot::path) - .containsExactly( - "StaleParent[0]", - "CanonicalParent[1]", - "CanonicalParent[1]/Child[0]"); - } - - private String renderSnapshotJsonForStableDocument() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(PDRectangle.A4) - .margin(18, 18, 18, 18) - .create()) { - ComponentBuilder cb = composer.componentBuilder(); - Entity heading = cb.text() - .entityName("StableHeading") - .textWithAutoSize("Stable JSON") - .textStyle(TextStyle.DEFAULT_STYLE) - .anchor(Anchor.topLeft()) - .build(); - Entity box = cb.rectangle() - .size(120, 32) - .margin(Margin.top(4)) - .build(); - - cb.vContainer(Align.left(8)) - .entityName("StableRoot") - .anchor(Anchor.topLeft()) - .addChild(heading) - .addChild(box) - .build(); - - return LayoutSnapshotJson.toJson(composer.layoutSnapshot()); - } - } - - private String renderSnapshotJsonForEqualCoordinateRoots() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf() - .pageSize(PDRectangle.A4) - .margin(18, 18, 18, 18) - .create()) { - ComponentBuilder cb = composer.componentBuilder(); - cb.rectangle() - .size(80, 24) - .anchor(Anchor.topLeft()) - .build(); - cb.rectangle() - .size(80, 24) - .anchor(Anchor.topLeft()) - .build(); - - return LayoutSnapshotJson.toJson(composer.layoutSnapshot()); - } - } -} diff --git a/qa/src/test/resources/layout-snapshots/integration/container_split.json b/qa/src/test/resources/layout-snapshots/integration/container_split.json deleted file mode 100644 index d7f702178..000000000 --- a/qa/src/test/resources/layout-snapshots/integration/container_split.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "formatVersion" : "1.0", - "canvas" : { - "pageWidth" : 595.276, - "pageHeight" : 841.89, - "innerWidth" : 495.276, - "innerHeight" : 741.89, - "margin" : { - "top" : 50.0, - "right" : 50.0, - "bottom" : 50.0, - "left" : 50.0 - } - }, - "totalPages" : 2, - "nodes" : [ { - "path" : "SplitContainer[0]", - "entityName" : "SplitContainer", - "entityKind" : "VContainer", - "parentPath" : null, - "childIndex" : 0, - "depth" : 1, - "layer" : 1, - "computedX" : 50.0, - "computedY" : 50.0, - "placementX" : 50.0, - "placementY" : 491.89, - "placementWidth" : 500.0, - "placementHeight" : 1310.0, - "startPage" : 1, - "endPage" : 0, - "contentWidth" : 500.0, - "contentHeight" : 1310.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "SplitContainer[0]/Rectangle[0]", - "entityName" : null, - "entityKind" : "Rectangle", - "parentPath" : "SplitContainer[0]", - "childIndex" : 0, - "depth" : 2, - "layer" : 2, - "computedX" : 50.0, - "computedY" : 360.0, - "placementX" : 50.0, - "placementY" : 191.89, - "placementWidth" : 500.0, - "placementHeight" : 600.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 500.0, - "contentHeight" : 600.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "SplitContainer[0]/Rectangle[1]", - "entityName" : null, - "entityKind" : "Rectangle", - "parentPath" : "SplitContainer[0]", - "childIndex" : 1, - "depth" : 2, - "layer" : 2, - "computedX" : 50.0, - "computedY" : 50.0, - "placementX" : 50.0, - "placementY" : 491.89, - "placementWidth" : 500.0, - "placementHeight" : 300.0, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 500.0, - "contentHeight" : 300.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - } ] -} diff --git a/qa/src/test/resources/layout-snapshots/integration/features/barcode_showcase_render.json b/qa/src/test/resources/layout-snapshots/integration/features/barcode_showcase_render.json deleted file mode 100644 index 26c68c1e6..000000000 --- a/qa/src/test/resources/layout-snapshots/integration/features/barcode_showcase_render.json +++ /dev/null @@ -1,557 +0,0 @@ -{ - "formatVersion" : "1.0", - "canvas" : { - "pageWidth" : 595.276, - "pageHeight" : 841.89, - "innerWidth" : 547.276, - "innerHeight" : 793.89, - "margin" : { - "top" : 24.0, - "right" : 24.0, - "bottom" : 24.0, - "left" : 24.0 - } - }, - "totalPages" : 1, - "nodes" : [ { - "path" : "BarcodeShowcaseRoot[0]", - "entityName" : "BarcodeShowcaseRoot", - "entityKind" : "VContainer", - "parentPath" : null, - "childIndex" : 0, - "depth" : 1, - "layer" : 1, - "computedX" : 24.0, - "computedY" : 198.952, - "placementX" : 24.0, - "placementY" : 198.952, - "placementWidth" : 547.276, - "placementHeight" : 618.937, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 547.276, - "contentHeight" : 618.937, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeShowcaseTitle[0]", - "entityName" : "BarcodeShowcaseTitle", - "entityKind" : "TextComponent", - "parentPath" : "BarcodeShowcaseRoot[0]", - "childIndex" : 0, - "depth" : 2, - "layer" : 2, - "computedX" : 24.0, - "computedY" : 797.54, - "placementX" : 24.0, - "placementY" : 797.54, - "placementWidth" : 286.088, - "placementHeight" : 20.35, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 286.088, - "contentHeight" : 20.35, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 4.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeShowcaseLead[1]", - "entityName" : "BarcodeShowcaseLead", - "entityKind" : "BlockText", - "parentPath" : "BarcodeShowcaseRoot[0]", - "childIndex" : 1, - "depth" : 2, - "layer" : 2, - "computedX" : 24.0, - "computedY" : 751.965, - "placementX" : 24.0, - "placementY" : 751.965, - "placementWidth" : 533.863, - "placementHeight" : 29.575, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 533.863, - "contentHeight" : 29.575, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeQrModule[2]", - "entityName" : "BarcodeQrModule", - "entityKind" : "Module", - "parentPath" : "BarcodeShowcaseRoot[0]", - "childIndex" : 2, - "depth" : 2, - "layer" : 2, - "computedX" : 24.0, - "computedY" : 539.365, - "placementX" : 24.0, - "placementY" : 539.365, - "placementWidth" : 547.276, - "placementHeight" : 198.6, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 547.276, - "contentHeight" : 198.6, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeQrModule[2]/BarcodeQrModuleHeading[0]", - "entityName" : "BarcodeQrModuleHeading", - "entityKind" : "TextComponent", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeQrModule[2]", - "childIndex" : 0, - "depth" : 3, - "layer" : 3, - "computedX" : 24.0, - "computedY" : 725.94, - "placementX" : 24.0, - "placementY" : 725.94, - "placementWidth" : 55.614, - "placementHeight" : 12.025, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 55.614, - "contentHeight" : 12.025, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeQrModule[2]/BarcodeQrModuleDivider[1]", - "entityName" : "BarcodeQrModuleDivider", - "entityKind" : "Line", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeQrModule[2]", - "childIndex" : 1, - "depth" : 3, - "layer" : 3, - "computedX" : 24.0, - "computedY" : 704.94, - "placementX" : 24.0, - "placementY" : 704.94, - "placementWidth" : 547.276, - "placementHeight" : 13.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 547.276, - "contentHeight" : 13.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 6.0, - "right" : 0.0, - "bottom" : 6.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeQrModule[2]/BarcodeQrSample[2]", - "entityName" : "BarcodeQrSample", - "entityKind" : "BarcodeComponent", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeQrModule[2]", - "childIndex" : 2, - "depth" : 3, - "layer" : 3, - "computedX" : 238.638, - "computedY" : 578.94, - "placementX" : 238.638, - "placementY" : 578.94, - "placementWidth" : 118.0, - "placementHeight" : 118.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 118.0, - "contentHeight" : 118.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeQrModule[2]/BarcodeQrModuleBody[3]", - "entityName" : "BarcodeQrModuleBody", - "entityKind" : "BlockText", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeQrModule[2]", - "childIndex" : 3, - "depth" : 3, - "layer" : 3, - "computedX" : 24.0, - "computedY" : 541.365, - "placementX" : 24.0, - "placementY" : 541.365, - "placementWidth" : 528.087, - "placementHeight" : 29.575, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 528.087, - "contentHeight" : 29.575, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeCode128Module[3]", - "entityName" : "BarcodeCode128Module", - "entityKind" : "Module", - "parentPath" : "BarcodeShowcaseRoot[0]", - "childIndex" : 3, - "depth" : 2, - "layer" : 2, - "computedX" : 24.0, - "computedY" : 387.552, - "placementX" : 24.0, - "placementY" : 387.552, - "placementWidth" : 547.276, - "placementHeight" : 139.812, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 547.276, - "contentHeight" : 139.812, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeCode128Module[3]/BarcodeCode128ModuleHeading[0]", - "entityName" : "BarcodeCode128ModuleHeading", - "entityKind" : "TextComponent", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeCode128Module[3]", - "childIndex" : 0, - "depth" : 3, - "layer" : 3, - "computedX" : 24.0, - "computedY" : 515.34, - "placementX" : 24.0, - "placementY" : 515.34, - "placementWidth" : 154.596, - "placementHeight" : 12.025, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 154.596, - "contentHeight" : 12.025, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeCode128Module[3]/BarcodeCode128ModuleDivider[1]", - "entityName" : "BarcodeCode128ModuleDivider", - "entityKind" : "Line", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeCode128Module[3]", - "childIndex" : 1, - "depth" : 3, - "layer" : 3, - "computedX" : 24.0, - "computedY" : 494.34, - "placementX" : 24.0, - "placementY" : 494.34, - "placementWidth" : 547.276, - "placementHeight" : 13.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 547.276, - "contentHeight" : 13.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 6.0, - "right" : 0.0, - "bottom" : 6.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeCode128Module[3]/BarcodeCode128Sample[2]", - "entityName" : "BarcodeCode128Sample", - "entityKind" : "BarcodeComponent", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeCode128Module[3]", - "childIndex" : 2, - "depth" : 3, - "layer" : 3, - "computedX" : 137.638, - "computedY" : 414.34, - "placementX" : 137.638, - "placementY" : 414.34, - "placementWidth" : 320.0, - "placementHeight" : 72.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 320.0, - "contentHeight" : 72.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeCode128Module[3]/BarcodeCode128ModuleBody[3]", - "entityName" : "BarcodeCode128ModuleBody", - "entityKind" : "BlockText", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeCode128Module[3]", - "childIndex" : 3, - "depth" : 3, - "layer" : 3, - "computedX" : 24.0, - "computedY" : 389.552, - "placementX" : 24.0, - "placementY" : 389.552, - "placementWidth" : 493.754, - "placementHeight" : 16.787, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 493.754, - "contentHeight" : 16.787, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeEan13Module[4]", - "entityName" : "BarcodeEan13Module", - "entityKind" : "Module", - "parentPath" : "BarcodeShowcaseRoot[0]", - "childIndex" : 4, - "depth" : 2, - "layer" : 2, - "computedX" : 24.0, - "computedY" : 198.952, - "placementX" : 24.0, - "placementY" : 198.952, - "placementWidth" : 547.276, - "placementHeight" : 176.6, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 547.276, - "contentHeight" : 176.6, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeEan13Module[4]/BarcodeEan13ModuleHeading[0]", - "entityName" : "BarcodeEan13ModuleHeading", - "entityKind" : "TextComponent", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeEan13Module[4]", - "childIndex" : 0, - "depth" : 3, - "layer" : 3, - "computedX" : 24.0, - "computedY" : 363.527, - "placementX" : 24.0, - "placementY" : 363.527, - "placementWidth" : 121.355, - "placementHeight" : 12.025, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 121.355, - "contentHeight" : 12.025, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeEan13Module[4]/BarcodeEan13ModuleDivider[1]", - "entityName" : "BarcodeEan13ModuleDivider", - "entityKind" : "Line", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeEan13Module[4]", - "childIndex" : 1, - "depth" : 3, - "layer" : 3, - "computedX" : 24.0, - "computedY" : 342.527, - "placementX" : 24.0, - "placementY" : 342.527, - "placementWidth" : 547.276, - "placementHeight" : 13.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 547.276, - "contentHeight" : 13.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 6.0, - "right" : 0.0, - "bottom" : 6.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeEan13Module[4]/BarcodeEan13Sample[2]", - "entityName" : "BarcodeEan13Sample", - "entityKind" : "BarcodeComponent", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeEan13Module[4]", - "childIndex" : 2, - "depth" : 3, - "layer" : 3, - "computedX" : 177.638, - "computedY" : 238.527, - "placementX" : 177.638, - "placementY" : 238.527, - "placementWidth" : 240.0, - "placementHeight" : 96.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 240.0, - "contentHeight" : 96.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "BarcodeShowcaseRoot[0]/BarcodeEan13Module[4]/BarcodeEan13ModuleBody[3]", - "entityName" : "BarcodeEan13ModuleBody", - "entityKind" : "BlockText", - "parentPath" : "BarcodeShowcaseRoot[0]/BarcodeEan13Module[4]", - "childIndex" : 3, - "depth" : 3, - "layer" : 3, - "computedX" : 24.0, - "computedY" : 200.952, - "placementX" : 24.0, - "placementY" : 200.952, - "placementWidth" : 540.703, - "placementHeight" : 29.575, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.703, - "contentHeight" : 29.575, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - } ] -} diff --git a/qa/src/test/resources/layout-snapshots/integration/features/document_features_paginated.json b/qa/src/test/resources/layout-snapshots/integration/features/document_features_paginated.json deleted file mode 100644 index 1a9aaa732..000000000 --- a/qa/src/test/resources/layout-snapshots/integration/features/document_features_paginated.json +++ /dev/null @@ -1,797 +0,0 @@ -{ - "formatVersion" : "1.0", - "canvas" : { - "pageWidth" : 595.276, - "pageHeight" : 841.89, - "innerWidth" : 539.276, - "innerHeight" : 771.89, - "margin" : { - "top" : 28.0, - "right" : 28.0, - "bottom" : 42.0, - "left" : 28.0 - } - }, - "totalPages" : 3, - "nodes" : [ { - "path" : "DocumentFeaturesRoot[0]", - "entityName" : "DocumentFeaturesRoot", - "entityKind" : "VContainer", - "parentPath" : null, - "childIndex" : 0, - "depth" : 1, - "layer" : 1, - "computedX" : 28.0, - "computedY" : -945.273, - "placementX" : 28.0, - "placementY" : 606.077, - "placementWidth" : 539.276, - "placementHeight" : 1891.592, - "startPage" : 2, - "endPage" : 0, - "contentWidth" : 539.276, - "contentHeight" : 1891.592, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/DocumentFeaturesTitle[0]", - "entityName" : "DocumentFeaturesTitle", - "entityKind" : "TextComponent", - "parentPath" : "DocumentFeaturesRoot[0]", - "childIndex" : 0, - "depth" : 2, - "layer" : 2, - "computedX" : 28.0, - "computedY" : 793.54, - "placementX" : 28.0, - "placementY" : 793.54, - "placementWidth" : 316.624, - "placementHeight" : 20.35, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 316.624, - "contentHeight" : 20.35, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 4.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/DocumentFeaturesLead[1]", - "entityName" : "DocumentFeaturesLead", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]", - "childIndex" : 1, - "depth" : 2, - "layer" : 2, - "computedX" : 28.0, - "computedY" : 735.177, - "placementX" : 28.0, - "placementY" : 735.177, - "placementWidth" : 537.055, - "placementHeight" : 42.362, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 537.055, - "contentHeight" : 42.362, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/OverviewModule[2]", - "entityName" : "OverviewModule", - "entityKind" : "Module", - "parentPath" : "DocumentFeaturesRoot[0]", - "childIndex" : 2, - "depth" : 2, - "layer" : 2, - "computedX" : 28.0, - "computedY" : 536.577, - "placementX" : 28.0, - "placementY" : 536.577, - "placementWidth" : 539.276, - "placementHeight" : 184.6, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 539.276, - "contentHeight" : 184.6, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/OverviewModule[2]/OverviewHeading[0]", - "entityName" : "OverviewHeading", - "entityKind" : "TextComponent", - "parentPath" : "DocumentFeaturesRoot[0]/OverviewModule[2]", - "childIndex" : 0, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : 709.152, - "placementX" : 28.0, - "placementY" : 709.152, - "placementWidth" : 57.811, - "placementHeight" : 12.025, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 57.811, - "contentHeight" : 12.025, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/OverviewModule[2]/OverviewBody[1]", - "entityName" : "OverviewBody", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/OverviewModule[2]", - "childIndex" : 1, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : 671.577, - "placementX" : 28.0, - "placementY" : 671.577, - "placementWidth" : 528.087, - "placementHeight" : 29.575, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 528.087, - "contentHeight" : 29.575, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/OverviewModule[2]/OverviewDivider[2]", - "entityName" : "OverviewDivider", - "entityKind" : "Line", - "parentPath" : "DocumentFeaturesRoot[0]/OverviewModule[2]", - "childIndex" : 2, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : 648.577, - "placementX" : 28.0, - "placementY" : 648.577, - "placementWidth" : 539.276, - "placementHeight" : 13.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 539.276, - "contentHeight" : 13.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 6.0, - "right" : 0.0, - "bottom" : 6.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/OverviewModule[2]/DocumentFeaturesQr[3]", - "entityName" : "DocumentFeaturesQr", - "entityKind" : "BarcodeComponent", - "parentPath" : "DocumentFeaturesRoot[0]/OverviewModule[2]", - "childIndex" : 3, - "depth" : 3, - "layer" : 3, - "computedX" : 245.638, - "computedY" : 536.577, - "placementX" : 245.638, - "placementY" : 536.577, - "placementWidth" : 104.0, - "placementHeight" : 104.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 104.0, - "contentHeight" : 104.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/ForcedPageBreakAfterOverview[3]", - "entityName" : "ForcedPageBreakAfterOverview", - "entityKind" : "PageBreakComponent", - "parentPath" : "DocumentFeaturesRoot[0]", - "childIndex" : 3, - "depth" : 2, - "layer" : 2, - "computedX" : 28.0, - "computedY" : 523.577, - "placementX" : 28.0, - "placementY" : 523.577, - "placementWidth" : 0.0, - "placementHeight" : 1.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 0.0, - "contentHeight" : 1.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/ManualPageBreakModule[4]", - "entityName" : "ManualPageBreakModule", - "entityKind" : "Module", - "parentPath" : "DocumentFeaturesRoot[0]", - "childIndex" : 4, - "depth" : 2, - "layer" : 2, - "computedX" : 28.0, - "computedY" : 395.202, - "placementX" : 28.0, - "placementY" : 395.202, - "placementWidth" : 539.276, - "placementHeight" : 116.375, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 539.276, - "contentHeight" : 116.375, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/ManualPageBreakModule[4]/ManualPageBreakHeading[0]", - "entityName" : "ManualPageBreakHeading", - "entityKind" : "TextComponent", - "parentPath" : "DocumentFeaturesRoot[0]/ManualPageBreakModule[4]", - "childIndex" : 0, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : 499.552, - "placementX" : 28.0, - "placementY" : 499.552, - "placementWidth" : 119.21, - "placementHeight" : 12.025, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 119.21, - "contentHeight" : 12.025, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/ManualPageBreakModule[4]/ManualPageBreakBody[1]", - "entityName" : "ManualPageBreakBody", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/ManualPageBreakModule[4]", - "childIndex" : 1, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : 461.977, - "placementX" : 28.0, - "placementY" : 461.977, - "placementWidth" : 536.552, - "placementHeight" : 29.575, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 536.552, - "contentHeight" : 29.575, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/ManualPageBreakModule[4]/ManualPageBreakDivider[2]", - "entityName" : "ManualPageBreakDivider", - "entityKind" : "Line", - "parentPath" : "DocumentFeaturesRoot[0]/ManualPageBreakModule[4]", - "childIndex" : 2, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : 434.777, - "placementX" : 28.0, - "placementY" : 434.777, - "placementWidth" : 539.276, - "placementHeight" : 17.2, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 539.276, - "contentHeight" : 17.2, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 8.0, - "right" : 0.0, - "bottom" : 8.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/ManualPageBreakModule[4]/ManualPageBreakFollowUp[3]", - "entityName" : "ManualPageBreakFollowUp", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/ManualPageBreakModule[4]", - "childIndex" : 3, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : 397.202, - "placementX" : 28.0, - "placementY" : 397.202, - "placementWidth" : 536.001, - "placementHeight" : 29.575, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 536.001, - "contentHeight" : 29.575, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "entityName" : "NarrativePaginationModule", - "entityKind" : "Module", - "parentPath" : "DocumentFeaturesRoot[0]", - "childIndex" : 5, - "depth" : 2, - "layer" : 2, - "computedX" : 28.0, - "computedY" : -945.273, - "placementX" : 28.0, - "placementY" : 606.077, - "placementWidth" : 539.276, - "placementHeight" : 1460.905, - "startPage" : 2, - "endPage" : 0, - "contentWidth" : 539.276, - "contentHeight" : 1460.905, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]/NarrativePaginationHeading[0]", - "entityName" : "NarrativePaginationHeading", - "entityKind" : "TextComponent", - "parentPath" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "childIndex" : 0, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : 371.177, - "placementX" : 28.0, - "placementY" : 371.177, - "placementWidth" : 114.855, - "placementHeight" : 12.025, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 114.855, - "contentHeight" : 12.025, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]/NarrativePaginationLead[1]", - "entityName" : "NarrativePaginationLead", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "childIndex" : 1, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : 333.602, - "placementX" : 28.0, - "placementY" : 333.602, - "placementWidth" : 515.994, - "placementHeight" : 29.575, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 515.994, - "contentHeight" : 29.575, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]/NarrativeParagraph1[2]", - "entityName" : "NarrativeParagraph1", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "childIndex" : 2, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : 191.727, - "placementX" : 28.0, - "placementY" : 191.727, - "placementWidth" : 539.193, - "placementHeight" : 131.875, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 539.193, - "contentHeight" : 131.875, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]/NarrativeParagraph2[3]", - "entityName" : "NarrativeParagraph2", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "childIndex" : 3, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : 49.852, - "placementX" : 28.0, - "placementY" : 49.852, - "placementWidth" : 539.193, - "placementHeight" : 131.875, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 539.193, - "contentHeight" : 131.875, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]/NarrativeParagraph3[4]", - "entityName" : "NarrativeParagraph3", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "childIndex" : 4, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : -92.023, - "placementX" : 28.0, - "placementY" : 686.015, - "placementWidth" : 539.193, - "placementHeight" : 195.727, - "startPage" : 1, - "endPage" : 0, - "contentWidth" : 539.193, - "contentHeight" : 195.727, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]/NarrativeParagraph4[5]", - "entityName" : "NarrativeParagraph4", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "childIndex" : 5, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : -233.898, - "placementX" : 28.0, - "placementY" : 544.14, - "placementWidth" : 539.193, - "placementHeight" : 131.875, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 539.193, - "contentHeight" : 131.875, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]/NarrativeParagraph5[6]", - "entityName" : "NarrativeParagraph5", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "childIndex" : 6, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : -375.773, - "placementX" : 28.0, - "placementY" : 402.265, - "placementWidth" : 539.193, - "placementHeight" : 131.875, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 539.193, - "contentHeight" : 131.875, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]/NarrativeParagraph6[7]", - "entityName" : "NarrativeParagraph6", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "childIndex" : 7, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : -517.648, - "placementX" : 28.0, - "placementY" : 260.39, - "placementWidth" : 539.193, - "placementHeight" : 131.875, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 539.193, - "contentHeight" : 131.875, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]/NarrativeParagraph7[8]", - "entityName" : "NarrativeParagraph7", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "childIndex" : 8, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : -659.523, - "placementX" : 28.0, - "placementY" : 118.515, - "placementWidth" : 539.193, - "placementHeight" : 131.875, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 539.193, - "contentHeight" : 131.875, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]/NarrativeParagraph8[9]", - "entityName" : "NarrativeParagraph8", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "childIndex" : 9, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : -801.398, - "placementX" : 28.0, - "placementY" : 749.952, - "placementWidth" : 539.193, - "placementHeight" : 200.452, - "startPage" : 2, - "endPage" : 1, - "contentWidth" : 539.193, - "contentHeight" : 200.452, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]/NarrativeParagraph9[10]", - "entityName" : "NarrativeParagraph9", - "entityKind" : "BlockText", - "parentPath" : "DocumentFeaturesRoot[0]/NarrativePaginationModule[5]", - "childIndex" : 10, - "depth" : 3, - "layer" : 3, - "computedX" : 28.0, - "computedY" : -943.273, - "placementX" : 28.0, - "placementY" : 608.077, - "placementWidth" : 539.193, - "placementHeight" : 131.875, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 539.193, - "contentHeight" : 131.875, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 2.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - } ] -} diff --git a/qa/src/test/resources/layout-snapshots/integration/massive_text.json b/qa/src/test/resources/layout-snapshots/integration/massive_text.json deleted file mode 100644 index a3d8763c9..000000000 --- a/qa/src/test/resources/layout-snapshots/integration/massive_text.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "formatVersion" : "1.0", - "canvas" : { - "pageWidth" : 595.276, - "pageHeight" : 841.89, - "innerWidth" : 495.276, - "innerHeight" : 741.89, - "margin" : { - "top" : 50.0, - "right" : 50.0, - "bottom" : 50.0, - "left" : 50.0 - } - }, - "totalPages" : 3, - "nodes" : [ { - "path" : "VContainer[0]", - "entityName" : null, - "entityKind" : "VContainer", - "parentPath" : null, - "childIndex" : 0, - "depth" : 1, - "layer" : 1, - "computedX" : 50.0, - "computedY" : -955.027, - "placementX" : 50.0, - "placementY" : 526.83, - "placementWidth" : 495.276, - "placementHeight" : 1948.84, - "startPage" : 2, - "endPage" : 0, - "contentWidth" : 495.276, - "contentHeight" : 1948.84, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "VContainer[0]/Module[0]", - "entityName" : null, - "entityKind" : "Module", - "parentPath" : "VContainer[0]", - "childIndex" : 0, - "depth" : 2, - "layer" : 2, - "computedX" : 50.0, - "computedY" : -955.027, - "placementX" : 50.0, - "placementY" : 526.83, - "placementWidth" : 495.276, - "placementHeight" : 1948.84, - "startPage" : 2, - "endPage" : 0, - "contentWidth" : 495.276, - "contentHeight" : 1948.84, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "VContainer[0]/Module[0]/Title_MassiveText[0]", - "entityName" : "Title_MassiveText", - "entityKind" : "TextComponent", - "parentPath" : "VContainer[0]/Module[0]", - "childIndex" : 0, - "depth" : 3, - "layer" : 3, - "computedX" : 55.0, - "computedY" : 773.213, - "placementX" : 55.0, - "placementY" : 773.213, - "placementWidth" : 114.84, - "placementHeight" : 13.676, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 114.84, - "contentHeight" : 13.676, - "margin" : { - "top" : 5.0, - "right" : 5.0, - "bottom" : 5.0, - "left" : 5.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "VContainer[0]/Module[0]/BlockText[1]", - "entityName" : null, - "entityKind" : "BlockText", - "parentPath" : "VContainer[0]/Module[0]", - "childIndex" : 1, - "depth" : 3, - "layer" : 3, - "computedX" : 51.638, - "computedY" : -955.027, - "placementX" : 51.638, - "placementY" : 526.83, - "placementWidth" : 492.0, - "placementHeight" : 1920.163, - "startPage" : 2, - "endPage" : 0, - "contentWidth" : 492.0, - "contentHeight" : 1920.163, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 5.0, - "bottom" : 0.0, - "left" : 25.0 - } - } ] -} diff --git a/qa/src/test/resources/layout-snapshots/integration/repository_showcase_render.json b/qa/src/test/resources/layout-snapshots/integration/repository_showcase_render.json deleted file mode 100644 index 64529e0ba..000000000 --- a/qa/src/test/resources/layout-snapshots/integration/repository_showcase_render.json +++ /dev/null @@ -1,647 +0,0 @@ -{ - "formatVersion" : "1.0", - "canvas" : { - "pageWidth" : 595.276, - "pageHeight" : 841.89, - "innerWidth" : 563.276, - "innerHeight" : 809.89, - "margin" : { - "top" : 16.0, - "right" : 16.0, - "bottom" : 16.0, - "left" : 16.0 - } - }, - "totalPages" : 1, - "nodes" : [ { - "path" : "RepositoryShowcaseRoot[0]", - "entityName" : "RepositoryShowcaseRoot", - "entityKind" : "VContainer", - "parentPath" : null, - "childIndex" : 0, - "depth" : 1, - "layer" : 1, - "computedX" : 19.0, - "computedY" : 196.502, - "placementX" : 19.0, - "placementY" : 196.502, - "placementWidth" : 557.276, - "placementHeight" : 625.387, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 557.276, - "contentHeight" : 625.387, - "margin" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/TextComponent[0]", - "entityName" : null, - "entityKind" : "TextComponent", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 0, - "depth" : 2, - "layer" : 2, - "computedX" : 162.833, - "computedY" : 775.69, - "placementX" : 162.833, - "placementY" : 775.69, - "placementWidth" : 269.61, - "placementHeight" : 46.2, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 269.61, - "contentHeight" : 46.2, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/ImageComponent[1]", - "entityName" : null, - "entityKind" : "ImageComponent", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 1, - "depth" : 2, - "layer" : 2, - "computedX" : 123.224, - "computedY" : 608.69, - "placementX" : 123.224, - "placementY" : 608.69, - "placementWidth" : 348.828, - "placementHeight" : 158.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 348.828, - "contentHeight" : 158.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/TextComponent[2]", - "entityName" : null, - "entityKind" : "TextComponent", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 2, - "depth" : 2, - "layer" : 2, - "computedX" : 115.311, - "computedY" : 584.74, - "placementX" : 115.311, - "placementY" : 584.74, - "placementWidth" : 364.654, - "placementHeight" : 14.95, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 364.654, - "contentHeight" : 14.95, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/BlockText[3]", - "entityName" : null, - "entityKind" : "BlockText", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 3, - "depth" : 2, - "layer" : 2, - "computedX" : 42.387, - "computedY" : 540.822, - "placementX" : 42.387, - "placementY" : 540.822, - "placementWidth" : 510.501, - "placementHeight" : 34.917, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 510.501, - "contentHeight" : 34.917, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/Line[4]", - "entityName" : null, - "entityKind" : "Line", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 4, - "depth" : 2, - "layer" : 2, - "computedX" : 44.164, - "computedY" : 521.822, - "placementX" : 44.164, - "placementY" : 521.822, - "placementWidth" : 506.948, - "placementHeight" : 10.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 506.948, - "contentHeight" : 10.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 2.0, - "right" : 2.0, - "bottom" : 2.0, - "left" : 2.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/TextComponent[5]", - "entityName" : null, - "entityKind" : "TextComponent", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 5, - "depth" : 2, - "layer" : 2, - "computedX" : 19.0, - "computedY" : 495.742, - "placementX" : 19.0, - "placementY" : 495.742, - "placementWidth" : 149.548, - "placementHeight" : 17.08, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 149.548, - "contentHeight" : 17.08, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/BlockText[6]", - "entityName" : null, - "entityKind" : "BlockText", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 6, - "depth" : 2, - "layer" : 2, - "computedX" : 19.0, - "computedY" : 450.657, - "placementX" : 19.0, - "placementY" : 450.657, - "placementWidth" : 356.984, - "placementHeight" : 36.085, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 356.984, - "contentHeight" : 36.085, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/TextComponent[7]", - "entityName" : null, - "entityKind" : "TextComponent", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 7, - "depth" : 2, - "layer" : 2, - "computedX" : 19.0, - "computedY" : 424.577, - "placementX" : 19.0, - "placementY" : 424.577, - "placementWidth" : 159.698, - "placementHeight" : 17.08, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 159.698, - "contentHeight" : 17.08, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/BlockText[8]", - "entityName" : null, - "entityKind" : "BlockText", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 8, - "depth" : 2, - "layer" : 2, - "computedX" : 19.0, - "computedY" : 394.927, - "placementX" : 19.0, - "placementY" : 394.927, - "placementWidth" : 520.704, - "placementHeight" : 20.65, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 520.704, - "contentHeight" : 20.65, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/RepositoryCapabilitiesTable[9]", - "entityName" : "RepositoryCapabilitiesTable", - "entityKind" : "VContainer", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 9, - "depth" : 2, - "layer" : 2, - "computedX" : 19.0, - "computedY" : 309.047, - "placementX" : 19.0, - "placementY" : 309.047, - "placementWidth" : 557.276, - "placementHeight" : 76.88, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 557.276, - "contentHeight" : 76.88, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/RepositoryCapabilitiesTable[9]/RepositoryCapabilitiesTable__row_0[0]", - "entityName" : "RepositoryCapabilitiesTable__row_0", - "entityKind" : "TableRow", - "parentPath" : "RepositoryShowcaseRoot[0]/RepositoryCapabilitiesTable[9]", - "childIndex" : 0, - "depth" : 3, - "layer" : 3, - "computedX" : 19.0, - "computedY" : 363.467, - "placementX" : 19.0, - "placementY" : 363.467, - "placementWidth" : 557.276, - "placementHeight" : 22.46, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 557.276, - "contentHeight" : 22.46, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/RepositoryCapabilitiesTable[9]/RepositoryCapabilitiesTable__row_1[1]", - "entityName" : "RepositoryCapabilitiesTable__row_1", - "entityKind" : "TableRow", - "parentPath" : "RepositoryShowcaseRoot[0]/RepositoryCapabilitiesTable[9]", - "childIndex" : 1, - "depth" : 3, - "layer" : 3, - "computedX" : 19.0, - "computedY" : 345.327, - "placementX" : 19.0, - "placementY" : 345.327, - "placementWidth" : 557.276, - "placementHeight" : 18.14, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 557.276, - "contentHeight" : 18.14, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/RepositoryCapabilitiesTable[9]/RepositoryCapabilitiesTable__row_2[2]", - "entityName" : "RepositoryCapabilitiesTable__row_2", - "entityKind" : "TableRow", - "parentPath" : "RepositoryShowcaseRoot[0]/RepositoryCapabilitiesTable[9]", - "childIndex" : 2, - "depth" : 3, - "layer" : 3, - "computedX" : 19.0, - "computedY" : 327.187, - "placementX" : 19.0, - "placementY" : 327.187, - "placementWidth" : 557.276, - "placementHeight" : 18.14, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 557.276, - "contentHeight" : 18.14, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/RepositoryCapabilitiesTable[9]/RepositoryCapabilitiesTable__row_3[3]", - "entityName" : "RepositoryCapabilitiesTable__row_3", - "entityKind" : "TableRow", - "parentPath" : "RepositoryShowcaseRoot[0]/RepositoryCapabilitiesTable[9]", - "childIndex" : 3, - "depth" : 3, - "layer" : 3, - "computedX" : 19.0, - "computedY" : 309.047, - "placementX" : 19.0, - "placementY" : 309.047, - "placementWidth" : 557.276, - "placementHeight" : 18.14, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 557.276, - "contentHeight" : 18.14, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/TextComponent[10]", - "entityName" : null, - "entityKind" : "TextComponent", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 10, - "depth" : 2, - "layer" : 2, - "computedX" : 19.0, - "computedY" : 282.967, - "placementX" : 19.0, - "placementY" : 282.967, - "placementWidth" : 123.293, - "placementHeight" : 17.08, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 123.293, - "contentHeight" : 17.08, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/RepositoryShowcaseLineGallery[11]", - "entityName" : "RepositoryShowcaseLineGallery", - "entityKind" : "HContainer", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 11, - "depth" : 2, - "layer" : 2, - "computedX" : 160.638, - "computedY" : 225.967, - "placementX" : 160.638, - "placementY" : 225.967, - "placementWidth" : 274.0, - "placementHeight" : 46.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 274.0, - "contentHeight" : 46.0, - "margin" : { - "top" : 2.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/RepositoryShowcaseLineGallery[11]/Line[0]", - "entityName" : null, - "entityKind" : "Line", - "parentPath" : "RepositoryShowcaseRoot[0]/RepositoryShowcaseLineGallery[11]", - "childIndex" : 0, - "depth" : 3, - "layer" : 3, - "computedX" : 314.638, - "computedY" : 241.967, - "placementX" : 314.638, - "placementY" : 241.967, - "placementWidth" : 120.0, - "placementHeight" : 14.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 120.0, - "contentHeight" : 14.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 3.0, - "right" : 3.0, - "bottom" : 3.0, - "left" : 3.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/RepositoryShowcaseLineGallery[11]/Line[1]", - "entityName" : null, - "entityKind" : "Line", - "parentPath" : "RepositoryShowcaseRoot[0]/RepositoryShowcaseLineGallery[11]", - "childIndex" : 1, - "depth" : 3, - "layer" : 3, - "computedX" : 282.638, - "computedY" : 225.967, - "placementX" : 282.638, - "placementY" : 225.967, - "placementWidth" : 14.0, - "placementHeight" : 46.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 14.0, - "contentHeight" : 46.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 3.0, - "right" : 3.0, - "bottom" : 3.0, - "left" : 3.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/RepositoryShowcaseLineGallery[11]/Line[2]", - "entityName" : null, - "entityKind" : "Line", - "parentPath" : "RepositoryShowcaseRoot[0]/RepositoryShowcaseLineGallery[11]", - "childIndex" : 2, - "depth" : 3, - "layer" : 3, - "computedX" : 160.638, - "computedY" : 229.967, - "placementX" : 160.638, - "placementY" : 229.967, - "placementWidth" : 104.0, - "placementHeight" : 38.0, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 104.0, - "contentHeight" : 38.0, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 4.0, - "right" : 4.0, - "bottom" : 4.0, - "left" : 4.0 - } - }, { - "path" : "RepositoryShowcaseRoot[0]/BlockText[12]", - "entityName" : null, - "entityKind" : "BlockText", - "parentPath" : "RepositoryShowcaseRoot[0]", - "childIndex" : 12, - "depth" : 2, - "layer" : 2, - "computedX" : 19.0, - "computedY" : 196.502, - "placementX" : 19.0, - "placementY" : 196.502, - "placementWidth" : 532.781, - "placementHeight" : 20.465, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 532.781, - "contentHeight" : 20.465, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - } ] -} diff --git a/qa/src/test/resources/layout-snapshots/integration/table_pagination_test.json b/qa/src/test/resources/layout-snapshots/integration/table_pagination_test.json deleted file mode 100644 index a58ab4a1a..000000000 --- a/qa/src/test/resources/layout-snapshots/integration/table_pagination_test.json +++ /dev/null @@ -1,2927 +0,0 @@ -{ - "formatVersion" : "1.0", - "canvas" : { - "pageWidth" : 595.276, - "pageHeight" : 841.89, - "innerWidth" : 555.276, - "innerHeight" : 801.89, - "margin" : { - "top" : 20.0, - "right" : 20.0, - "bottom" : 20.0, - "left" : 20.0 - } - }, - "totalPages" : 5, - "nodes" : [ { - "path" : "PaginationTable[0]", - "entityName" : "PaginationTable", - "entityKind" : "VContainer", - "parentPath" : null, - "childIndex" : 0, - "depth" : 1, - "layer" : 1, - "computedX" : 27.638, - "computedY" : -2429.56, - "placementX" : 27.638, - "placementY" : 686.29, - "placementWidth" : 540.0, - "placementHeight" : 3493.159, - "startPage" : 4, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 3493.159, - "margin" : { - "top" : 10.0, - "right" : 10.0, - "bottom" : 10.0, - "left" : 10.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_0[0]", - "entityName" : "PaginationTable__row_0", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 0, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 790.94, - "placementX" : 27.638, - "placementY" : 790.94, - "placementWidth" : 540.0, - "placementHeight" : 20.95, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 20.95, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_1[1]", - "entityName" : "PaginationTable__row_1", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 1, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 757.04, - "placementX" : 27.638, - "placementY" : 757.04, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_2[2]", - "entityName" : "PaginationTable__row_2", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 2, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 723.14, - "placementX" : 27.638, - "placementY" : 723.14, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_3[3]", - "entityName" : "PaginationTable__row_3", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 3, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 689.24, - "placementX" : 27.638, - "placementY" : 689.24, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_4[4]", - "entityName" : "PaginationTable__row_4", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 4, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 655.34, - "placementX" : 27.638, - "placementY" : 655.34, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_5[5]", - "entityName" : "PaginationTable__row_5", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 5, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 621.44, - "placementX" : 27.638, - "placementY" : 621.44, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_6[6]", - "entityName" : "PaginationTable__row_6", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 6, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 587.54, - "placementX" : 27.638, - "placementY" : 587.54, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_7[7]", - "entityName" : "PaginationTable__row_7", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 7, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 553.64, - "placementX" : 27.638, - "placementY" : 553.64, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_8[8]", - "entityName" : "PaginationTable__row_8", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 8, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 519.74, - "placementX" : 27.638, - "placementY" : 519.74, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_9[9]", - "entityName" : "PaginationTable__row_9", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 9, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 485.84, - "placementX" : 27.638, - "placementY" : 485.84, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_10[10]", - "entityName" : "PaginationTable__row_10", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 10, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 451.94, - "placementX" : 27.638, - "placementY" : 451.94, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_11[11]", - "entityName" : "PaginationTable__row_11", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 11, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 418.04, - "placementX" : 27.638, - "placementY" : 418.04, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_12[12]", - "entityName" : "PaginationTable__row_12", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 12, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 384.14, - "placementX" : 27.638, - "placementY" : 384.14, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_13[13]", - "entityName" : "PaginationTable__row_13", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 13, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 350.24, - "placementX" : 27.638, - "placementY" : 350.24, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_14[14]", - "entityName" : "PaginationTable__row_14", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 14, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 316.34, - "placementX" : 27.638, - "placementY" : 316.34, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_15[15]", - "entityName" : "PaginationTable__row_15", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 15, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 282.44, - "placementX" : 27.638, - "placementY" : 282.44, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_16[16]", - "entityName" : "PaginationTable__row_16", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 16, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 248.54, - "placementX" : 27.638, - "placementY" : 248.54, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_17[17]", - "entityName" : "PaginationTable__row_17", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 17, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 214.64, - "placementX" : 27.638, - "placementY" : 214.64, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_18[18]", - "entityName" : "PaginationTable__row_18", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 18, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 180.74, - "placementX" : 27.638, - "placementY" : 180.74, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_19[19]", - "entityName" : "PaginationTable__row_19", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 19, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 146.84, - "placementX" : 27.638, - "placementY" : 146.84, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_20[20]", - "entityName" : "PaginationTable__row_20", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 20, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 112.94, - "placementX" : 27.638, - "placementY" : 112.94, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_21[21]", - "entityName" : "PaginationTable__row_21", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 21, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 79.04, - "placementX" : 27.638, - "placementY" : 79.04, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_22[22]", - "entityName" : "PaginationTable__row_22", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 22, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 45.14, - "placementX" : 27.638, - "placementY" : 45.14, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 0, - "endPage" : 0, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_23[23]", - "entityName" : "PaginationTable__row_23", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 23, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : 11.24, - "placementX" : 27.638, - "placementY" : 787.99, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_24[24]", - "entityName" : "PaginationTable__row_24", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 24, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -22.66, - "placementX" : 27.638, - "placementY" : 754.09, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_25[25]", - "entityName" : "PaginationTable__row_25", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 25, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -56.56, - "placementX" : 27.638, - "placementY" : 720.19, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_26[26]", - "entityName" : "PaginationTable__row_26", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 26, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -90.46, - "placementX" : 27.638, - "placementY" : 686.29, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_27[27]", - "entityName" : "PaginationTable__row_27", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 27, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -124.36, - "placementX" : 27.638, - "placementY" : 652.39, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_28[28]", - "entityName" : "PaginationTable__row_28", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 28, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -158.26, - "placementX" : 27.638, - "placementY" : 618.49, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_29[29]", - "entityName" : "PaginationTable__row_29", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 29, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -192.16, - "placementX" : 27.638, - "placementY" : 584.59, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_30[30]", - "entityName" : "PaginationTable__row_30", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 30, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -226.06, - "placementX" : 27.638, - "placementY" : 550.69, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_31[31]", - "entityName" : "PaginationTable__row_31", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 31, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -259.96, - "placementX" : 27.638, - "placementY" : 516.79, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_32[32]", - "entityName" : "PaginationTable__row_32", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 32, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -293.86, - "placementX" : 27.638, - "placementY" : 482.89, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_33[33]", - "entityName" : "PaginationTable__row_33", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 33, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -327.76, - "placementX" : 27.638, - "placementY" : 448.99, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_34[34]", - "entityName" : "PaginationTable__row_34", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 34, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -361.66, - "placementX" : 27.638, - "placementY" : 415.09, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_35[35]", - "entityName" : "PaginationTable__row_35", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 35, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -395.56, - "placementX" : 27.638, - "placementY" : 381.19, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_36[36]", - "entityName" : "PaginationTable__row_36", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 36, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -429.46, - "placementX" : 27.638, - "placementY" : 347.29, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_37[37]", - "entityName" : "PaginationTable__row_37", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 37, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -463.36, - "placementX" : 27.638, - "placementY" : 313.39, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_38[38]", - "entityName" : "PaginationTable__row_38", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 38, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -497.26, - "placementX" : 27.638, - "placementY" : 279.49, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_39[39]", - "entityName" : "PaginationTable__row_39", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 39, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -531.16, - "placementX" : 27.638, - "placementY" : 245.59, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_40[40]", - "entityName" : "PaginationTable__row_40", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 40, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -565.06, - "placementX" : 27.638, - "placementY" : 211.69, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_41[41]", - "entityName" : "PaginationTable__row_41", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 41, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -598.96, - "placementX" : 27.638, - "placementY" : 177.79, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_42[42]", - "entityName" : "PaginationTable__row_42", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 42, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -632.86, - "placementX" : 27.638, - "placementY" : 143.89, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_43[43]", - "entityName" : "PaginationTable__row_43", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 43, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -666.76, - "placementX" : 27.638, - "placementY" : 109.99, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_44[44]", - "entityName" : "PaginationTable__row_44", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 44, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -700.66, - "placementX" : 27.638, - "placementY" : 76.09, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_45[45]", - "entityName" : "PaginationTable__row_45", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 45, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -734.56, - "placementX" : 27.638, - "placementY" : 42.19, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 1, - "endPage" : 1, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_46[46]", - "entityName" : "PaginationTable__row_46", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 46, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -768.46, - "placementX" : 27.638, - "placementY" : 787.99, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_47[47]", - "entityName" : "PaginationTable__row_47", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 47, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -802.36, - "placementX" : 27.638, - "placementY" : 754.09, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_48[48]", - "entityName" : "PaginationTable__row_48", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 48, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -836.26, - "placementX" : 27.638, - "placementY" : 720.19, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_49[49]", - "entityName" : "PaginationTable__row_49", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 49, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -870.16, - "placementX" : 27.638, - "placementY" : 686.29, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_50[50]", - "entityName" : "PaginationTable__row_50", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 50, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -904.06, - "placementX" : 27.638, - "placementY" : 652.39, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_51[51]", - "entityName" : "PaginationTable__row_51", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 51, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -937.96, - "placementX" : 27.638, - "placementY" : 618.49, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_52[52]", - "entityName" : "PaginationTable__row_52", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 52, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -971.86, - "placementX" : 27.638, - "placementY" : 584.59, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_53[53]", - "entityName" : "PaginationTable__row_53", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 53, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1005.76, - "placementX" : 27.638, - "placementY" : 550.69, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_54[54]", - "entityName" : "PaginationTable__row_54", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 54, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1039.66, - "placementX" : 27.638, - "placementY" : 516.79, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_55[55]", - "entityName" : "PaginationTable__row_55", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 55, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1073.56, - "placementX" : 27.638, - "placementY" : 482.89, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_56[56]", - "entityName" : "PaginationTable__row_56", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 56, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1107.46, - "placementX" : 27.638, - "placementY" : 448.99, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_57[57]", - "entityName" : "PaginationTable__row_57", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 57, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1141.36, - "placementX" : 27.638, - "placementY" : 415.09, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_58[58]", - "entityName" : "PaginationTable__row_58", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 58, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1175.26, - "placementX" : 27.638, - "placementY" : 381.19, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_59[59]", - "entityName" : "PaginationTable__row_59", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 59, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1209.16, - "placementX" : 27.638, - "placementY" : 347.29, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_60[60]", - "entityName" : "PaginationTable__row_60", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 60, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1243.06, - "placementX" : 27.638, - "placementY" : 313.39, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_61[61]", - "entityName" : "PaginationTable__row_61", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 61, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1276.96, - "placementX" : 27.638, - "placementY" : 279.49, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_62[62]", - "entityName" : "PaginationTable__row_62", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 62, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1310.86, - "placementX" : 27.638, - "placementY" : 245.59, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_63[63]", - "entityName" : "PaginationTable__row_63", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 63, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1344.76, - "placementX" : 27.638, - "placementY" : 211.69, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_64[64]", - "entityName" : "PaginationTable__row_64", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 64, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1378.66, - "placementX" : 27.638, - "placementY" : 177.79, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_65[65]", - "entityName" : "PaginationTable__row_65", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 65, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1412.56, - "placementX" : 27.638, - "placementY" : 143.89, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_66[66]", - "entityName" : "PaginationTable__row_66", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 66, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1446.46, - "placementX" : 27.638, - "placementY" : 109.99, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_67[67]", - "entityName" : "PaginationTable__row_67", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 67, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1480.36, - "placementX" : 27.638, - "placementY" : 76.09, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_68[68]", - "entityName" : "PaginationTable__row_68", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 68, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1514.26, - "placementX" : 27.638, - "placementY" : 42.19, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 2, - "endPage" : 2, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_69[69]", - "entityName" : "PaginationTable__row_69", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 69, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1548.16, - "placementX" : 27.638, - "placementY" : 787.99, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_70[70]", - "entityName" : "PaginationTable__row_70", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 70, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1582.06, - "placementX" : 27.638, - "placementY" : 754.09, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_71[71]", - "entityName" : "PaginationTable__row_71", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 71, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1615.96, - "placementX" : 27.638, - "placementY" : 720.19, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_72[72]", - "entityName" : "PaginationTable__row_72", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 72, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1649.86, - "placementX" : 27.638, - "placementY" : 686.29, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_73[73]", - "entityName" : "PaginationTable__row_73", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 73, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1683.76, - "placementX" : 27.638, - "placementY" : 652.39, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_74[74]", - "entityName" : "PaginationTable__row_74", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 74, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1717.66, - "placementX" : 27.638, - "placementY" : 618.49, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_75[75]", - "entityName" : "PaginationTable__row_75", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 75, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1751.56, - "placementX" : 27.638, - "placementY" : 584.59, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_76[76]", - "entityName" : "PaginationTable__row_76", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 76, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1785.46, - "placementX" : 27.638, - "placementY" : 550.69, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_77[77]", - "entityName" : "PaginationTable__row_77", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 77, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1819.36, - "placementX" : 27.638, - "placementY" : 516.79, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_78[78]", - "entityName" : "PaginationTable__row_78", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 78, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1853.26, - "placementX" : 27.638, - "placementY" : 482.89, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_79[79]", - "entityName" : "PaginationTable__row_79", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 79, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1887.16, - "placementX" : 27.638, - "placementY" : 448.99, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_80[80]", - "entityName" : "PaginationTable__row_80", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 80, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1921.06, - "placementX" : 27.638, - "placementY" : 415.09, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_81[81]", - "entityName" : "PaginationTable__row_81", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 81, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1954.96, - "placementX" : 27.638, - "placementY" : 381.19, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_82[82]", - "entityName" : "PaginationTable__row_82", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 82, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -1988.86, - "placementX" : 27.638, - "placementY" : 347.29, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_83[83]", - "entityName" : "PaginationTable__row_83", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 83, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2022.76, - "placementX" : 27.638, - "placementY" : 313.39, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_84[84]", - "entityName" : "PaginationTable__row_84", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 84, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2056.66, - "placementX" : 27.638, - "placementY" : 279.49, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_85[85]", - "entityName" : "PaginationTable__row_85", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 85, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2090.56, - "placementX" : 27.638, - "placementY" : 245.59, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_86[86]", - "entityName" : "PaginationTable__row_86", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 86, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2124.46, - "placementX" : 27.638, - "placementY" : 211.69, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_87[87]", - "entityName" : "PaginationTable__row_87", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 87, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2158.36, - "placementX" : 27.638, - "placementY" : 177.79, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_88[88]", - "entityName" : "PaginationTable__row_88", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 88, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2192.26, - "placementX" : 27.638, - "placementY" : 143.89, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_89[89]", - "entityName" : "PaginationTable__row_89", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 89, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2226.16, - "placementX" : 27.638, - "placementY" : 109.99, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_90[90]", - "entityName" : "PaginationTable__row_90", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 90, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2260.06, - "placementX" : 27.638, - "placementY" : 76.09, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_91[91]", - "entityName" : "PaginationTable__row_91", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 91, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2293.96, - "placementX" : 27.638, - "placementY" : 42.19, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 3, - "endPage" : 3, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_92[92]", - "entityName" : "PaginationTable__row_92", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 92, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2327.86, - "placementX" : 27.638, - "placementY" : 787.99, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 4, - "endPage" : 4, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_93[93]", - "entityName" : "PaginationTable__row_93", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 93, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2361.76, - "placementX" : 27.638, - "placementY" : 754.09, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 4, - "endPage" : 4, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_94[94]", - "entityName" : "PaginationTable__row_94", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 94, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2395.66, - "placementX" : 27.638, - "placementY" : 720.19, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 4, - "endPage" : 4, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - }, { - "path" : "PaginationTable[0]/PaginationTable__row_95[95]", - "entityName" : "PaginationTable__row_95", - "entityKind" : "TableRow", - "parentPath" : "PaginationTable[0]", - "childIndex" : 95, - "depth" : 2, - "layer" : 2, - "computedX" : 27.638, - "computedY" : -2429.56, - "placementX" : 27.638, - "placementY" : 686.29, - "placementWidth" : 540.0, - "placementHeight" : 33.9, - "startPage" : 4, - "endPage" : 4, - "contentWidth" : 540.0, - "contentHeight" : 33.9, - "margin" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - }, - "padding" : { - "top" : 0.0, - "right" : 0.0, - "bottom" : 0.0, - "left" : 0.0 - } - } ] -} diff --git a/render-pdf/pom.xml b/render-pdf/pom.xml index e6a2d2f53..f60b9410a 100644 --- a/render-pdf/pom.xml +++ b/render-pdf/pom.xml @@ -172,9 +172,9 @@ test io.github.demchaav @@ -208,25 +208,6 @@ maven-surefire-plugin ${maven.surefire.plugin.version} - - - org.apache.maven.plugins - maven-jar-plugin - ${maven.jar.plugin.version} - - - attach-test-jar - - test-jar - - - - @@ -239,17 +220,6 @@ release - - org.apache.maven.plugins - maven-jar-plugin - ${maven.jar.plugin.version} - - - attach-test-jar - none - - - org.apache.maven.plugins maven-source-plugin diff --git a/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderSessionTest.java b/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderSessionTest.java deleted file mode 100644 index 4e241391d..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderSessionTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.demcha.compose.engine.render.pdf.ecs; - -import com.demcha.compose.engine.components.content.shape.FillColor; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.renderable.Rectangle; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.render.pdf.ecs.handlers.PdfRectangleRenderHandler; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; - -class PdfRenderSessionTest { - - @Test - void shouldReuseSamePageSurfaceWithinOneRenderPass() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfStream stream = new PdfStream(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - - try (PdfRenderSession session = (PdfRenderSession) stream.openRenderPass()) { - PDPageContentStream first = session.pageSurface(2); - PDPageContentStream second = session.pageSurface(2); - PDPageContentStream third = session.pageSurface(1); - - assertThat(first).isSameAs(second); - assertThat(third).isNotSameAs(first); - assertThat(session.cachedPageCount()).isEqualTo(2); - assertThat(document.getNumberOfPages()).isEqualTo(3); - } - } - } - - @Test - void shouldEnsurePagesWithoutOpeningAnySurface() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfStream stream = new PdfStream(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - - try (PdfRenderSession session = (PdfRenderSession) stream.openRenderPass()) { - session.ensurePage(2); - - assertThat(document.getNumberOfPages()).isEqualTo(3); - assertThat(session.cachedPageCount()).isZero(); - } - } - } - - @Test - void shouldCloseIdempotently() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfStream stream = new PdfStream(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - PdfRenderSession session = (PdfRenderSession) stream.openRenderPass(); - - session.pageSurface(0); - session.close(); - - assertThat(session.isClosed()).isTrue(); - assertThatCode(session::close).doesNotThrowAnyException(); - } - } - - @Test - void shouldKeepSessionOwnedSurfaceOpenAfterHandlerReturns() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfRenderingSystemECS renderingSystem = new PdfRenderingSystemECS(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - Entity entity = new Entity(); - entity.addComponent(new Rectangle()); - entity.addComponent(new Placement(12, 18, 40, 24, 0, 0)); - entity.addComponent(new ContentSize(40, 24)); - entity.addComponent(FillColor.defaultColor()); - - try (PdfRenderSession session = (PdfRenderSession) renderingSystem.stream().openRenderPass()) { - renderingSystem.activeRenderSession(session); - - new PdfRectangleRenderHandler().render( - new EntityManager(), - entity, - entity.getComponent(Rectangle.class).orElseThrow(), - renderingSystem, - false); - - PDPageContentStream surface = session.pageSurface(0); - assertThatCode(() -> { - surface.saveGraphicsState(); - surface.addRect(1, 1, 2, 2); - surface.restoreGraphicsState(); - }).doesNotThrowAnyException(); - } finally { - renderingSystem.activeRenderSession(null); - } - } - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderingSystemECSDispatchTest.java b/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderingSystemECSDispatchTest.java deleted file mode 100644 index cbd48f0d1..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderingSystemECSDispatchTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.demcha.compose.engine.render.pdf.ecs; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.render.RenderHandler; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class PdfRenderingSystemECSDispatchTest { - - @Test - void shouldUseRegisteredHandler() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfRenderingSystemECS renderingSystem = new PdfRenderingSystemECS(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - EntityManager manager = new EntityManager(); - Entity entity = new Entity(); - entity.addComponent(new StubRender()); - manager.putEntity(entity); - manager.setLayers(Map.of(0, List.of(entity.getUuid()))); - - AtomicBoolean handlerCalled = new AtomicBoolean(false); - renderingSystem.renderHandlers().register(new RenderHandler() { - @Override - public Class renderType() { - return StubRender.class; - } - - @Override - public boolean render(EntityManager manager, - Entity entity, - StubRender renderComponent, - PdfRenderingSystemECS renderingSystem, - boolean guideLines) { - handlerCalled.set(true); - return true; - } - }); - - renderingSystem.process(manager); - - assertThat(handlerCalled).isTrue(); - } - } - - @Test - void shouldFailWhenRenderHasNoHandler() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfRenderingSystemECS renderingSystem = new PdfRenderingSystemECS(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - EntityManager manager = new EntityManager(); - Entity entity = new Entity(); - entity.addComponent(new UnsupportedRender()); - manager.putEntity(entity); - manager.setLayers(Map.of(0, List.of(entity.getUuid()))); - - assertThatThrownBy(() -> renderingSystem.process(manager)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining(UnsupportedRender.class.getName()); - } - } - - @Test - void shouldPreserveRenderOrderAndReuseSamePageSurfaceInsideProcess() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfRenderingSystemECS renderingSystem = new PdfRenderingSystemECS(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - EntityManager manager = new EntityManager(); - - Entity lowerEntity = new Entity(); - lowerEntity.addComponent(new StubRender()); - lowerEntity.addComponent(new Placement(10, 10, 20, 20, 0, 0)); - lowerEntity.addComponent(new ComputedPosition(10, 10)); - - Entity shiftedRightEntity = new Entity(); - shiftedRightEntity.addComponent(new StubRender()); - shiftedRightEntity.addComponent(new Placement(10, 10, 20, 20, 0, 0)); - shiftedRightEntity.addComponent(new ComputedPosition(10, 10)); - shiftedRightEntity.addComponent(Margin.left(5)); - - Entity upperEntity = new Entity(); - upperEntity.addComponent(new StubRender()); - upperEntity.addComponent(new Placement(10, 50, 20, 20, 0, 0)); - upperEntity.addComponent(new ComputedPosition(10, 50)); - - manager.putEntity(lowerEntity); - manager.putEntity(shiftedRightEntity); - manager.putEntity(upperEntity); - manager.setLayers(Map.of(0, List.of(shiftedRightEntity.getUuid(), lowerEntity.getUuid(), upperEntity.getUuid()))); - - List callOrder = new ArrayList<>(); - List surfaceIds = new ArrayList<>(); - renderingSystem.renderHandlers().register(new RenderHandler() { - @Override - public Class renderType() { - return StubRender.class; - } - - @Override - public boolean render(EntityManager manager, - Entity entity, - StubRender renderComponent, - PdfRenderingSystemECS renderingSystem, - boolean guideLines) throws IOException { - callOrder.add(entity.getUuid()); - surfaceIds.add(System.identityHashCode(renderingSystem.pageSurface(entity))); - return true; - } - }); - - renderingSystem.process(manager); - - assertThat(callOrder).containsExactly(upperEntity.getUuid(), lowerEntity.getUuid(), shiftedRightEntity.getUuid()); - assertThat(surfaceIds).hasSize(3); - assertThat(surfaceIds.get(0)).isEqualTo(surfaceIds.get(1)); - assertThat(surfaceIds.get(1)).isEqualTo(surfaceIds.get(2)); - assertThat(document.getNumberOfPages()).isEqualTo(1); - } - } - - private static final class StubRender implements com.demcha.compose.engine.render.Render { - } - - private static final class UnsupportedRender implements com.demcha.compose.engine.render.Render { - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderingSystemECSImageCacheTest.java b/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderingSystemECSImageCacheTest.java deleted file mode 100644 index 69f6f45d6..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/PdfRenderingSystemECSImageCacheTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.demcha.compose.engine.render.pdf.ecs; - -import com.demcha.compose.engine.components.content.ImageData; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; -import org.junit.jupiter.api.Test; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; - -import static org.assertj.core.api.Assertions.assertThat; - -class PdfRenderingSystemECSImageCacheTest { - - @Test - void shouldReuseOriginalImageXObjectWithinSingleDocument() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfRenderingSystemECS renderingSystem = new PdfRenderingSystemECS(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - ImageData imageData = ImageData.create(createPngBytes(200, 100)); - - PDImageXObject first = renderingSystem.getOrCreateImageXObject(imageData); - PDImageXObject second = renderingSystem.getOrCreateImageXObject(imageData); - - assertThat(second).isSameAs(first); - assertThat(renderingSystem.imageCacheStats().originalCount()).isEqualTo(1); - assertThat(renderingSystem.imageCacheStats().scaledVariantCount()).isEqualTo(0); - } - } - - @Test - void shouldReuseScaledVariantForEquivalentRoundedTargetSizes() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfRenderingSystemECS renderingSystem = new PdfRenderingSystemECS(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - ImageData imageData = ImageData.create(createPngBytes(200, 100)); - - PDImageXObject first = renderingSystem.getOrCreateImageXObject(imageData, 24.20, 16.20); - PDImageXObject second = renderingSystem.getOrCreateImageXObject(imageData, 24.24, 16.24); - - assertThat(second).isSameAs(first); - assertThat(renderingSystem.imageCacheStats().originalCount()).isEqualTo(0); - assertThat(renderingSystem.imageCacheStats().scaledVariantCount()).isEqualTo(1); - } - } - - @Test - void shouldCreateScaledVariantAtHigherThanPointDensity() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfRenderingSystemECS renderingSystem = new PdfRenderingSystemECS(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - ImageData imageData = ImageData.create(createPngBytes(400, 200)); - - PDImageXObject scaled = renderingSystem.getOrCreateImageXObject(imageData, 40, 20); - - assertThat(scaled.getWidth()).isEqualTo(80); - assertThat(scaled.getHeight()).isEqualTo(40); - assertThat(renderingSystem.imageCacheStats().originalCount()).isEqualTo(0); - assertThat(renderingSystem.imageCacheStats().scaledVariantCount()).isEqualTo(1); - } - } - - @Test - void shouldCreateOriginalAndOnlyNeededScaledVariants() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfRenderingSystemECS renderingSystem = new PdfRenderingSystemECS(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - ImageData imageData = ImageData.create(createPngBytes(200, 100)); - - PDImageXObject original = renderingSystem.getOrCreateImageXObject(imageData, 150, 80); - PDImageXObject variantSmall = renderingSystem.getOrCreateImageXObject(imageData, 40, 20); - PDImageXObject variantMedium = renderingSystem.getOrCreateImageXObject(imageData, 50, 25); - PDImageXObject repeatedSmall = renderingSystem.getOrCreateImageXObject(imageData, 40.2, 20.1); - - assertThat(variantSmall).isSameAs(repeatedSmall); - assertThat(original).isNotSameAs(variantSmall); - assertThat(variantMedium).isNotSameAs(variantSmall); - assertThat(renderingSystem.imageCacheStats().originalCount()).isEqualTo(1); - assertThat(renderingSystem.imageCacheStats().scaledVariantCount()).isEqualTo(2); - } - } - - private byte[] createPngBytes(int width, int height) { - try { - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - Graphics2D graphics = image.createGraphics(); - try { - graphics.setColor(new Color(25, 85, 135)); - graphics.fillRect(0, 0, width, height); - graphics.setColor(new Color(240, 190, 60)); - graphics.fillOval(width / 4, height / 4, width / 2, height / 2); - } finally { - graphics.dispose(); - } - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ImageIO.write(image, "png", outputStream); - return outputStream.toByteArray(); - } catch (Exception e) { - throw new IllegalStateException("Failed to generate PNG for PDF image cache test", e); - } - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfLinkRenderHandlerTest.java b/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfLinkRenderHandlerTest.java deleted file mode 100644 index 6d1279cca..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfLinkRenderHandlerTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.demcha.compose.engine.render.pdf.ecs.handlers; - -import com.demcha.compose.engine.components.content.link.LinkUrl; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.renderable.Link; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.render.pdf.ecs.PdfCanvas; -import com.demcha.compose.engine.render.pdf.ecs.PdfRenderingSystemECS; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class PdfLinkRenderHandlerTest { - - @Test - void shouldCreatePageAndAttachUriAnnotation() throws Exception { - try (PDDocument document = new PDDocument()) { - PdfRenderingSystemECS renderingSystem = new PdfRenderingSystemECS(document, new PdfCanvas(PDRectangle.A4, 0.0f)); - EntityManager manager = new EntityManager(); - Entity entity = new Entity(); - - entity.addComponent(new Link()); - entity.addComponent(new Placement(25, 40, 120, 20, 0, 0)); - entity.addComponent(new ContentSize(120, 20)); - entity.addComponent(Padding.of(4)); - entity.addComponent(new LinkUrl("https://example.com/profile")); - - boolean rendered = new PdfLinkRenderHandler().render( - manager, - entity, - entity.getComponent(Link.class).orElseThrow(), - renderingSystem, - false); - - assertThat(rendered).isTrue(); - assertThat(document.getNumberOfPages()).isEqualTo(1); - assertThat(document.getPage(0).getAnnotations()).hasSize(1); - assertThat(document.getPage(0).getAnnotations().get(0)).isInstanceOf(PDAnnotationLink.class); - - PDAnnotationLink annotation = (PDAnnotationLink) document.getPage(0).getAnnotations().get(0); - assertThat(annotation.getAction()).isInstanceOf(PDActionURI.class); - assertThat(((PDActionURI) annotation.getAction()).getURI()).isEqualTo("https://example.com/profile"); - assertThat(annotation.getRectangle().getWidth()).isEqualTo(128.0f); - assertThat(annotation.getRectangle().getHeight()).isEqualTo(28.0f); - } - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfTableRowRenderHandlerTest.java b/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfTableRowRenderHandlerTest.java deleted file mode 100644 index aebbcb290..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfTableRowRenderHandlerTest.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.demcha.compose.engine.render.pdf.ecs.handlers; - -import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory; -import com.demcha.compose.engine.components.content.table.TableCellLayoutStyle; -import com.demcha.compose.engine.components.content.shape.Side; -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.content.table.TableResolvedCell; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.ParentComponent; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.render.pdf.PdfFont; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -class PdfTableRowRenderHandlerTest { - - private final PdfTableRowRenderHandler handler = new PdfTableRowRenderHandler(); - - @Test - void shouldTreatFirstRowOnNewPageAsNewFragment() { - EntityManager manager = new EntityManager(); - Entity parent = new Entity(); - Entity previousRow = new Entity(); - Entity currentRow = new Entity(); - - parent.getChildren().add(previousRow.getUuid()); - parent.getChildren().add(currentRow.getUuid()); - - previousRow.addComponent(new ParentComponent(parent)); - currentRow.addComponent(new ParentComponent(parent)); - - previousRow.addComponent(new Placement(0, 0, 200, 20, 0, 0)); - currentRow.addComponent(new Placement(0, 0, 200, 20, 1, 1)); - - manager.putEntity(parent); - manager.putEntity(previousRow); - manager.putEntity(currentRow); - - TableResolvedCell cell = new TableResolvedCell( - "cell", - 0, - 100, - 20, - List.of("value"), - TableCellLayoutStyle.builder() - .stroke(new Stroke(ComponentColor.BLACK, 2.0)) - .build(), - new Padding(3.0, 0.5, 0.5, 0.5), - Set.of(Side.RIGHT, Side.BOTTOM) - ); - - assertThat(handler.startsPageFragment(manager, currentRow)).isTrue(); - assertThat(handler.effectiveBorderSides(cell, true)).containsExactlyInAnyOrder(Side.TOP, Side.RIGHT, Side.BOTTOM); - assertThat(handler.effectiveFillInsets(cell, true)) - .isEqualTo(new Padding(1.0, 0.5, 0.5, 0.5)); - } - - @Test - void shouldKeepSingleSeparatorWhenRowsStayOnSamePage() { - EntityManager manager = new EntityManager(); - Entity parent = new Entity(); - Entity previousRow = new Entity(); - Entity currentRow = new Entity(); - - parent.getChildren().add(previousRow.getUuid()); - parent.getChildren().add(currentRow.getUuid()); - - previousRow.addComponent(new ParentComponent(parent)); - currentRow.addComponent(new ParentComponent(parent)); - - previousRow.addComponent(new Placement(0, 40, 200, 20, 0, 0)); - currentRow.addComponent(new Placement(0, 20, 200, 20, 0, 0)); - - manager.putEntity(parent); - manager.putEntity(previousRow); - manager.putEntity(currentRow); - - TableResolvedCell cell = new TableResolvedCell( - "cell", - 0, - 100, - 20, - List.of("value"), - TableCellLayoutStyle.builder() - .stroke(new Stroke(ComponentColor.BLACK, 2.0)) - .build(), - new Padding(3.0, 0.5, 0.5, 0.5), - Set.of(Side.RIGHT, Side.BOTTOM) - ); - - assertThat(handler.startsPageFragment(manager, currentRow)).isFalse(); - assertThat(handler.effectiveBorderSides(cell, false)).containsExactlyInAnyOrder(Side.RIGHT, Side.BOTTOM); - assertThat(handler.effectiveFillInsets(cell, false)) - .isEqualTo(new Padding(3.0, 0.5, 0.5, 0.5)); - } - - @Test - void shouldResolveTopAndMiddleAnchorsForMultilineContent() { - PdfFont font = PdfFontLibraryFactory.standardLibrary() - .getFont(TextStyle.DEFAULT_STYLE.fontName(), PdfFont.class) - .orElseThrow(); - - TableResolvedCell topCell = new TableResolvedCell( - "top", - 0, - 120, - 60, - List.of("09:00 17:00", "Stock take"), - TableCellLayoutStyle.merge(TableCellLayoutStyle.DEFAULT, TableCellLayoutStyle.builder() - .textAnchor(Anchor.topLeft()) - .build()), - Padding.zero(), - Set.of(Side.TOP, Side.LEFT, Side.RIGHT, Side.BOTTOM) - ); - - TableResolvedCell middleCell = new TableResolvedCell( - "middle", - 0, - 120, - 60, - List.of("09:00 17:00", "Stock take"), - TableCellLayoutStyle.merge(TableCellLayoutStyle.DEFAULT, TableCellLayoutStyle.builder() - .textAnchor(Anchor.centerLeft()) - .build()), - Padding.zero(), - Set.of(Side.TOP, Side.LEFT, Side.RIGHT, Side.BOTTOM) - ); - - var topLines = handler.resolveTextLines(font, topCell, 0, 0); - var middleLines = handler.resolveTextLines(font, middleCell, 0, 0); - double lineHeight = font.getLineHeight(topCell.style().textStyle()); - - assertThat(topLines).hasSize(2); - assertThat(middleLines).hasSize(2); - assertThat(topLines.get(0).baselineY()).isGreaterThan(middleLines.get(0).baselineY()); - assertThat(topLines.get(0).baselineY() - topLines.get(1).baselineY()).isEqualTo(lineHeight); - assertThat(middleLines.get(0).baselineY() - middleLines.get(1).baselineY()).isEqualTo(lineHeight); - } - - @Test - void shouldApplyCellLineSpacingWhenResolvingMultilineContent() { - PdfFont font = PdfFontLibraryFactory.standardLibrary() - .getFont(TextStyle.DEFAULT_STYLE.fontName(), PdfFont.class) - .orElseThrow(); - double lineSpacing = 2.5; - - TableResolvedCell cell = new TableResolvedCell( - "spaced", - 0, - 120, - 80, - List.of("First", "Second"), - TableCellLayoutStyle.merge(TableCellLayoutStyle.DEFAULT, TableCellLayoutStyle.builder() - .textAnchor(Anchor.topLeft()) - .lineSpacing(lineSpacing) - .build()), - Padding.zero(), - Set.of(Side.TOP, Side.LEFT, Side.RIGHT, Side.BOTTOM) - ); - - var lines = handler.resolveTextLines(font, cell, 0, 0); - double lineHeight = font.getLineHeight(cell.style().textStyle()); - - assertThat(lines).hasSize(2); - assertThat(lines.get(0).baselineY() - lines.get(1).baselineY()).isEqualTo(lineHeight + lineSpacing); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/EngineComposerHarness.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/EngineComposerHarness.java deleted file mode 100644 index 03acf456a..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/EngineComposerHarness.java +++ /dev/null @@ -1,576 +0,0 @@ -package com.demcha.compose.testsupport; - -import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory; -import com.demcha.compose.font.FontFamilyDefinition; -import com.demcha.compose.font.FontName; -import com.demcha.compose.testsupport.engine.assembly.ComponentBuilder; -import com.demcha.compose.engine.components.content.header_footer.HeaderFooterConfig; -import com.demcha.compose.engine.components.content.header_footer.HeaderFooterZone; -import com.demcha.compose.engine.components.content.metadata.DocumentMetadata; -import com.demcha.compose.engine.components.content.protection.PdfProtectionConfig; -import com.demcha.compose.engine.components.content.watermark.WatermarkConfig; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.debug.LayoutSnapshot; -import com.demcha.compose.engine.debug.LayoutSnapshotExtractor; -import com.demcha.compose.engine.layout.LayoutSystem; -import com.demcha.compose.engine.render.pdf.ecs.PdfCanvas; -import com.demcha.compose.engine.render.pdf.ecs.PdfFileManagerSystem; -import com.demcha.compose.engine.render.pdf.PdfFont; -import com.demcha.compose.engine.render.pdf.ecs.PdfRenderingSystemECS; -import com.demcha.compose.engine.render.pdf.ecs.helpers.PdfBookmarkBuilder; -import com.demcha.compose.engine.render.pdf.helpers.PdfHeaderFooterRenderer; -import com.demcha.compose.engine.render.pdf.helpers.PdfWatermarkRenderer; -import com.demcha.compose.engine.measurement.FontLibraryTextMeasurementSystem; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDDocumentInformation; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.encryption.AccessPermission; -import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy; - -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -/** - * Test-only low-level engine harness used while production authoring stays - * canonical through {@code DocumentSession}. - */ -public final class EngineComposerHarness implements Closeable { - private final PDDocument doc; - private final EntityManager entityManager; - private final ComponentBuilder componentBuilder; - private final Canvas canvas; - private final LayoutSystem layoutSystem; - private final PdfRenderingSystemECS renderingSystem; - private final Path outputFile; - private final PdfFileManagerSystem fileManagerSystem; - private final List headerFooterConfigs = new ArrayList<>(); - private WatermarkConfig watermarkConfig; - private DocumentMetadata documentMetadata; - private PdfProtectionConfig protectionConfig; - private boolean postProcessed; - private boolean layoutResolved; - private boolean rendered; - private long layoutConfigVersion; - private long postProcessConfigVersion; - private long resolvedLayoutVersion = -1; - private long renderedVersion = -1; - private long postProcessedVersion = -1; - - private EngineComposerHarness(Path outputFile, - boolean markdown, - boolean guideLines, - PDRectangle pageSize, - Margin margin, - Collection customFontFamilies) { - this.doc = new PDDocument(); - this.entityManager = new EntityManager(PdfFontLibraryFactory.library(doc, customFontFamilies), markdown); - this.entityManager.setGuideLines(guideLines); - this.componentBuilder = createComponentBuilder(entityManager); - this.canvas = new PdfCanvas(pageSize, 0.0f, 0.0f); - if (margin != null) { - this.canvas.addMargin(margin); - } - this.renderingSystem = new PdfRenderingSystemECS(doc, canvas); - this.layoutSystem = new LayoutSystem<>(canvas, renderingSystem); - this.outputFile = outputFile; - this.fileManagerSystem = outputFile == null ? null : new PdfFileManagerSystem(outputFile, doc); - setupSystems(); - } - - /** - * Starts a test harness builder for in-memory output. - * - * @return harness builder - */ - public static Builder pdf() { - return new Builder(null); - } - - /** - * Starts a test harness builder for file output. - * - * @param outputFile target PDF path - * @return harness builder - */ - public static Builder pdf(Path outputFile) { - return new Builder(outputFile); - } - - /** - * Returns the low-level engine builder facade. - * - * @return component builder - */ - public ComponentBuilder componentBuilder() { - return componentBuilder; - } - - /** - * Returns the active test canvas. - * - * @return canvas - */ - public Canvas canvas() { - return canvas; - } - - /** - * Enables or disables markdown parsing in the test entity manager. - * - * @param enabled whether markdown should be enabled - */ - public void markdown(boolean enabled) { - entityManager.setMarkdown(enabled); - } - - /** - * Enables or disables guide-line rendering in tests. - * - * @param enabled whether guides should be rendered - */ - public void guideLines(boolean enabled) { - entityManager.setGuideLines(enabled); - } - - /** - * Adds an extra canvas margin. - * - * @param margin margin to add - */ - public void margin(Margin margin) { - canvas.addMargin(margin); - markLayoutDirty(); - } - - /** - * Lists font families available to this harness. - * - * @return available font names - */ - public List availableFonts() { - return List.copyOf(entityManager.getFonts().availableFonts()); - } - - /** - * Adds a repeating header. - * - * @param config header config - * @return this harness - */ - public EngineComposerHarness header(HeaderFooterConfig config) { - this.headerFooterConfigs.add(HeaderFooterConfig.builder() - .zone(HeaderFooterZone.HEADER) - .height(config.getHeight()) - .leftText(config.getLeftText()) - .centerText(config.getCenterText()) - .rightText(config.getRightText()) - .fontSize(config.getFontSize()) - .textColor(config.getTextColor()) - .showSeparator(config.isShowSeparator()) - .separatorColor(config.getSeparatorColor()) - .separatorThickness(config.getSeparatorThickness()) - .build()); - markPostProcessDirty(); - return this; - } - - /** - * Adds a simple repeating header. - * - * @return this harness - */ - public EngineComposerHarness header(String leftText, String centerText, String rightText) { - this.headerFooterConfigs.add(HeaderFooterConfig.builder() - .zone(HeaderFooterZone.HEADER) - .leftText(leftText) - .centerText(centerText) - .rightText(rightText) - .showSeparator(true) - .build()); - markPostProcessDirty(); - return this; - } - - /** - * Adds a repeating footer. - * - * @param config footer config - * @return this harness - */ - public EngineComposerHarness footer(HeaderFooterConfig config) { - this.headerFooterConfigs.add(HeaderFooterConfig.builder() - .zone(HeaderFooterZone.FOOTER) - .height(config.getHeight()) - .leftText(config.getLeftText()) - .centerText(config.getCenterText()) - .rightText(config.getRightText()) - .fontSize(config.getFontSize()) - .textColor(config.getTextColor()) - .showSeparator(config.isShowSeparator()) - .separatorColor(config.getSeparatorColor()) - .separatorThickness(config.getSeparatorThickness()) - .build()); - markPostProcessDirty(); - return this; - } - - /** - * Adds a simple repeating footer. - * - * @return this harness - */ - public EngineComposerHarness footer(String leftText, String centerText, String rightText) { - this.headerFooterConfigs.add(HeaderFooterConfig.builder() - .zone(HeaderFooterZone.FOOTER) - .leftText(leftText) - .centerText(centerText) - .rightText(rightText) - .showSeparator(true) - .build()); - markPostProcessDirty(); - return this; - } - - /** - * Adds a document-wide watermark. - * - * @param config watermark config - * @return this harness - */ - public EngineComposerHarness watermark(WatermarkConfig config) { - this.watermarkConfig = config; - markPostProcessDirty(); - return this; - } - - /** - * Adds a text watermark. - * - * @param text watermark text - * @return this harness - */ - public EngineComposerHarness watermark(String text) { - this.watermarkConfig = WatermarkConfig.builder().text(text).build(); - markPostProcessDirty(); - return this; - } - - /** - * Adds document metadata. - * - * @param metadata metadata config - * @return this harness - */ - public EngineComposerHarness metadata(DocumentMetadata metadata) { - this.documentMetadata = metadata; - markPostProcessDirty(); - return this; - } - - /** - * Adds protection settings. - * - * @param config protection config - * @return this harness - */ - public EngineComposerHarness protect(PdfProtectionConfig config) { - this.protectionConfig = config; - markPostProcessDirty(); - return this; - } - - /** - * Returns a deterministic layout snapshot. - * - * @return layout snapshot - * @throws Exception if layout fails - */ - public LayoutSnapshot layoutSnapshot() throws Exception { - buildComponents(); - ensureLayoutResolved(); - return LayoutSnapshotExtractor.extract(entityManager, canvas); - } - - /** - * Builds and saves the document when configured with an output path. - * - * @throws Exception if rendering or writing fails - */ - public void build() throws Exception { - buildComponents(); - ensureRendered(); - applyPostProcessing(); - if (fileManagerSystem != null) { - fileManagerSystem.process(entityManager); - } - } - - /** - * Builds and returns PDF bytes. - * - * @return rendered bytes - * @throws Exception if rendering fails - */ - public byte[] toBytes() throws Exception { - buildComponents(); - ensureRendered(); - applyPostProcessing(); - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - doc.save(baos); - return baos.toByteArray(); - } - } - - /** - * Builds and returns the underlying PDFBox document for focused tests. - * - * @return rendered document owned by this harness - * @throws Exception if rendering fails - */ - public PDDocument toPDDocument() throws Exception { - buildComponents(); - ensureRendered(); - applyPostProcessing(); - return doc; - } - - @Override - public void close() throws IOException { - doc.close(); - } - - private void setupSystems() { - entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class)); - entityManager.getSystems().addSystem(layoutSystem); - entityManager.getSystems().addSystem(renderingSystem); - } - - private void buildComponents() { - componentBuilder.buildsComponents(); - } - - private void ensureLayoutResolved() throws Exception { - long currentVersion = currentLayoutInputsVersion(); - if (layoutResolved && currentVersion == resolvedLayoutVersion) { - return; - } - if (rendered) { - throw new IllegalStateException("Harness was mutated after rendering. Create a new harness for a second render pass."); - } - layoutResolved = false; - rendered = false; - postProcessed = false; - layoutSystem.process(entityManager); - layoutResolved = true; - resolvedLayoutVersion = currentLayoutInputsVersion(); - } - - private void ensureRendered() throws Exception { - ensureLayoutResolved(); - long currentVersion = currentRenderInputsVersion(); - if (rendered && currentVersion == renderedVersion) { - return; - } - if (postProcessed) { - throw new IllegalStateException("Harness was mutated after post-processing. Create a new harness for a second export."); - } - renderingSystem.process(entityManager); - rendered = true; - renderedVersion = currentRenderInputsVersion(); - } - - private void applyPostProcessing() throws IOException { - long currentVersion = currentPostProcessInputsVersion(); - if (postProcessed && currentVersion == postProcessedVersion) { - return; - } - if (postProcessed) { - throw new IllegalStateException("Harness post-processing has already been applied for a previous state."); - } - postProcessed = true; - postProcessedVersion = currentVersion; - - if (watermarkConfig != null) { - PdfWatermarkRenderer.apply(doc, watermarkConfig); - } - if (!headerFooterConfigs.isEmpty()) { - Margin canvasMargin = canvas.margin(); - float marginLeft = canvasMargin != null ? (float) canvasMargin.left() : 24f; - float marginRight = canvasMargin != null ? (float) canvasMargin.right() : 24f; - PdfHeaderFooterRenderer.apply(doc, headerFooterConfigs, marginLeft, marginRight); - } - PdfBookmarkBuilder.buildOutline(doc, entityManager); - if (documentMetadata != null) { - PDDocumentInformation info = doc.getDocumentInformation(); - if (documentMetadata.getTitle() != null) { - info.setTitle(documentMetadata.getTitle()); - } - if (documentMetadata.getAuthor() != null) { - info.setAuthor(documentMetadata.getAuthor()); - } - if (documentMetadata.getSubject() != null) { - info.setSubject(documentMetadata.getSubject()); - } - if (documentMetadata.getKeywords() != null) { - info.setKeywords(documentMetadata.getKeywords()); - } - if (documentMetadata.getCreator() != null) { - info.setCreator(documentMetadata.getCreator()); - } - if (documentMetadata.getProducer() != null) { - info.setProducer(documentMetadata.getProducer()); - } - } - if (protectionConfig != null) { - applyProtection(); - } - } - - private void applyProtection() throws IOException { - AccessPermission permission = new AccessPermission(); - permission.setCanPrint(protectionConfig.isCanPrint()); - permission.setCanExtractContent(protectionConfig.isCanCopyContent()); - permission.setCanModify(protectionConfig.isCanModify()); - permission.setCanFillInForm(protectionConfig.isCanFillForms()); - permission.setCanExtractForAccessibility(protectionConfig.isCanExtractForAccessibility()); - permission.setCanAssembleDocument(protectionConfig.isCanAssemble()); - permission.setCanPrintFaithful(protectionConfig.isCanPrintHighQuality()); - - StandardProtectionPolicy policy = new StandardProtectionPolicy( - protectionConfig.getOwnerPassword(), - protectionConfig.getUserPassword(), - permission); - policy.setEncryptionKeyLength(protectionConfig.getKeyLength()); - doc.protect(policy); - } - - private long currentLayoutInputsVersion() { - return entityManager.getMutationVersion() * 31L + layoutConfigVersion; - } - - private long currentRenderInputsVersion() { - return currentLayoutInputsVersion(); - } - - private long currentPostProcessInputsVersion() { - return currentRenderInputsVersion() * 31L + postProcessConfigVersion; - } - - private void markLayoutDirty() { - layoutConfigVersion++; - layoutResolved = false; - rendered = false; - postProcessed = false; - } - - private void markPostProcessDirty() { - postProcessConfigVersion++; - postProcessed = false; - } - - private static ComponentBuilder createComponentBuilder(EntityManager entityManager) { - try { - Constructor constructor = ComponentBuilder.class.getDeclaredConstructor(EntityManager.class); - constructor.setAccessible(true); - return constructor.newInstance(entityManager); - } catch (ReflectiveOperationException e) { - throw new IllegalStateException("Failed to create test component builder", e); - } - } - - /** - * Builder for test harness instances. - */ - public static final class Builder { - private final Path outputFile; - private PDRectangle pageSize = PDRectangle.A4; - private Margin margin; - private boolean markdown = true; - private boolean guideLines; - private final List customFontFamilies = new ArrayList<>(); - - private Builder(Path outputFile) { - this.outputFile = outputFile; - } - - public Builder pageSize(PDRectangle pageSize) { - this.pageSize = Objects.requireNonNull(pageSize, "pageSize"); - return this; - } - - public Builder margin(Margin margin) { - this.margin = margin; - return this; - } - - public Builder margin(float top, float right, float bottom, float left) { - this.margin = new Margin(top, right, bottom, left); - return this; - } - - public Builder markdown(boolean enabled) { - this.markdown = enabled; - return this; - } - - public Builder guideLines(boolean enabled) { - this.guideLines = enabled; - return this; - } - - public Builder registerFontFamily(FontFamilyDefinition definition) { - this.customFontFamilies.add(Objects.requireNonNull(definition, "definition")); - return this; - } - - public Builder registerFontFamily(FontName familyName, Path regular) { - return registerFontFamily(FontFamilyDefinition.files(familyName, regular).build()); - } - - public Builder registerFontFamily(String familyName, Path regular) { - return registerFontFamily(FontName.of(familyName), regular); - } - - public Builder registerFontFamily(FontName familyName, Path regular, Path bold, Path italic) { - return registerFontFamily(FontFamilyDefinition.files(familyName, regular) - .boldPath(bold) - .italicPath(italic) - .build()); - } - - public Builder registerFontFamily(String familyName, Path regular, Path bold, Path italic) { - return registerFontFamily(FontName.of(familyName), regular, bold, italic); - } - - public Builder registerFontFamily(FontName familyName, Path regular, Path bold, Path italic, Path boldItalic) { - return registerFontFamily(FontFamilyDefinition.files(familyName, regular) - .boldPath(bold) - .italicPath(italic) - .boldItalicPath(boldItalic) - .build()); - } - - public Builder registerFontFamily(String familyName, Path regular, Path bold, Path italic, Path boldItalic) { - return registerFontFamily(FontName.of(familyName), regular, bold, italic, boldItalic); - } - - public EngineComposerHarness create() { - return new EngineComposerHarness( - outputFile, - markdown, - guideLines, - pageSize, - margin, - List.copyOf(customFontFamilies)); - } - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BarcodeBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BarcodeBuilder.java deleted file mode 100644 index 7fb53acd6..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BarcodeBuilder.java +++ /dev/null @@ -1,165 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.EmptyBox; -import com.demcha.compose.engine.components.content.barcode.BarcodeData; -import com.demcha.compose.engine.components.content.barcode.BarcodeType; -import com.demcha.compose.engine.components.renderable.BarcodeComponent; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.core.EntityManager; -import lombok.extern.slf4j.Slf4j; - -import java.awt.Color; - -/** - * Builder for barcode/QR-code leaf entities. - * - *

Barcodes are fixed-size leaves rendered as bitmap images at draw time. - * The builder follows the same contract as {@link ImageBuilder} and - * {@link CircleBuilder}: extend {@link EmptyBox}, attach a renderable marker - * and content, register via {@code build()}.

- * - *

Usage

- *
- * cb.barcode()
- *     .data("https://example.com/invoice/12345")
- *     .type(BarcodeType.QR_CODE)
- *     .size(120, 120)
- *     .anchor(Anchor.topRight())
- *     .build();
- * 
- * - * @author Artem Demchyshyn - */ -@Slf4j -public class BarcodeBuilder extends EmptyBox { - - private String content; - private BarcodeType type = BarcodeType.QR_CODE; - private Color foreground = Color.BLACK; - private Color background = Color.WHITE; - private int quietZoneMargin = 0; - - BarcodeBuilder(EntityManager entityManager) { - super(entityManager); - } - - /** - * Sets the data content to encode in the barcode. - * - * @param content the string payload (URL, product code, etc.) - * @return this builder - */ - public BarcodeBuilder data(String content) { - this.content = content; - return this; - } - - /** - * Sets the barcode symbology type. - * - * @param type the barcode type, for example {@link BarcodeType#QR_CODE} - * or {@link BarcodeType#CODE_128} - * @return this builder - */ - public BarcodeBuilder type(BarcodeType type) { - this.type = type; - return this; - } - - /** - * Sets a QR Code barcode type. Convenience shorthand for - * {@code type(BarcodeType.QR_CODE)}. - */ - public BarcodeBuilder qrCode() { - return type(BarcodeType.QR_CODE); - } - - /** - * Sets a Code 128 barcode type. - */ - public BarcodeBuilder code128() { - return type(BarcodeType.CODE_128); - } - - /** - * Sets a Code 39 barcode type. - */ - public BarcodeBuilder code39() { - return type(BarcodeType.CODE_39); - } - - /** - * Sets an EAN-13 barcode type. - */ - public BarcodeBuilder ean13() { - return type(BarcodeType.EAN_13); - } - - /** - * Sets an EAN-8 barcode type. - */ - public BarcodeBuilder ean8() { - return type(BarcodeType.EAN_8); - } - - /** - * Sets the foreground (bar) color. - * - * @param foreground the bar color - * @return this builder - */ - public BarcodeBuilder foreground(Color foreground) { - this.foreground = foreground; - return this; - } - - /** - * Sets the foreground (bar) color from a {@link ComponentColor}. - */ - public BarcodeBuilder foreground(ComponentColor foreground) { - return foreground(foreground.color()); - } - - /** - * Sets the background (space) color. - * - * @param background the background color - * @return this builder - */ - public BarcodeBuilder background(Color background) { - this.background = background; - return this; - } - - /** - * Sets the background (space) color from a {@link ComponentColor}. - */ - public BarcodeBuilder background(ComponentColor background) { - return background(background.color()); - } - - /** - * Sets the quiet zone margin in barcode modules. - * - * @param margin number of modules for the quiet zone (0 = no extra margin) - * @return this builder - */ - public BarcodeBuilder quietZone(int margin) { - this.quietZoneMargin = Math.max(0, margin); - return this; - } - - @Override - public void initialize() { - entity.addComponent(new BarcodeComponent()); - } - - @Override - public com.demcha.compose.engine.components.core.Entity build() { - if (content == null || content.isEmpty()) { - throw new IllegalStateException("BarcodeBuilder requires data content before build()"); - } - entity.addComponent(BarcodeData.of(content, type, foreground, background, quietZoneMargin)); - return registerBuiltEntity(); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BarcodeBuilderTest.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BarcodeBuilderTest.java deleted file mode 100644 index 2c1b6b1f4..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BarcodeBuilderTest.java +++ /dev/null @@ -1,231 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.engine.components.content.barcode.BarcodeData; -import com.demcha.compose.engine.components.content.barcode.BarcodeType; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.renderable.BarcodeComponent; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.Align; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.awt.Color; -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Unit and visual integration tests for {@link BarcodeBuilder}. - */ -class BarcodeBuilderTest { - - @TempDir - Path tempDir; - - // ===== Unit Tests ===== - - @Test - void shouldCreateBarcodeEntityWithQrCodeTypeByDefault() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity entity = composer.componentBuilder() - .barcode() - .data("https://example.com") - .size(100, 100) - .anchor(Anchor.topLeft()) - .build(); - - assertThat(entity.hasAssignable(BarcodeComponent.class)).isTrue(); - assertThat(entity.getComponent(BarcodeData.class)) - .hasValueSatisfying(data -> { - assertThat(data.getContent()).isEqualTo("https://example.com"); - assertThat(data.getType()).isEqualTo(BarcodeType.QR_CODE); - assertThat(data.getForeground()).isEqualTo(Color.BLACK); - assertThat(data.getBackground()).isEqualTo(Color.WHITE); - }); - assertThat(entity.getComponent(ContentSize.class)) - .hasValueSatisfying(size -> { - assertThat(size.width()).isEqualTo(100); - assertThat(size.height()).isEqualTo(100); - }); - } - } - - @Test - void shouldSetBarcodeTypeViaConvenienceMethods() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity entity = composer.componentBuilder() - .barcode() - .data("12345678") - .code128() - .size(200, 80) - .anchor(Anchor.topLeft()) - .build(); - - assertThat(entity.getComponent(BarcodeData.class)) - .hasValueSatisfying(data -> - assertThat(data.getType()).isEqualTo(BarcodeType.CODE_128)); - } - } - - @Test - void shouldSetCustomColorsAndQuietZone() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity entity = composer.componentBuilder() - .barcode() - .data("TEST") - .type(BarcodeType.CODE_39) - .foreground(Color.BLUE) - .background(Color.YELLOW) - .quietZone(4) - .size(200, 60) - .anchor(Anchor.topLeft()) - .build(); - - assertThat(entity.getComponent(BarcodeData.class)) - .hasValueSatisfying(data -> { - assertThat(data.getForeground()).isEqualTo(Color.BLUE); - assertThat(data.getBackground()).isEqualTo(Color.YELLOW); - assertThat(data.getMargin()).isEqualTo(4); - }); - } - } - - @Test - void shouldFailWithoutData() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - BarcodeBuilder builder = composer.componentBuilder() - .barcode() - .size(100, 100) - .anchor(Anchor.topLeft()); - - assertThatThrownBy(builder::build) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("data content"); - } - } - - // ===== Visual Integration Tests ===== - - @Test - void shouldRenderQrCodeToPdf() throws Exception { - Path outputFile = tempDir.resolve("barcode-qr.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .margin(40, 40, 40, 40) - .create()) { - var cb = composer.componentBuilder(); - - cb.barcode() - .data("https://github.com/DemchaAV/GraphCompose") - .qrCode() - .size(150, 150) - .anchor(Anchor.topLeft()) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists().isRegularFile().isNotEmptyFile(); - try (PDDocument saved = Loader.loadPDF(outputFile.toFile())) { - assertThat(saved.getNumberOfPages()).isGreaterThan(0); - } - } - - @Test - void shouldRenderCode128ToPdf() throws Exception { - Path outputFile = tempDir.resolve("barcode-code128.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .margin(40, 40, 40, 40) - .create()) { - var cb = composer.componentBuilder(); - - cb.barcode() - .data("INV-2026-001234") - .code128() - .size(250, 80) - .anchor(Anchor.topLeft()) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists().isNotEmptyFile(); - try (PDDocument saved = Loader.loadPDF(outputFile.toFile())) { - assertThat(saved.getNumberOfPages()).isGreaterThan(0); - } - } - - @Test - void shouldRenderEan13ToPdf() throws Exception { - Path outputFile = tempDir.resolve("barcode-ean13.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .margin(40, 40, 40, 40) - .create()) { - var cb = composer.componentBuilder(); - - cb.barcode() - .data("4006381333931") - .ean13() - .size(200, 80) - .foreground(ComponentColor.DARK_BLUE) - .anchor(Anchor.topLeft()) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists().isNotEmptyFile(); - } - - @Test - void shouldRenderMultipleBarcodesInModule() throws Exception { - Path outputFile = tempDir.resolve("barcode-multi.pdf"); - - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf(outputFile) - .margin(40, 40, 40, 40) - .create()) { - var cb = composer.componentBuilder(); - - var module = cb.moduleBuilder(Align.middle(8), composer.canvas()) - .anchor(Anchor.topLeft()) - .addChild( - cb.barcode() - .data("https://graphcompose.dev") - .qrCode() - .size(120, 120) - .build() - ) - .addChild( - cb.barcode() - .data("SHIP-99887766") - .code128() - .size(220, 70) - .build() - ) - .addChild( - cb.barcode() - .data("5901234123457") - .ean13() - .size(200, 80) - .build() - ) - .build(); - - composer.build(); - } - - assertThat(outputFile).exists().isNotEmptyFile(); - try (PDDocument saved = Loader.loadPDF(outputFile.toFile())) { - assertThat(saved.getNumberOfPages()).isGreaterThan(0); - } - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilder.java deleted file mode 100644 index fe08b8195..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilder.java +++ /dev/null @@ -1,816 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.EmptyBox; -import com.demcha.compose.engine.components.content.text.BlockTextData; -import com.demcha.compose.engine.components.content.text.BlockTextLineMetrics; -import com.demcha.compose.engine.components.content.text.LineTextData; -import com.demcha.compose.engine.components.content.text.Text; -import com.demcha.compose.engine.components.content.text.TextDataBody; -import com.demcha.compose.engine.components.content.text.TextIndentStrategy; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.HAnchor; -import com.demcha.compose.engine.components.renderable.BlockText; -import com.demcha.compose.engine.components.renderable.TextComponent; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.measurement.TextMeasurementSystem; -import com.demcha.compose.engine.utils.TextSanitizer; -import com.demcha.compose.engine.text.markdown.MarkDownParser; -import lombok.NonNull; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - -import java.util.*; - -/** - * Builder for breakable multi-line text blocks. - *

- * This builder is used when text should behave like paragraph content rather - * than a single measured line. It can parse markdown, normalize bullet prefixes, - * wrap lines to the available width, and emit the line metadata later consumed - * by layout, pagination, and rendering. - *

- * - *

Compared with {@link TextBuilder}, this builder is the better choice for - * profile sections, descriptions, lists, and any content that may need to span - * multiple lines or pages.

- */ -@Slf4j -public class BlockTextBuilder extends EmptyBox { - - private final MarkDownParser markDownParser = new MarkDownParser(); - private TextIndentStrategy blockStrategy; - - private Map, Component> baseComponents; - - @Setter - private double lineSpacing = 0.0; - - private List lines; - - private TextStyle textStyle; - private String bulletOffset; - - BlockTextBuilder(EntityManager entityManager, Align align, TextStyle textStyle) { - super(entityManager); - align(align); - this.textStyle = textStyle; - this.addComponent(textStyle); - this.blockStrategy = TextIndentStrategy.NONE; - } - - /** - * Sets the indentation strategy used when wrapped lines are generated. - * - * @param strategy the indent strategy to apply - * @return this builder - */ - public BlockTextBuilder strategy(TextIndentStrategy strategy) { - this.blockStrategy = strategy; - return this; - } - - private static String normalizeBulletPrefix(String bulletOffset) { - if (bulletOffset == null) - return ""; - if (bulletOffset.isEmpty()) - return ""; - - // If user passes a bullet like "-" or "===", add a space so it doesn't stick to - // the first word. - char last = bulletOffset.charAt(bulletOffset.length() - 1); - if (!Character.isWhitespace(last)) { - return bulletOffset + " "; - } - return bulletOffset; - } - - /** - * Build an indentation string (spaces) with visual width ~= prefix width. - * This is crucial for custom bullets like "===" where char-count based indents - * are wrong. - */ - private static String computeIndentFromPrefix(TextMeasurementSystem measurementSystem, TextStyle style, String prefix) { - if (prefix == null || prefix.isEmpty()) - return ""; - - double target = measurementSystem.textWidth(style, prefix); - double spaceW = measurementSystem.textWidth(style, " "); - - if (spaceW <= 0) - return ""; - - int spaces = (int) Math.ceil(target / spaceW); - String indent = " ".repeat(Math.max(0, spaces)); - - // Debug: confirm we actually match the prefix width. - double indentW = measurementSystem.textWidth(style, indent); - log.debug("Bullet indent compute | prefix='{}' prefixW={} spaceW={} spaces={} indentW={}", - vis(prefix), target, spaceW, spaces, indentW); - - return indent; - } - - private static void appendPrefixBodies(List out, String prefix, TextStyle style) { - if (prefix == null || prefix.isEmpty()) - return; - - boolean hasVisible = prefix.chars().anyMatch(ch -> !Character.isWhitespace(ch)); - - // If it's only spaces/tabs, keep as a single token. - if (!hasVisible) { - out.add(new TextDataBody(prefix, style)); - return; - } - - // Split trailing whitespace into separate tokens to avoid "===CVRewriter" / - // "-Honed" issues - int end = prefix.length(); - while (end > 0 && Character.isWhitespace(prefix.charAt(end - 1))) { - end--; - } - - String core = prefix.substring(0, end); - String ws = prefix.substring(end); - - if (!core.isEmpty()) { - out.add(new TextDataBody(core, style)); - } - - for (int i = 0; i < ws.length(); i++) { - char c = ws.charAt(i); - out.add(new TextDataBody(String.valueOf(c), style)); - } - } - - private static String vis(String s) { - if (s == null) - return "null"; - return s - .replace("\\", "\\\\") - .replace("\r", "\\r") - .replace("\n", "\\n") - .replace("\t", "\\t"); - } - - // ========================= - // Bullet + Indent helpers - // ========================= - - private static String bodiesToDbg(List bodies) { - if (bodies == null) - return "null"; - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < bodies.size(); i++) { - TextDataBody b = bodies.get(i); - if (i > 0) - sb.append(", "); - if (b == null) { - sb.append("null"); - } else { - sb.append("'").append(vis(b.text())).append("'"); - } - } - sb.append("]"); - return sb.toString(); - } - - private static int countLeadingWhitespace(String s) { - int i = 0; - while (i < s.length()) { - char c = s.charAt(i); - if (c == ' ' || c == ' ') { - i++; - } else { - break; - } - } - return i; - } - - public BlockTextBuilder text(TextBuilder textBuilder) { - var rowText = textBuilder.build(); - - var style = rowText.getComponent(TextStyle.class).orElse(textStyle.DEFAULT_STYLE); - this.textStyle = style; - this.lines = new ArrayList<>(); - - var boundingBox = InnerBoxSize.from(this.entity).orElseThrow(); - BlockTextData blockTextData = breakLines(rowText, boundingBox); - - addComponent(blockTextData); - addComponent(textStyle); - return this; - } - - public BlockTextBuilder text(List text, - TextStyle style, - Padding padding, - Margin margin) { - this.textStyle = style; - this.lines = new ArrayList<>(); - this.baseComponents = new HashMap<>(); - - this.baseComponents.put(Padding.class, padding); - this.baseComponents.put(Margin.class, margin); - this.baseComponents.put(TextStyle.class, style); - - var boundingBox = InnerBoxSize.from(this.entity).orElseThrow(); - BlockTextData blockTextData = breakLinesFromList(text, boundingBox, bulletOffset == null ? "" : bulletOffset); - - addComponent(blockTextData); - addComponent(textStyle); - return this; - } - public BlockTextBuilder bulletOffset(String bulletOffset){ - this.bulletOffset = bulletOffset; - return this; - } - - // ========================= - // Tokenize + Wrap Helpers - // ========================= - - public BlockTextData breakLines(@NonNull Entity entity, @NonNull InnerBoxSize innerBoxSize) { - // Early exit if not a block of text - if (!entity.hasAssignable(TextComponent.class)) { - log.debug("Entity doesn't have BlockText component"); - return new BlockTextData(lines, (float) lineSpacing); - } - - var text = entity.getComponent(Text.class) - .orElseThrow(() -> new IllegalStateException("Missing Text component")); - - var style = entity.getComponent(TextStyle.class).orElse(textStyle); - this.textStyle = style; - - // Copy components (excluding those that are irrelevant for line entities) - var components = new HashMap<>(entity.view()); - components.remove(Text.class); - components.remove(TextComponent.class); - components.remove(ContentSize.class); - components.remove(Padding.class); - - this.baseComponents = new HashMap<>(components); - - // Replace original entity with per-line children - entityManager.remove(entity); - - return breakLinesFromList(List.of(text.value()), innerBoxSize, ""); - } - - public BlockTextData breakLinesFromList(@NonNull List text, - @NonNull InnerBoxSize innerBoxSize, - String bulletOffset) { - - final TextMeasurementSystem measurementSystem = textMeasurementSystem(); - final TextStyle style = measurementStyle(); - - Margin margin = (Margin) baseComponents.getOrDefault(Margin.class, Margin.zero()); - margin = margin == null ? Margin.zero() : margin; - - final double maxWidth = innerBoxSize.width(); - final double horizontalMargins = margin.horizontal(); - - final BulletSpec bullet = BulletSpec.from(bulletOffset, measurementSystem, style); - - log.debug( - "breakLinesFromList | markdown={} | listSize={} | maxWidth={} | hMargins={} | bulletPrefix='{}' | softIndent='{}' | hardIndent='{}'", - entityManager.isMarkdown(), text.size(), maxWidth, horizontalMargins, - vis(bullet.prefix()), vis(bullet.softWrapIndent()), vis(bullet.hardWrapIndent())); - - for (String inputLine : text) { - // Bullet/indent are handled as separate tokens (NOT concatenated into the - // markdown input). - String normalizedText = normalizeNewlines(inputLine); - String[] explicitLines = splitExplicitLines(normalizedText); - - for (int lineIndex = 0; lineIndex < explicitLines.length; lineIndex++) { - String explicitLine = TextSanitizer.sanitize(explicitLines[lineIndex]); - - List tokens = tokenizeExplicitLine(explicitLine, lineIndex, bullet, style); - if (tokens.isEmpty()) { - continue; - } - - // Soft wrap indent differs from explicit "\n" continuation indent. - // Apply strategy for wrapped lines - String wrapIndent = ""; - if (blockStrategy.indentWrappedLines()) { - wrapIndent = (lineIndex == 0) ? bullet.softWrapIndent() : bullet.hardWrapIndent(); - } - - wrapTokensIntoLines(tokens, measurementSystem, maxWidth, wrapIndent, style); - } - } - - return new BlockTextData(lines, (float) lineSpacing); - } - - private String normalizeNewlines(String text) { - // Handle both actual newlines and escaped sequences (literal backslash + n) - if (text == null) - return ""; - String normalized = text.replace("\\n", "\n").replace("\\r", "\r"); - log.debug("AFTER NORMALIZE normalizedText: '{}'", normalized); - return normalized; - } - - private String[] splitExplicitLines(String normalizedText) { - String[] lines = normalizedText.split("\\r?\\n"); - log.debug("SPLIT into {} lines: {}", lines.length, Arrays.toString(lines)); - return lines; - } - - private List tokenizeExplicitLine(String explicitLine, - int explicitLineIndex, - BulletSpec bullet, - TextStyle style) { - log.debug("Processing line {}: '{}'", explicitLineIndex, explicitLine); - - if (explicitLine == null || explicitLine.isBlank()) { - return List.of(); - } - - boolean isMarkdown = entityManager.isMarkdown(); - - if (isMarkdown) { - // Render bullet/indent as plain TextDataBody tokens, parse the rest as inline - // markdown. - List bodies = tokenizeMarkdownLine(explicitLine, explicitLineIndex, bullet, style); - - log.debug("Markdown tokensCount={} | explicitIndex={} | rawLine='{}' | tokens={}", - bodies.size(), explicitLineIndex, vis(explicitLine), bodiesToDbg(bodies)); - - if (bodies.isEmpty()) { - log.warn( - "Markdown produced 0 tokens for non-empty line | explicitIndex={} | line='{}' | hardIndent='{}'", - explicitLineIndex, vis(explicitLine), vis(bullet == null ? "" : bullet.hardWrapIndent())); - } - - return bodies; - } - - // Non-markdown: split into words + spaces - String[] words = explicitLine.split("\\s+"); - List tokens = new ArrayList<>(); - - // Apply strategy for first line indent - if (explicitLineIndex == 0) { - if (blockStrategy.indentFirstLine()) { - appendPrefixBodies(tokens, bullet == null ? "" : bullet.prefix(), style); - } - } else { - // For subsequent explicit lines, apply strategy for wrapped lines - if (blockStrategy.indentWrappedLines()) { - appendPrefixBodies(tokens, bullet == null ? "" : bullet.hardWrapIndent(), style); - } - } - - for (int i = 0; i < words.length; i++) { - if (words[i].isEmpty()) - continue; - - tokens.add(new TextDataBody(words[i], style)); - - if (i < words.length - 1) { - tokens.add(new TextDataBody(" ", style)); - } - } - - return tokens; - } - - /** - * Wraps tokens into lines based on the maximum width. - * Handles "sticky" punctuation to prevent orphaned brackets. - */ - private void wrapTokensIntoLines(List tokens, - TextMeasurementSystem measurementSystem, - double maxWidth, - String offsetStr, - TextStyle baseStyle) { - - if (tokens == null || tokens.isEmpty()) - return; - - // Find the style of the first non-blank token (for accurate space width - // calculation) - TextStyle styleForIndent = baseStyle; - for (TextDataBody t : tokens) { - if (t.text() != null && !t.text().isBlank()) { - styleForIndent = t.textStyle(); - break; - } - } - - // Measure indent reliably (10 spaces => 10 * spaceWidth) - double indentWidth = 0; - if (!offsetStr.isEmpty() && offsetStr.isBlank()) { - double spaceW = measurementSystem.textWidth(styleForIndent, " "); - indentWidth = spaceW * offsetStr.length(); - } else if (!offsetStr.isEmpty()) { - indentWidth = measurementSystem.textWidth(styleForIndent, offsetStr); - } - - Deque line = new ArrayDeque<>(); - double lineWidth = 0; - - for (int i = 0; i < tokens.size(); i++) { - TextDataBody token = tokens.get(i); - String text = token.text(); - - boolean isIndentToken = !offsetStr.isEmpty() && text.equals(offsetStr); - - // Skip leading blanks, BUT keep the indent token AND the first token (bullet - // prefix) - boolean isFirstToken = (i == 0); - if (line.isEmpty() && text.isBlank() && !isIndentToken && !isFirstToken) { - continue; - } - - double tokenWidth = isIndentToken - ? indentWidth - : measurementSystem.textWidth(token.textStyle(), text); - - boolean stickyToNext = isStickyToNext(text); - boolean stickyToPrev = isSticky(text); - - // Check if fits - if (lineWidth + tokenWidth <= maxWidth) { - // It fits. But if it's sticky to next, check if next fits too. - boolean forceWrap = false; - if (stickyToNext && i + 1 < tokens.size()) { - TextDataBody nextToken = tokens.get(i + 1); - double nextWidth = measurementSystem.textWidth(nextToken.textStyle(), nextToken.text()); - - // If adding the next token would exceed the width, we force a wrap NOW - // so that the current token (e.g. "(") moves to the next line together with the - // next token. - if (lineWidth + tokenWidth + nextWidth > maxWidth) { - forceWrap = true; - } - } - - if (!forceWrap) { - line.addLast(token); - lineWidth += tokenWidth; - continue; - } - } - - // Doesn't fit OR forceWrap - if (text.isBlank() && !isIndentToken) { - continue; - } - - // If sticky to previous, try to bring previous token to the new line - if (stickyToPrev && !line.isEmpty()) { - TextDataBody prev = line.removeLast(); - double previousWidth = measuredTokenWidth(prev, offsetStr, indentWidth, measurementSystem); - lineWidth -= previousWidth; - // Flush the line without 'prev' - if (!line.isEmpty()) { - boolean hasContent = false; - for (TextDataBody b : line) { - boolean isIndent = !offsetStr.isEmpty() && b.text().equals(offsetStr); - if (!isIndent && !b.text().isBlank()) { - hasContent = true; - break; - } - } - - if (hasContent) { - createLineFromBodies(new ArrayList<>(line), lineWidth, baseStyle); - } - } - line.clear(); - lineWidth = 0; - - // Add indent for wrapped lines - if (!offsetStr.isEmpty()) { - line.addLast(new TextDataBody(offsetStr, baseStyle)); - lineWidth += indentWidth; - } - - // Add 'prev' to new line - line.addLast(prev); - lineWidth += previousWidth; - - // Add current token - line.addLast(token); - lineWidth += tokenWidth; - } else { - // Normal wrap - if (!line.isEmpty()) { - createLineFromBodies(new ArrayList<>(line), lineWidth, baseStyle); - line.clear(); - lineWidth = 0; - - // add indent for wrapped lines - if (!offsetStr.isEmpty()) { - line.addLast(new TextDataBody(offsetStr, baseStyle)); - lineWidth += indentWidth; - } - } - - line.addLast(token); - lineWidth += tokenWidth; - } - } - - if (!line.isEmpty()) { - createLineFromBodies(new ArrayList<>(line), lineWidth, baseStyle); - } - } - - /** - * Returns the measured width of a token, using a precomputed indent width for - * indent-only tokens and the measurement system for all other tokens. - * - * @param token token to measure - * @param offsetStr current wrap-indent string (empty when no indent is active) - * @param indentWidth precomputed pixel width of the indent string - * @param measurementSystem active text measurement system - * @return measured width in layout units, or {@code 0.0} if the token is {@code null} - */ - private double measuredTokenWidth(TextDataBody token, - String offsetStr, - double indentWidth, - TextMeasurementSystem measurementSystem) { - if (token == null) { - return 0.0; - } - - boolean isIndentToken = !offsetStr.isEmpty() && offsetStr.equals(token.text()); - return isIndentToken - ? indentWidth - : measurementSystem.textWidth(token.textStyle(), token.text()); - } - - // ========================= - // Line Factories - // ========================= - - /** - * Determines if the token should "stick" to the previous token. - * For example, closing brackets ')' or punctuation ',' should not be separated - * from the word preceding them. - */ - private boolean isSticky(String text) { - if (text == null || text.isEmpty()) - return false; - char first = text.charAt(0); - return ",.;:!?)]}".indexOf(first) >= 0; - } - - /** - * Determines if the token should "stick" to the next token. - * For example, opening brackets '(' should not be separated from the word - * following them. - */ - private boolean isStickyToNext(String text) { - if (text == null || text.isEmpty()) - return false; - char last = text.charAt(text.length() - 1); - return "([{".indexOf(last) >= 0; - } - - private TextMeasurementSystem textMeasurementSystem() { - return entityManager.getSystems() - .textMeasurement() - .orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to build block text.")); - } - - private TextStyle measurementStyle() { - if (baseComponents != null) { - TextStyle style = (TextStyle) baseComponents.get(TextStyle.class); - if (style != null) { - return style; - } - } - return textStyle == null ? TextStyle.DEFAULT_STYLE : textStyle; - } - - /** - * Creates a new {@link LineTextData} from the accumulated token bodies, caching - * the measured line width and resolved line metrics so that layout, alignment, - * and pagination can reuse them without re-measuring. - * - * @param bodies ordered text bodies forming one visual line - * @param lineWidth precomputed total width of the line in layout units - * @param fallbackStyle style used to resolve metrics for bodies without explicit styles - */ - private void createLineFromBodies(List bodies, - double lineWidth, - TextStyle fallbackStyle) { - log.debug("createLineFromBodies: {}", bodies); - TextMeasurementSystem.LineMetrics lineMetrics = - BlockTextLineMetrics.resolveBodiesMetrics(entityManager, bodies, fallbackStyle); - lines.add(new LineTextData( - bodies, - 0, - lineWidth, - lineMetrics, - lineMetrics.baselineOffsetFromBottom())); - } - - @Override - public void initialize() { - entity.addComponent(new BlockText()); - } - - public BlockTextBuilder align(Align align) { - HAnchor h = align.h(); - if (HAnchor.LEFT != h && HAnchor.RIGHT != h && HAnchor.CENTER != h) { - log.info("Align has to be HAnchor.LEFT or HAnchor.RIGHT current {}", align); - throw new IllegalStateException("Align has to be HAnchor.LEFT or HAnchor.RIGHT in BlockText"); - } - entity.addComponent(align); - return self(); - } - - private ContentSize computeContentSize() { - Padding padding = entity.getComponent(Padding.class).orElse(Padding.zero()); - var blockTextData = entity.getComponent(BlockTextData.class).orElseThrow(); - - if (blockTextData.lines().isEmpty()) { - return new ContentSize(padding.horizontal(), padding.vertical()); - } - - TextStyle style = entity.getComponent(TextStyle.class).orElse(TextStyle.DEFAULT_STYLE); - TextMeasurementSystem measurementSystem = textMeasurementSystem(); - - var width = blockTextData.lines().stream() - .mapToDouble(line -> line.hasCachedLineWidth() - ? line.lineWidth() - : line.width(measurementSystem, style)) - .max() - .orElse(0.0); - - var spacingOpt = entity.getComponent(Align.class); - double spacing = spacingOpt.orElse(Align.defaultAlign(0.0)).spacing(); - - TextMeasurementSystem.LineMetrics baseMetrics = - BlockTextLineMetrics.resolveStyleMetrics(entityManager, style); - List lineMetrics = blockTextData.lines().stream() - .map(line -> BlockTextLineMetrics.resolveLineMetrics(entityManager, line, style)) - .toList(); - - double high = padding.vertical(); - for (int i = 0; i < lineMetrics.size(); i++) { - high += lineMetrics.get(i).lineHeight(); - if (i < lineMetrics.size() - 1) { - high += BlockTextLineMetrics.interLineGap( - lineMetrics.get(i), - lineMetrics.get(i + 1), - baseMetrics, - spacing); - } - } - - return new ContentSize(width + padding.horizontal(), high); - } - - // ========================= - // Debug helpers (no logic) - // ========================= - - @Override - public Entity build() { - ContentSize contentSize = computeContentSize(); - entity.addComponent(contentSize); - return registerBuiltEntity(); - } - - private List tokenizeMarkdownLine(String explicitLine, - int explicitLineIndex, - BulletSpec bullet, - TextStyle style) { - if (explicitLine == null || explicitLine.isBlank()) { - return List.of(); - } - - List bodies = new ArrayList<>(); - - // Bullet/first-line indent ONLY on the first explicit line - if (explicitLineIndex == 0) { - if (blockStrategy.indentFirstLine()) { - appendPrefixBodies(bodies, bullet == null ? "" : bullet.prefix(), style); - } - } - - // Continuation indent for lines after explicit "\n" - String continuationIndent = ""; - if (explicitLineIndex > 0 && bullet != null && blockStrategy.indentWrappedLines()) { - continuationIndent = bullet.hardWrapIndent(); - } - - // Remove leading whitespace before parsing markdown (4 spaces / tab becomes a - // code block) - int leadWs = countLeadingWhitespace(explicitLine); - String leadingWs = explicitLine.substring(0, leadWs); - String rest = explicitLine.substring(leadWs); - - String indent = continuationIndent + leadingWs; - if (!indent.isEmpty()) { - bodies.add(new TextDataBody(indent, style)); - } - - // Parse the rest as inline markdown (with list-marker fallback) - bodies.addAll(tokenizeMarkdownInline(rest, style)); - return bodies; - } - - /** - * Inline markdown tokenization with a safety fallback for list markers. - * Many markdown parsers consider lines starting with "- ", "* ", "+ " to be - * LIST blocks. - * When parsing line-by-line (inline), they may return 0 tokens. - * We preserve the marker as plain text and parse the rest. - */ - private List tokenizeMarkdownInline(String lineToParse, TextStyle style) { - if (lineToParse == null || lineToParse.isBlank()) { - return List.of(); - } - - // Detect a markdown list marker at the beginning (after leading spaces) - int i = 0; - while (i < lineToParse.length() && lineToParse.charAt(i) == ' ') { - i++; - } - - if (i + 1 < lineToParse.length()) { - char c = lineToParse.charAt(i); - boolean marker = (c == '-' || c == '*' || c == '+'); - boolean hasSpaceAfter = (i + 1 < lineToParse.length() && lineToParse.charAt(i + 1) == ' '); - - if (marker && hasSpaceAfter) { - // IMPORTANT: don't keep the trailing space inside the same token as the marker. - // Some width-calculators trim/ignore trailing spaces, which visually produces - // "-Honed". - // Split marker and space into separate tokens. - String leading = lineToParse.substring(0, i); // spaces before marker - String rest = lineToParse.substring(i + 2); - - List bodies = new ArrayList<>(); - if (!leading.isEmpty()) { - bodies.add(new TextDataBody(leading, style)); - } - - bodies.add(new TextDataBody(String.valueOf(lineToParse.charAt(i)), style)); // '-', '*', '+' - bodies.add(new TextDataBody(" ", style)); - bodies.addAll(markDownParser.getBody(rest, style)); - return bodies; - } - } - - // Default: parse entire line as inline markdown - return markDownParser.getBody(lineToParse, style); - } - - /** - * bulletPrefix: - * - if contains visible chars (e.g. "-", "•", "==="), we render it ONLY on the - * first explicit line - * and compute indent width based on font metrics. - * - if whitespace-only (e.g. " "), we treat it as a plain first-line indent and - * keep the classic - * hanging indent for explicit "\n" continuation lines. - *

- * softWrapIndent: used when a single line wraps by width. - * hardWrapIndent: used for explicit "\n" continuation lines (lineIndex > 0). - */ - private record BulletSpec(String prefix, String softWrapIndent, String hardWrapIndent) { - - static BulletSpec from(String bulletOffset, TextMeasurementSystem measurementSystem, TextStyle style) { - String raw = bulletOffset == null ? "" : bulletOffset; - - boolean hasVisibleChars = raw.chars().anyMatch(ch -> !Character.isWhitespace(ch)); - - if (hasVisibleChars) { - String prefix = normalizeBulletPrefix(raw); - String indent = computeIndentFromPrefix(measurementSystem, style, prefix); - - // For visible bullets, both soft-wrap and explicit "\n" continuation should - // align under the content. - return new BulletSpec(prefix, indent, indent); - } - - // Whitespace-only: treat as a FIXED left indent for all lines. - // This is what you want for paragraphs like "Projects" where there is no - // visible bullet. - // (If you want a true hanging indent, pass a visible bullet prefix like "- ", - // "• ", "=== ", etc.) - String prefix = raw; - return new BulletSpec(prefix, prefix, prefix); - } - } -} - diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilderTest.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilderTest.java deleted file mode 100644 index 3a2d1ccb2..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilderTest.java +++ /dev/null @@ -1,209 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.font.FontName; -import com.demcha.compose.engine.components.content.text.BlockTextData; -import com.demcha.compose.engine.components.content.text.BlockTextLineMetrics; -import com.demcha.compose.engine.components.content.text.LineTextData; -import com.demcha.compose.engine.components.content.text.TextDecoration; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.render.pdf.PdfFont; -import com.demcha.compose.engine.measurement.TextMeasurementSystem; -import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory; -import com.demcha.compose.engine.measurement.FontLibraryTextMeasurementSystem; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -class BlockTextBuilderTest { - - private EntityManager entityManager; - - @BeforeEach - void setUp() { - entityManager = new EntityManager(PdfFontLibraryFactory.standardLibrary(), false); - entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class)); - entityManager.setMarkdown(true); - } - - @Test - void testComplexMarkdownText() { - String text = "**Lead Engineer**, Nikoplast, Odessa, Ukraine | *Sep 2016 – Feb 2022*\n" + - "- Developed strong leadership and mentoring skills, guiding a team of 8 and acting as a key point of contact for management, effectively **communicating with senior non-technical staff**.\n" - + - "- Championed process improvements and maintained rigorous quality protocols, demonstrating a pragmatic approach to problem-solving and delivery excellence."; - - TextStyle textStyle = TextStyle.builder() - .size(10) - .color(ComponentColor.BLACK) - .fontName(FontName.HELVETICA) - .decoration(TextDecoration.DEFAULT) - .build(); - - BlockTextBuilder builder = new BlockTextBuilder(entityManager, Align.left(1.0), textStyle); - builder.size(new ContentSize(500, 100)); // Set a reasonable width - builder.padding(Padding.zero()); - builder.margin(Margin.zero()); - builder.anchor(Anchor.topLeft()); - - // Use breakLinesFromList directly or via text() method if available for list - // Since the input is a single string with newlines, we can split it or pass as - // list - List textList = List.of(text); - - builder.text(textList, textStyle, Padding.zero(), Margin.zero()); - Entity entity = builder.build(); - - assertTrue(entity.has(BlockTextData.class)); - BlockTextData blockTextData = entity.getComponent(BlockTextData.class).orElseThrow(); - - assertFalse(blockTextData.lines().isEmpty(), "BlockTextData should contain lines"); - - // Basic validation of content structure - // We expect multiple lines due to wrapping and newlines in source text - assertTrue(blockTextData.lines().size() > 1, "Should have multiple lines"); - - // Verify that bold/italic parsing happened (checking if we have different - // styles in the first line) - // The first line starts with "**Lead Engineer**", so the first segment should - // be bold. - var firstLine = blockTextData.lines().get(0); - assertFalse(firstLine.bodies().isEmpty()); - - // Note: Exact verification depends on how MarkDownParser splits tokens and how - // line breaking works with the font. - // But we can check if we have at least some bold text. - boolean hasBold = blockTextData.lines().stream() - .flatMap(line -> line.bodies().stream()) - .anyMatch(body -> body.textStyle().decoration() == TextDecoration.BOLD - || body.textStyle().decoration() == TextDecoration.BOLD_ITALIC); - - assertTrue(hasBold, "Should contain bold text segments"); - - boolean hasItalic = blockTextData.lines().stream() - .flatMap(line -> line.bodies().stream()) - .anyMatch(body -> body.textStyle().decoration() == TextDecoration.ITALIC - || body.textStyle().decoration() == TextDecoration.BOLD_ITALIC); - - assertTrue(hasItalic, "Should contain italic text segments"); - } - - @Test - void shouldIncreasePrecomputedHeightWhenMarkdownContainsHeadingLine() { - TextStyle textStyle = TextStyle.builder() - .size(10) - .color(ComponentColor.BLACK) - .fontName(FontName.HELVETICA) - .decoration(TextDecoration.DEFAULT) - .build(); - - BlockTextBuilder plainBuilder = new BlockTextBuilder(entityManager, Align.left(2.0), textStyle); - plainBuilder.size(new ContentSize(320, 100)); - plainBuilder.padding(Padding.zero()); - plainBuilder.margin(Margin.zero()); - plainBuilder.anchor(Anchor.topLeft()); - plainBuilder.text(List.of("First line\nSecond line\nThird line"), textStyle, Padding.zero(), Margin.zero()); - Entity plainEntity = plainBuilder.build(); - - BlockTextBuilder headingBuilder = new BlockTextBuilder(entityManager, Align.left(2.0), textStyle); - headingBuilder.size(new ContentSize(320, 100)); - headingBuilder.padding(Padding.zero()); - headingBuilder.margin(Margin.zero()); - headingBuilder.anchor(Anchor.topLeft()); - headingBuilder.text(List.of("First line\n# Section heading\nThird line"), textStyle, Padding.zero(), Margin.zero()); - Entity headingEntity = headingBuilder.build(); - - double plainHeight = plainEntity.getComponent(ContentSize.class).orElseThrow().height(); - double headingHeight = headingEntity.getComponent(ContentSize.class).orElseThrow().height(); - BlockTextData headingBlockTextData = headingEntity.getComponent(BlockTextData.class).orElseThrow(); - - assertTrue(headingHeight > plainHeight, - "Heading line should enlarge ContentSize before rendering so containers reserve more height"); - assertCachedMeasurementsPresent(headingBlockTextData); - assertTrue(headingBlockTextData.lines().get(1).lineMetrics().lineHeight() - > headingBlockTextData.lines().get(0).lineMetrics().lineHeight(), - "Heading line should carry taller cached metrics than the body lines"); - assertContentSizeMatchesCachedMeasurements(headingEntity, headingBlockTextData); - } - - @Test - void shouldBuildBlockTextWithoutLayoutSystemWhenMeasurementSystemIsRegistered() { - TextStyle textStyle = TextStyle.builder() - .size(10) - .color(ComponentColor.BLACK) - .fontName(FontName.HELVETICA) - .decoration(TextDecoration.DEFAULT) - .build(); - - Entity entity = new BlockTextBuilder(entityManager, Align.left(2.0), textStyle) - .size(new ContentSize(320, 100)) - .padding(Padding.zero()) - .margin(Margin.zero()) - .anchor(Anchor.topLeft()) - .text(List.of("Line one\n# Heading\nLine three"), textStyle, Padding.zero(), Margin.zero()) - .build(); - - assertTrue(entity.getComponent(BlockTextData.class).isPresent()); - assertTrue(entity.getComponent(ContentSize.class).isPresent()); - - BlockTextData blockTextData = entity.getComponent(BlockTextData.class).orElseThrow(); - assertCachedMeasurementsPresent(blockTextData); - assertContentSizeMatchesCachedMeasurements(entity, blockTextData); - } - - private void assertCachedMeasurementsPresent(BlockTextData blockTextData) { - assertFalse(blockTextData.lines().isEmpty(), "Expected cached lines to be present"); - - for (LineTextData line : blockTextData.lines()) { - assertTrue(line.hasCachedLineWidth(), "Line width cache should be populated"); - assertTrue(line.hasCachedLineMetrics(), "Line metrics cache should be populated"); - assertTrue(line.hasCachedBaselineOffset(), "Baseline cache should be populated"); - assertEquals(line.lineMetrics().baselineOffsetFromBottom(), line.baselineOffset(), 0.0001, - "Cached baseline should match the cached line metrics"); - } - } - - private void assertContentSizeMatchesCachedMeasurements(Entity entity, BlockTextData blockTextData) { - ContentSize contentSize = entity.getComponent(ContentSize.class).orElseThrow(); - Padding padding = entity.getComponent(Padding.class).orElse(Padding.zero()); - Align align = entity.getComponent(Align.class).orElse(Align.defaultAlign(0.0)); - TextStyle style = entity.getComponent(TextStyle.class).orElse(TextStyle.DEFAULT_STYLE); - - double expectedWidth = blockTextData.lines().stream() - .mapToDouble(LineTextData::lineWidth) - .max() - .orElse(0.0) + padding.horizontal(); - - TextMeasurementSystem.LineMetrics baseMetrics = - BlockTextLineMetrics.resolveStyleMetrics(entityManager, style); - - double expectedHeight = padding.vertical(); - List lines = blockTextData.lines(); - for (int i = 0; i < lines.size(); i++) { - TextMeasurementSystem.LineMetrics metrics = lines.get(i).lineMetrics(); - expectedHeight += metrics.lineHeight(); - if (i < lines.size() - 1) { - expectedHeight += BlockTextLineMetrics.interLineGap( - metrics, - lines.get(i + 1).lineMetrics(), - baseMetrics, - align.spacing()); - } - } - - assertEquals(expectedWidth, contentSize.width(), 0.001, - "Content width should be derived from cached line widths"); - assertEquals(expectedHeight, contentSize.height(), 0.001, - "Content height should be derived from cached line metrics"); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/CircleBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/CircleBuilder.java deleted file mode 100644 index 7a47b85e0..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/CircleBuilder.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.EmptyBox; -import com.demcha.compose.engine.components.content.shape.FillColor; -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.renderable.Circle; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.core.EntityManager; - -import java.awt.Color; - -public class CircleBuilder extends EmptyBox { - CircleBuilder(EntityManager entityManager) { - super(entityManager); - } - - public CircleBuilder circle(Circle circle) { - return addComponent(circle); - } - - public CircleBuilder fillColor(FillColor fillColor) { - return addComponent(fillColor); - } - - public CircleBuilder fillColor(Color fillColor) { - return fillColor(new FillColor(fillColor)); - } - - public CircleBuilder fillColor(ComponentColor fillColor) { - return fillColor(new FillColor(fillColor)); - } - - public CircleBuilder fillColor(int r, int g, int b) { - return fillColor(new FillColor(new Color(r, g, b))); - } - - public CircleBuilder stroke(Stroke stroke) { - return addComponent(stroke); - } - - @Override - public void initialize() { - entity.addComponent(new Circle()); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ComponentBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ComponentBuilder.java deleted file mode 100644 index a650839bb..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ComponentBuilder.java +++ /dev/null @@ -1,232 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.BuildEntity; -import com.demcha.compose.engine.components.content.text.TextIndentStrategy; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.core.EntityManager; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * Root builder facade for creating document entities. - *

- * {@code ComponentBuilder} is the public entry to the engine-level builder - * layer. Each factory method returns a concrete builder for one entity family, - * such as text, containers, images, lines, or tables. The concrete builder - * attaches components to a new {@code Entity}; calling {@code build()} then - * registers that entity in the shared {@code EntityManager}. - *

- * - *

Use this type when you want direct control over the engine rather than the - * higher-level template layer.

- */ -public class ComponentBuilder { - private final List builders = new ArrayList<>(); - private final EntityManager entityManager; - - private ComponentBuilder(EntityManager entityManager) { - this.entityManager = Objects.requireNonNull(entityManager, "entityManager"); - } - - private T register(T builder) { - builders.add(builder); - return builder; - } - - /** - * Creates a builder for multi-line block text with wrapping and optional - * markdown-aware tokenization. - * - *

The resulting entity is a breakable text leaf whose content is later - * processed by layout and pagination systems.

- */ - public BlockTextBuilder blockText(Align align, TextStyle textStyle) { - return register(new BlockTextBuilder(entityManager, align, textStyle)); - } - - /** - * Creates a block-text builder preconfigured for paragraph-like indentation. - * - *

This convenience method applies a bullet offset and first-line indent - * strategy before returning the builder.

- */ - public BlockTextBuilder blockTextParagraph(Align align, TextStyle textStyle, String bulletOffset) { - BlockTextBuilder blockTextBuilder = register(new BlockTextBuilder(entityManager, align, textStyle)); - blockTextBuilder.bulletOffset(bulletOffset) - .strategy(TextIndentStrategy.FIRST_LINE); - return blockTextBuilder; - } - - /** - * Creates a builder for a circle leaf entity. - */ - public CircleBuilder circle() { - return register(new CircleBuilder(entityManager)); - } - - /** - * Creates a text builder specialized for user-facing URL display text. - */ - public DisplayUrlTextBuilder displayUrlText() { - return register(new DisplayUrlTextBuilder(entityManager)); - } - - /** - * Creates a builder for a clickable link entity. - */ - public LinkBuilder link() { - return register(new LinkBuilder(entityManager)); - } - - /** - * Creates a builder for a fixed-size line primitive. - */ - public LineBuilder line() { - return register(new LineBuilder(entityManager)); - } - - /** - * Creates a semantic section module aligned within its parent according to - * the supplied {@link Align}. - * - *

Modules resolve to the full available width of their parent minus their - * own horizontal margins, stack children vertically, and grow primarily in - * height as content is added.

- */ - public ModuleBuilder moduleBuilder(Align align) { - return register(new ModuleBuilder(entityManager, align)); - } - - /** - * Creates a semantic section module with a root-width seed based on a canvas - * reference area. - */ - public ModuleBuilder moduleBuilder(Align align, Canvas canvas) { - return register(new ModuleBuilder(entityManager, align, canvas)); - } - - /** - * Creates a semantic section module with an explicit root-width seed. - */ - public ModuleBuilder moduleBuilder(Align align, ContentSize contentSize) { - return register(new ModuleBuilder(entityManager, align, contentSize)); - } - - /** - * Creates a semantic section module with a root-width seed based on a - * precomputed inner box. - */ - public ModuleBuilder moduleBuilder(Align align, InnerBoxSize innerBoxSize) { - return register(new ModuleBuilder(entityManager, align, innerBoxSize)); - } - - /** - * Creates a builder for a rectangle leaf entity. - */ - public RectangleBuilder rectangle() { - return register(new RectangleBuilder(entityManager)); - } - - /** - * Creates a builder for a barcode or QR-code leaf entity. - * - *

The resulting entity renders a barcode image at draw time using - * ZXing. Like images, barcodes are fixed-size leaves positioned by - * anchor and sized explicitly.

- */ - public BarcodeBuilder barcode() { - return register(new BarcodeBuilder(entityManager)); - } - - /** - * Creates a builder for an image leaf entity. - */ - public ImageBuilder image() { - return register(new ImageBuilder(entityManager)); - } - - /** - * Creates a builder for single-line text or fixed text leaves. - */ - public TextBuilder text() { - return register(new TextBuilder(entityManager)); - } - - /** - * Creates a container builder that arranges children as a row-like group. - */ - public RowBuilder row(Align align) { - return register(new RowBuilder(entityManager, align)); - } - - /** - * Creates a horizontal container builder. - */ - public HContainerBuilder hContainer(Align align) { - return register(new HContainerBuilder(entityManager, align)); - } - - /** - * Creates a vertical container builder. - */ - public VContainerBuilder vContainer(Align align) { - return register(new VContainerBuilder(entityManager, align)); - } - - /** - * Creates the engine-level table builder. - * - *

Tables materialize as a breakable vertical root with row-atomic leaf - * entities, which keeps pagination behavior consistent with the rest of the engine.

- */ - public TableBuilder table() { - return register(new TableBuilder(entityManager)); - } - - /** - * Creates a generic element builder for low-level custom composition. - */ - public ElementBuilder element() { - return register(new ElementBuilder(entityManager)); - } - - /** - * Creates a forced page-break entity. - * - *

When this entity is encountered during pagination, the layout system - * advances to the next page.

- */ - public PageBreakBuilder pageBreak() { - return register(new PageBreakBuilder(entityManager)); - } - - /** - * Creates a horizontal divider line with sensible defaults. - * - *

This is a convenience wrapper around the line primitive. Use - * {@code divider().width(520).thickness(1)} to customize appearance.

- */ - public DividerBuilder divider() { - return register(new DividerBuilder(entityManager)); - } - - /** - * Builds every registered builder that has not yet been materialized. - * - *

This is primarily an orchestration helper used by composer internals to - * ensure deferred builders are registered before layout begins.

- */ - public void buildsComponents() { - if (builders.isEmpty()) - return; - builders.stream() - .filter(builder -> !builder.built()) - .forEach(BuildEntity::build); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/DisplayUrlTextBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/DisplayUrlTextBuilder.java deleted file mode 100644 index 6ba0b1066..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/DisplayUrlTextBuilder.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.engine.components.content.link.DisplayText; -import com.demcha.compose.engine.core.EntityManager; - -public class DisplayUrlTextBuilder extends TextBuilder{ - - DisplayUrlTextBuilder(EntityManager entityManager) { - super(entityManager); - } - - @Override - public void initialize() { - entity.addComponent(new DisplayText()); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/DividerBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/DividerBuilder.java deleted file mode 100644 index 3d18e19bc..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/DividerBuilder.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.EmptyBox; -import com.demcha.compose.engine.components.content.shape.LinePath; -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.content.shape.StrokeColor; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.renderable.Line; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import lombok.extern.slf4j.Slf4j; - -import java.awt.Color; - -/** - * Convenience builder for a horizontal divider line. - * - *

A divider is a thin horizontal {@link Line} entity with sensible - * defaults for color, thickness, and vertical spacing. It is a convenience - * wrapper rather than a new entity type.

- * - *

Usage

- *
- * cb.divider()
- *     .width(520)
- *     .thickness(1)
- *     .color(ComponentColor.LIGHT_GRAY)
- *     .verticalSpacing(12)
- *     .build();
- * 
- * - * @author Artem Demchyshyn - */ -@Slf4j -public class DividerBuilder extends EmptyBox { - - private double width = 0; - private double thickness = 1; - private Color color = ComponentColor.LIGHT_GRAY; - private double verticalSpacing = 8; - - DividerBuilder(EntityManager entityManager) { - super(entityManager); - } - - /** - * Sets the width of the divider line in points. - * - * @param width line width - * @return this builder - */ - public DividerBuilder width(double width) { - this.width = width; - return this; - } - - /** - * Sets the line thickness (stroke width) in points. - * - * @param thickness stroke width - * @return this builder - */ - public DividerBuilder thickness(double thickness) { - this.thickness = thickness; - return this; - } - - /** - * Sets the divider color. - * - * @param color the line color - * @return this builder - */ - public DividerBuilder color(Color color) { - this.color = color; - return this; - } - - /** - * Sets the divider color from a {@link ComponentColor}. - */ - public DividerBuilder color(ComponentColor color) { - return color(color.color()); - } - - /** - * Sets vertical spacing (padding above and below the divider line). - * - * @param spacing spacing in points - * @return this builder - */ - public DividerBuilder verticalSpacing(double spacing) { - this.verticalSpacing = spacing; - return this; - } - - @Override - public void initialize() { - entity.addComponent(new Line()); - entity.addComponent(LinePath.horizontal()); - } - - @Override - public Entity build() { - double totalHeight = thickness + verticalSpacing * 2; - entity.addComponent(new ContentSize(width, totalHeight)); - entity.addComponent(new Stroke(new StrokeColor(color), thickness)); - entity.addComponent(new Padding(verticalSpacing, 0, verticalSpacing, 0)); - return registerBuiltEntity(); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ElementBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ElementBuilder.java deleted file mode 100644 index 0f6725f14..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ElementBuilder.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.EmptyBox; -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.coordinator.Position; -import com.demcha.compose.engine.components.renderable.Element; -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.core.EntityManager; - -import java.util.Optional; - - -public class ElementBuilder extends EmptyBox { - ElementBuilder(EntityManager entityManager) { - super(entityManager); - } - - @Override - public void initialize() { - entity().addComponent(new Element()); - } - - - public ElementBuilder fillPageSize(Canvas canvas) { - return fillPageSize(canvas.width(), canvas.height()); - } - - public ElementBuilder fillPageSize(double width, double height) { - float w = (float) width; - float h = (float) height; - // Store logical (CSS-like) top-left coordinates - addComponent(new ContentSize(w, h)); - addComponent(new Position(0, 0)); // top-left origin for your layout system - return this; - } - - public ElementBuilder fillHorizontal(Canvas canvas, double height) { - return fillHorizontal(canvas.width(), height); - } - - public ElementBuilder fillHorizontal(double width, double height) { - addComponent(new ContentSize(width, height)); - return this; - } - - public Optional getComponent(Class clazz) { - return entity.getComponent(clazz); - } - - -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/HContainerBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/HContainerBuilder.java deleted file mode 100644 index 88c32c20a..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/HContainerBuilder.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.ContainerBuilder; -import com.demcha.compose.engine.components.layout.StackAxis; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.renderable.HContainer; -import com.demcha.compose.engine.core.EntityManager; -import lombok.extern.slf4j.Slf4j; - -/** - * Builder class for creating horizontal containers ({@link HContainer}). - * This builder extends {@link ContainerBuilder} and specializes in arranging - * child entities horizontally, managing their positions and calculating the - * container's overall dimensions based on its children. - *

- * It uses Lombok's {@code @Slf4j} for logging. - *

- */ -@Slf4j -public class HContainerBuilder extends ContainerBuilder { - - /** - * Constructs a new {@code HContainerBuilder} associated with a specific Entity Manager. - * - * @param entityManager The {@link EntityManager} to which the container and its entities will belong. - */ - HContainerBuilder(EntityManager entityManager, Align align) { - super(entityManager, align); - } - - - /** - * Initializes the builder for creating a new horizontal container. - * This method calls the common creation logic from the superclass and then - * adds the specific {@link HContainer} component to the entity being built. - * - * @param align The alignment strategy to be used for arranging children within the container. - * @return This builder instance, allowing for method chaining. - */ - - @Override - public void initialize() { - entity.addComponent(new HContainer()); - entity.addComponent(StackAxis.HORIZONTAL); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ImageBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ImageBuilder.java deleted file mode 100644 index 852a1cdae..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ImageBuilder.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.EmptyBox; -import com.demcha.compose.engine.components.content.ImageData; -import com.demcha.compose.engine.components.content.ImageIntrinsicSize; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.renderable.ImageComponent; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.core.EntityManager; -import lombok.extern.slf4j.Slf4j; -import java.nio.file.Path; - -@Slf4j -public class ImageBuilder extends EmptyBox { - private Double scaleX; - private Double scaleY; - private Double fitWidth; - private Double fitHeight; - - ImageBuilder(EntityManager entityManager) { - super(entityManager); - } - - public ImageBuilder image(ImageData imageData) { - return addComponent(imageData); - } - - public ImageBuilder image(Path path) { - return image(ImageData.create(path)); - } - - public ImageBuilder image(String path) { - return image(ImageData.create(path)); - } - - public ImageBuilder image(byte[] bytes) { - return image(ImageData.create(bytes)); - } - - public ImageBuilder scale(double factor) { - validatePositive("scale", factor); - this.scaleX = factor; - this.scaleY = factor; - return this; - } - - public ImageBuilder scaleX(double factor) { - validatePositive("scaleX", factor); - this.scaleX = factor; - return this; - } - - public ImageBuilder scaleY(double factor) { - validatePositive("scaleY", factor); - this.scaleY = factor; - return this; - } - - public ImageBuilder fitToBounds(double maxWidth, double maxHeight) { - validatePositive("fit width", maxWidth); - validatePositive("fit height", maxHeight); - this.fitWidth = maxWidth; - this.fitHeight = maxHeight; - return this; - } - - public ImageBuilder fitToPage(Canvas canvas) { - return fitToBounds(canvas.innerWidth(), canvas.innerHeigh()); - } - - @Override - public void initialize() { - entity.addComponent(new ImageComponent()); - } - - @Override - public Entity build() { - ImageData imageData = entity.getComponent(ImageData.class).orElseThrow(() -> - new IllegalStateException("ImageBuilder requires ImageData before build()")); - - ImageIntrinsicSize intrinsicSize = imageData.getMetadata().intrinsicSize(); - entity.addComponent(intrinsicSize); - - if (!entity.has(ContentSize.class)) { - Padding padding = entity.getComponent(Padding.class).orElse(Padding.zero()); - entity.addComponent(resolveContentSize(intrinsicSize, padding)); - } - - return registerBuiltEntity(); - } - - private ContentSize resolveContentSize(ImageIntrinsicSize intrinsicSize, Padding padding) { - double drawableWidth = intrinsicSize.width(); - double drawableHeight = intrinsicSize.height(); - - if (fitWidth != null && fitHeight != null) { - double availableWidth = Math.max(0.0, fitWidth - padding.horizontal()); - double availableHeight = Math.max(0.0, fitHeight - padding.vertical()); - - if (availableWidth == 0.0 || availableHeight == 0.0) { - drawableWidth = 0.0; - drawableHeight = 0.0; - } else { - double scale = Math.min(availableWidth / intrinsicSize.width(), availableHeight / intrinsicSize.height()); - drawableWidth = intrinsicSize.width() * scale; - drawableHeight = intrinsicSize.height() * scale; - } - } else if (scaleX != null || scaleY != null) { - double resolvedScaleX = scaleX != null ? scaleX : 1.0; - double resolvedScaleY = scaleY != null ? scaleY : 1.0; - drawableWidth = intrinsicSize.width() * resolvedScaleX; - drawableHeight = intrinsicSize.height() * resolvedScaleY; - } - - return new ContentSize(drawableWidth + padding.horizontal(), drawableHeight + padding.vertical()); - } - - private void validatePositive(String name, double value) { - if (value <= 0.0 || Double.isNaN(value) || Double.isInfinite(value)) { - throw new IllegalArgumentException(name + " must be a finite positive number: " + value); - } - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ImageBuilderTest.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ImageBuilderTest.java deleted file mode 100644 index 474ba4fec..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ImageBuilderTest.java +++ /dev/null @@ -1,160 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.engine.components.content.ImageIntrinsicSize; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.layout.LayoutSystem; -import com.demcha.compose.engine.render.pdf.ecs.PdfCanvas; -import com.demcha.compose.engine.render.pdf.ecs.PdfRenderingSystemECS; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.within; - -class ImageBuilderTest { - - private EntityManager entityManager; - - @BeforeEach - void setUp() { - entityManager = new EntityManager(); - PDDocument doc = new PDDocument(); - PdfCanvas canvas = new PdfCanvas(PDRectangle.A4, 0.0f); - PdfRenderingSystemECS renderingSystemECS = new PdfRenderingSystemECS(doc, canvas); - entityManager.getSystems().addSystem(new LayoutSystem(canvas, renderingSystemECS)); - entityManager.getSystems().addSystem(renderingSystemECS); - } - - @Test - void shouldUseIntrinsicSizeByDefault() { - Entity entity = new ImageBuilder(entityManager) - .image(createPngBytes(200, 100)) - .build(); - - ImageIntrinsicSize intrinsicSize = entity.getComponent(ImageIntrinsicSize.class).orElseThrow(); - ContentSize contentSize = entity.getComponent(ContentSize.class).orElseThrow(); - - assertThat(intrinsicSize.width()).isEqualTo(200); - assertThat(intrinsicSize.height()).isEqualTo(100); - assertThat(contentSize.width()).isEqualTo(200); - assertThat(contentSize.height()).isEqualTo(100); - } - - @Test - void shouldApplyUniformScalePreservingAspectRatio() { - Entity entity = new ImageBuilder(entityManager) - .image(createPngBytes(200, 100)) - .scale(0.5) - .build(); - - ContentSize contentSize = entity.getComponent(ContentSize.class).orElseThrow(); - - assertThat(contentSize.width()).isCloseTo(100, within(0.001)); - assertThat(contentSize.height()).isCloseTo(50, within(0.001)); - } - - @Test - void shouldScaleOnlyWidthAxis() { - Entity entity = new ImageBuilder(entityManager) - .image(createPngBytes(200, 100)) - .scaleX(1.5) - .build(); - - ContentSize contentSize = entity.getComponent(ContentSize.class).orElseThrow(); - - assertThat(contentSize.width()).isCloseTo(300, within(0.001)); - assertThat(contentSize.height()).isCloseTo(100, within(0.001)); - } - - @Test - void shouldScaleOnlyHeightAxis() { - Entity entity = new ImageBuilder(entityManager) - .image(createPngBytes(200, 100)) - .scaleY(2.0) - .build(); - - ContentSize contentSize = entity.getComponent(ContentSize.class).orElseThrow(); - - assertThat(contentSize.width()).isCloseTo(200, within(0.001)); - assertThat(contentSize.height()).isCloseTo(200, within(0.001)); - } - - @Test - void shouldFitWithinBoundsPreservingAspectRatio() { - Entity entity = new ImageBuilder(entityManager) - .image(createPngBytes(200, 100)) - .fitToBounds(100, 100) - .build(); - - ContentSize contentSize = entity.getComponent(ContentSize.class).orElseThrow(); - - assertThat(contentSize.width()).isCloseTo(100, within(0.001)); - assertThat(contentSize.height()).isCloseTo(50, within(0.001)); - } - - @Test - void shouldFitToPageAndRespectPadding() { - PdfCanvas canvas = new PdfCanvas(200, 120, 0, 0); - canvas.addMargin(Margin.of(10)); - - Entity entity = new ImageBuilder(entityManager) - .image(createPngBytes(300, 150)) - .padding(Padding.of(5)) - .fitToPage(canvas) - .build(); - - ContentSize contentSize = entity.getComponent(ContentSize.class).orElseThrow(); - - assertThat(contentSize.width()).isCloseTo(180, within(0.001)); - assertThat(contentSize.height()).isCloseTo(95, within(0.001)); - } - - @Test - void shouldPreferExplicitSizeOverFitAndScale() { - Entity entity = new ImageBuilder(entityManager) - .image(createPngBytes(200, 100)) - .scale(0.5) - .fitToBounds(50, 50) - .size(70, 40) - .build(); - - ContentSize contentSize = entity.getComponent(ContentSize.class).orElseThrow(); - - assertThat(contentSize.width()).isEqualTo(70); - assertThat(contentSize.height()).isEqualTo(40); - } - - private byte[] createPngBytes(int width, int height) { - try { - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - Graphics2D graphics = image.createGraphics(); - try { - graphics.setColor(new Color(32, 40, 88)); - graphics.fillRect(0, 0, width, height); - graphics.setColor(new Color(255, 214, 10)); - graphics.fillRect(0, 0, width / 2, height / 2); - graphics.setColor(Color.WHITE); - graphics.drawRect(0, 0, width - 1, height - 1); - } finally { - graphics.dispose(); - } - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ImageIO.write(image, "png", outputStream); - return outputStream.toByteArray(); - } catch (Exception e) { - throw new IllegalStateException("Failed to generate test PNG", e); - } - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LineBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LineBuilder.java deleted file mode 100644 index 6925c8aa9..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LineBuilder.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.EmptyBox; -import com.demcha.compose.engine.components.content.shape.LinePath; -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.renderable.Line; -import com.demcha.compose.engine.core.EntityManager; - -public class LineBuilder extends EmptyBox { - LineBuilder(EntityManager entityManager) { - super(entityManager); - } - - public LineBuilder line(Line line) { - return addComponent(line); - } - - public LineBuilder stroke(Stroke stroke) { - return addComponent(stroke); - } - - public LineBuilder path(LinePath linePath) { - return addComponent(linePath); - } - - public LineBuilder path(double startX, double startY, double endX, double endY) { - return path(new LinePath(startX, startY, endX, endY)); - } - - public LineBuilder horizontal() { - return path(LinePath.horizontal()); - } - - public LineBuilder vertical() { - return path(LinePath.vertical()); - } - - public LineBuilder diagonalAscending() { - return path(LinePath.diagonalAscending()); - } - - public LineBuilder diagonalDescending() { - return path(LinePath.diagonalDescending()); - } - - @Override - public void initialize() { - entity.addComponent(new Line()); - entity.addComponent(LinePath.horizontal()); - entity.addComponent(new Stroke()); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LineBuilderTest.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LineBuilderTest.java deleted file mode 100644 index d2483e0a2..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LineBuilderTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.engine.components.content.shape.LinePath; -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.core.EntityManager; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class LineBuilderTest { - - @Test - void shouldProvideDefaultHorizontalPathAndStroke() { - var entity = new LineBuilder(new EntityManager()) - .size(140, 10) - .build(); - - assertThat(entity.getComponent(LinePath.class)).hasValue(LinePath.horizontal()); - assertThat(entity.getComponent(Stroke.class)) - .hasValueSatisfying(stroke -> { - assertThat(stroke.width()).isEqualTo(Stroke.DEFAULT_WIDTH); - assertThat(stroke.strokeColor().color()).isEqualTo(ComponentColor.BLACK); - }); - } - - @Test - void shouldAllowOverridingPathAndStroke() { - LinePath customPath = new LinePath(0.25, 1.0, 0.75, 0.0); - Stroke customStroke = new Stroke(ComponentColor.ROYAL_BLUE, 4.0); - - var entity = new LineBuilder(new EntityManager()) - .size(120, 40) - .vertical() - .path(customPath) - .stroke(customStroke) - .build(); - - assertThat(entity.getComponent(LinePath.class)).hasValue(customPath); - assertThat(entity.getComponent(Stroke.class)).hasValue(customStroke); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LinkBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LinkBuilder.java deleted file mode 100644 index 8f4419889..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/LinkBuilder.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.EmptyBox; -import com.demcha.compose.engine.components.content.link.LinkUrl; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.OuterBoxSize; -import com.demcha.compose.engine.components.renderable.Link; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; - -public class LinkBuilder extends EmptyBox { - LinkBuilder(EntityManager entityManager) { - super(entityManager); - } - - public LinkBuilder displayText(T text) { - Entity built = text.build(); - var contentSize = OuterBoxSize.from(built).orElseThrow(); - Padding padding = entity().getComponent(Padding.class).orElse(Padding.zero()); - size(new ContentSize(contentSize.width() + padding.horizontal(), contentSize.height() + padding.vertical())); - addChild(built); - return self(); - } - - public LinkBuilder linkUrl(T linkUrl) { - entity().addComponent(linkUrl); - return self(); - } - - - @Override - public void initialize() { - entity().addComponent(new Link()); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ModuleBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ModuleBuilder.java deleted file mode 100644 index 6058a953f..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/ModuleBuilder.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.ContainerBuilder; -import com.demcha.compose.engine.components.layout.StackAxis; -import com.demcha.compose.engine.components.content.bookmark.BookmarkEntry; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.geometry.ModuleWidthSeed; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.renderable.Module; -import com.demcha.compose.engine.components.renderable.VContainer; -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.core.EntityManager; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class ModuleBuilder extends ContainerBuilder { - /** - * Constructs a new {@code ModuleBuilder} with layout-resolved full-width - * semantics and no explicit root-width seed. - */ - ModuleBuilder(EntityManager entityManager, Align align) { - super(entityManager, align); - } - - /** - * Constructs a new {@code ModuleBuilder} with a root-width seed derived from - * the supplied content size. - */ - ModuleBuilder(EntityManager entityManager, Align align, ContentSize contentSize) { - this(entityManager, align); - if (contentSize != null) { - seedWidth(contentSize.width()); - entity.addComponent(contentSize); - } - } - - /** - * Constructs a new {@code ModuleBuilder} with a root-width seed based on the - * canvas inner width. - */ - ModuleBuilder(EntityManager entityManager, Align align, Canvas canvas) { - this(entityManager, align); - if (canvas != null) { - double seedWidth = canvas.innerWidth(); - seedWidth(seedWidth); - entity.addComponent(new ContentSize(seedWidth, 0)); - } - } - - /** - * Constructs a new {@code ModuleBuilder} with a root-width seed based on a - * precomputed inner box. - */ - ModuleBuilder(EntityManager entityManager, Align align, InnerBoxSize innerBoxSize) { - this(entityManager, align, - innerBoxSize == null ? null : new ContentSize(innerBoxSize.width(), innerBoxSize.height())); - } - - @Override - public void initialize() { - entity.addComponent(new Module()); - entity.addComponent(StackAxis.VERTICAL); - entity.addComponentIfAbsent(new VContainer()); - entity.addComponentIfAbsent(new ContentSize(0, 0)); - } - - private void seedWidth(double width) { - entity.addComponent(new ModuleWidthSeed(Math.max(0, width))); - } - - /** - * Marks this module as a PDF bookmark entry. - * - *

The bookmark title will appear in the PDF reader's outline panel, - * pointing to the page and Y-position of this module.

- * - * @param title the bookmark display title - * @return this builder - */ - public ModuleBuilder bookmark(String title) { - entity.addComponent(new BookmarkEntry(title)); - return this; - } - - /** - * Marks this module as a nested PDF bookmark entry. - * - * @param title the bookmark display title - * @param level nesting level (0 = root, 1 = child, etc.) - * @return this builder - */ - public ModuleBuilder bookmark(String title, int level) { - entity.addComponent(new BookmarkEntry(title, level)); - return this; - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/PageBreakBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/PageBreakBuilder.java deleted file mode 100644 index d3a3c692e..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/PageBreakBuilder.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.EmptyBox; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.renderable.PageBreakComponent; -import com.demcha.compose.engine.core.EntityManager; -import lombok.extern.slf4j.Slf4j; - -/** - * Builder for a forced page-break entity. - * - *

The page-break entity is a zero-width, zero-height entity with a marker - * component. The layout system should recognize this marker and advance to - * the next page. If the page breaker does not recognize the marker, a small - * height is used as a fallback.

- * - *

Usage

- *
- * cb.pageBreak().build();
- * 
- * - * @author Artem Demchyshyn - */ -@Slf4j -public class PageBreakBuilder extends EmptyBox { - - PageBreakBuilder(EntityManager entityManager) { - super(entityManager); - } - - @Override - public void initialize() { - entity.addComponent(new PageBreakComponent()); - // Use a small symbolic size — the page break is not rendered visually. - // The actual page advance happens via the rendering pipeline which - // recognizes the PageBreakComponent marker. - entity.addComponent(new ContentSize(0, 1)); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/RectangleBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/RectangleBuilder.java deleted file mode 100644 index 1441dd262..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/RectangleBuilder.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.ShapeBuilderBase; -import com.demcha.compose.engine.components.renderable.Rectangle; -import com.demcha.compose.engine.core.EntityManager; - -public class RectangleBuilder extends ShapeBuilderBase { - RectangleBuilder(EntityManager entityManager) { - super(entityManager); - } - - - public RectangleBuilder rectangle(Rectangle rectangle) { - return addComponent(rectangle); - } - - - @Override - public void initialize() { - entity.addComponent(new Rectangle()); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/RowBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/RowBuilder.java deleted file mode 100644 index 273f750ae..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/RowBuilder.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.ContainerBuilder; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.renderable.HContainer; -import com.demcha.compose.engine.core.EntityManager; - -// horizontall aligne -public class RowBuilder extends ContainerBuilder { - - /** - * Constructs a new {@code HContainerBuilder} associated with a specific Entity Manager. - * - * @param entityManager The {@link EntityManager} to which the container and its entities will belong. - */ - RowBuilder(EntityManager entityManager, Align align) { - super(entityManager,align); - } - - - /** - * Initializes the builder for creating a new horizontal container. - * This method calls the common creation logic from the superclass and then - * adds the specific {@link HContainer} component to the entity being built. - * - * @return This builder instance, allowing for method chaining. - */ - - - @Override - public void initialize() { - entity.addComponent(new HContainer()); - } - - -} - - diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilder.java deleted file mode 100644 index f6cc8e284..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilder.java +++ /dev/null @@ -1,557 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.ContainerBuilder; -import com.demcha.compose.engine.components.layout.StackAxis; -import com.demcha.compose.engine.components.content.shape.Side; -import com.demcha.compose.engine.components.content.table.TableCellContent; -import com.demcha.compose.engine.components.content.table.TableCellLayoutStyle; -import com.demcha.compose.engine.components.content.table.TableColumnLayout; -import com.demcha.compose.engine.components.content.table.TableLayoutData; -import com.demcha.compose.engine.components.content.table.TableResolvedCell; -import com.demcha.compose.engine.components.content.table.TableRowData; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.core.EntityName; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.HAnchor; -import com.demcha.compose.engine.components.layout.VAnchor; -import com.demcha.compose.engine.components.renderable.TableRow; -import com.demcha.compose.engine.components.renderable.VContainer; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.measurement.TextMeasurementSystem; - -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Engine-level builder for v1 tables. - *

- * The table implementation is intentionally hybrid. The table root is a - * breakable vertical container, while each row is materialized as an atomic - * leaf entity carrying precomputed cell payload. That design lets the engine - * negotiate widths once at the table level while keeping pagination row-safe. - *

- * - *

Use this builder when you need column width negotiation, scoped row/column - * styling, and table pagination that behaves consistently with the rest of the - * layout engine.

- */ -public class TableBuilder extends ContainerBuilder { - private static final double EPS = 1e-6; - - private final List columnSpecs = new ArrayList<>(); - private final List> rows = new ArrayList<>(); - private final Map rowStyles = new HashMap<>(); - private final Map columnStyles = new HashMap<>(); - - private TableCellLayoutStyle defaultCellStyle; - private Double requestedWidth; - private boolean materialized; - - TableBuilder(EntityManager entityManager) { - super(entityManager, Align.defaultAlign(0)); - } - - @Override - public void initialize() { - entity.addComponent(new VContainer()); - entity.addComponent(StackAxis.VERTICAL); - } - - public TableBuilder columns(TableColumnLayout... specs) { - ensureMutable(); - Objects.requireNonNull(specs, "specs"); - columnSpecs.clear(); - columnSpecs.addAll(List.of(specs)); - return self(); - } - - public TableBuilder width(double width) { - ensureMutable(); - if (width <= 0) { - throw new IllegalArgumentException("Table width must be greater than 0."); - } - this.requestedWidth = width; - return self(); - } - - public TableBuilder defaultCellStyle(TableCellLayoutStyle style) { - ensureMutable(); - this.defaultCellStyle = Objects.requireNonNull(style, "style"); - return self(); - } - - public TableBuilder rowStyle(int rowIndex, TableCellLayoutStyle style) { - ensureMutable(); - validateNonNegativeIndex(rowIndex, "Row"); - rowStyles.put(rowIndex, Objects.requireNonNull(style, "style")); - return self(); - } - - public TableBuilder columnStyle(int columnIndex, TableCellLayoutStyle style) { - ensureMutable(); - validateNonNegativeIndex(columnIndex, "Column"); - columnStyles.put(columnIndex, Objects.requireNonNull(style, "style")); - return self(); - } - - public TableBuilder row(TableCellContent... cells) { - ensureMutable(); - Objects.requireNonNull(cells, "cells"); - - List values = new ArrayList<>(cells.length); - for (TableCellContent cell : cells) { - values.add(cell == null ? TableCellContent.text("") : cell); - } - rows.add(List.copyOf(values)); - return self(); - } - - public TableBuilder row(String... cells) { - Objects.requireNonNull(cells, "cells"); - - TableCellContent[] values = new TableCellContent[cells.length]; - for (int i = 0; i < cells.length; i++) { - values[i] = TableCellContent.text(cells[i]); - } - return row(values); - } - - @Override - public Entity build() { - if (!materialized) { - materializeRows(); - materialized = true; - } - return super.build(); - } - - private void materializeRows() { - validateRowsExist(); - - int columnCount = resolveColumnCount(); - validateRowSpans(columnCount); - - List> logicalRows = buildLogicalRows(); - List normalizedSpecs = normalizeSpecs(columnCount); - TableCellLayoutStyle[][] stylesGrid = buildStylesGrid(logicalRows, columnCount); - double[] naturalWidths = resolveNaturalColumnWidths(normalizedSpecs, logicalRows, stylesGrid, columnCount); - double naturalWidth = sum(naturalWidths); - double[] finalWidths = resolveFinalColumnWidths(normalizedSpecs, naturalWidths, naturalWidth); - double finalWidth = sum(finalWidths); - - List rowEntities = new ArrayList<>(logicalRows.size()); - - for (int rowIndex = 0; rowIndex < logicalRows.size(); rowIndex++) { - Entity rowEntity = buildRowEntity(rowIndex, logicalRows.get(rowIndex), stylesGrid, finalWidths); - addChild(rowEntity); - entityManager.putEntity(rowEntity); - rowEntities.add(rowEntity); - } - - entity.addComponent(new TableLayoutData( - toList(finalWidths), - naturalWidth, - finalWidth, - logicalRows.size(), - columnCount, - List.copyOf(rowEntities))); - } - - private Entity buildRowEntity(int rowIndex, - List rowCells, - TableCellLayoutStyle[][] stylesGrid, - double[] columnWidths) { - double rowHeight = 0.0; - for (LogicalCell logical : rowCells) { - TableCellLayoutStyle style = stylesGrid[rowIndex][logical.startColumn]; - rowHeight = Math.max(rowHeight, cellNaturalHeight(logical.content, style)); - } - - List cells = new ArrayList<>(rowCells.size()); - double rowWidth = 0.0; - for (LogicalCell logical : rowCells) { - TableCellLayoutStyle style = stylesGrid[rowIndex][logical.startColumn]; - double x = sumRange(columnWidths, 0, logical.startColumn); - double width = sumRange(columnWidths, logical.startColumn, logical.startColumn + logical.colSpan); - cells.add(new TableResolvedCell( - cellName(rowIndex, logical.startColumn), - x, - width, - rowHeight, - sanitizeCellLines(logical.content), - style, - fillInsets(stylesGrid, rowIndex, logical.startColumn, logical.colSpan), - borderSides(stylesGrid, rowIndex, logical.startColumn, logical.colSpan) - )); - rowWidth = Math.max(rowWidth, x + width); - } - - Entity rowEntity = new Entity(); - rowEntity.addComponent(new EntityName(rowName(rowIndex))); - rowEntity.addComponent(new Anchor(HAnchor.LEFT, VAnchor.DEFAULT)); - rowEntity.addComponent(new ContentSize(rowWidth, rowHeight)); - rowEntity.addComponent(new TableRowData(cells)); - rowEntity.addComponent(new TableRow()); - return rowEntity; - } - - private List> buildLogicalRows() { - List> result = new ArrayList<>(rows.size()); - for (List row : rows) { - List logical = new ArrayList<>(row.size()); - int col = 0; - for (TableCellContent cell : row) { - logical.add(new LogicalCell(col, cell.colSpan(), cell)); - col += cell.colSpan(); - } - result.add(List.copyOf(logical)); - } - return List.copyOf(result); - } - - private TableCellLayoutStyle[][] buildStylesGrid(List> logicalRows, int columnCount) { - TableCellLayoutStyle tableDefault = TableCellLayoutStyle.merge(TableCellLayoutStyle.DEFAULT, defaultCellStyle); - TableCellLayoutStyle[][] grid = new TableCellLayoutStyle[logicalRows.size()][columnCount]; - - for (int rowIndex = 0; rowIndex < logicalRows.size(); rowIndex++) { - TableCellLayoutStyle rowOverride = rowStyles.get(rowIndex); - for (LogicalCell logical : logicalRows.get(rowIndex)) { - TableCellLayoutStyle resolved = TableCellLayoutStyle.merge(tableDefault, columnStyles.get(logical.startColumn)); - resolved = TableCellLayoutStyle.merge(resolved, rowOverride); - resolved = TableCellLayoutStyle.merge(resolved, logical.content.styleOverride()); - for (int col = logical.startColumn; col < logical.startColumn + logical.colSpan; col++) { - grid[rowIndex][col] = resolved; - } - } - } - - return grid; - } - - private double[] resolveNaturalColumnWidths(List normalizedSpecs, - List> logicalRows, - TableCellLayoutStyle[][] stylesGrid, - int columnCount) { - double[] widths = new double[columnCount]; - double[] singleCellRequired = new double[columnCount]; - - for (int rowIndex = 0; rowIndex < logicalRows.size(); rowIndex++) { - for (LogicalCell logical : logicalRows.get(rowIndex)) { - if (logical.colSpan != 1) { - continue; - } - int col = logical.startColumn; - singleCellRequired[col] = Math.max(singleCellRequired[col], - cellNaturalWidth(logical.content, stylesGrid[rowIndex][col])); - } - } - - for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { - TableColumnLayout spec = normalizedSpecs.get(columnIndex); - if (spec.isFixed()) { - if (spec.requiredFixedWidth() + EPS < singleCellRequired[columnIndex]) { - throw new IllegalStateException("Fixed column %d width %.2f is smaller than required natural width %.2f." - .formatted(columnIndex, spec.requiredFixedWidth(), singleCellRequired[columnIndex])); - } - widths[columnIndex] = spec.requiredFixedWidth(); - } else { - widths[columnIndex] = singleCellRequired[columnIndex]; - } - } - - for (int rowIndex = 0; rowIndex < logicalRows.size(); rowIndex++) { - for (LogicalCell logical : logicalRows.get(rowIndex)) { - if (logical.colSpan == 1) { - continue; - } - int startCol = logical.startColumn; - int endCol = startCol + logical.colSpan; - double need = cellNaturalWidth(logical.content, stylesGrid[rowIndex][startCol]); - double have = sumRange(widths, startCol, endCol); - if (need <= have + EPS) { - continue; - } - distributeDeficit(widths, normalizedSpecs, startCol, endCol, need - have, rowIndex, logical.colSpan); - } - } - - return widths; - } - - private void distributeDeficit(double[] widths, - List normalizedSpecs, - int startCol, - int endCol, - double deficit, - int rowIndex, - int colSpan) { - List autoColumns = new ArrayList<>(); - for (int col = startCol; col < endCol; col++) { - if (normalizedSpecs.get(col).isAuto()) { - autoColumns.add(col); - } - } - if (autoColumns.isEmpty()) { - throw new IllegalStateException("Spanned cell at row %d over %d fixed columns starting at %d requires extra width %.2f but all spanned columns are fixed." - .formatted(rowIndex, colSpan, startCol, deficit)); - } - double share = deficit / autoColumns.size(); - for (int col : autoColumns) { - widths[col] += share; - } - } - - private double[] resolveFinalColumnWidths(List normalizedSpecs, - double[] naturalWidths, - double naturalWidth) { - double[] finalWidths = naturalWidths.clone(); - - if (requestedWidth == null) { - return finalWidths; - } - - if (requestedWidth + EPS < naturalWidth) { - throw new IllegalStateException("Requested table width %.2f is smaller than natural width %.2f." - .formatted(requestedWidth, naturalWidth)); - } - - double extra = requestedWidth - naturalWidth; - if (extra <= EPS) { - return finalWidths; - } - - List autoColumns = new ArrayList<>(); - for (int i = 0; i < normalizedSpecs.size(); i++) { - if (normalizedSpecs.get(i).isAuto()) { - autoColumns.add(i); - } - } - - if (autoColumns.isEmpty()) { - finalWidths[finalWidths.length - 1] += extra; - return finalWidths; - } - - double share = extra / autoColumns.size(); - for (Integer autoColumn : autoColumns) { - finalWidths[autoColumn] += share; - } - - return finalWidths; - } - - private List normalizeSpecs(int columnCount) { - List normalized = new ArrayList<>(columnCount); - for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { - normalized.add(columnIndex < columnSpecs.size() ? columnSpecs.get(columnIndex) : TableColumnLayout.auto()); - } - return normalized; - } - - private int resolveColumnCount() { - int maxRowSpanSum = 0; - for (List row : rows) { - int spanSum = 0; - for (TableCellContent cell : row) { - spanSum += cell.colSpan(); - } - maxRowSpanSum = Math.max(maxRowSpanSum, spanSum); - } - return Math.max(columnSpecs.size(), maxRowSpanSum); - } - - private double cellNaturalWidth(TableCellContent cell, TableCellLayoutStyle style) { - Padding padding = style.padding() == null ? Padding.zero() : style.padding(); - double maxWidth = 0.0; - for (String line : sanitizeCellLines(cell)) { - maxWidth = Math.max(maxWidth, measureText(line, style).width()); - } - return maxWidth + padding.horizontal(); - } - - private double cellNaturalHeight(TableCellContent cell, TableCellLayoutStyle style) { - Padding padding = style.padding() == null ? Padding.zero() : style.padding(); - int lineCount = Math.max(1, sanitizeCellLines(cell).size()); - return (lineCount * measureLineHeight(style)) - + ((lineCount - 1) * lineSpacing(style)) - + padding.vertical(); - } - - private double measureLineHeight(TableCellLayoutStyle style) { - return textMeasurementSystem().lineHeight(style.textStyle()); - } - - private double lineSpacing(TableCellLayoutStyle style) { - return style.lineSpacing() == null ? 0.0 : style.lineSpacing(); - } - - private ContentSize measureText(String text, TableCellLayoutStyle style) { - return textMeasurementSystem().measure(style.textStyle(), text); - } - - private TextMeasurementSystem textMeasurementSystem() { - return entityManager.getSystems() - .textMeasurement() - .orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to measure table text.")); - } - - private EnumSet borderSides(TableCellLayoutStyle[][] stylesGrid, int rowIndex, int startCol, int colSpan) { - EnumSet sides = EnumSet.of(Side.BOTTOM, Side.RIGHT); - if (ownsTopBoundary(stylesGrid, rowIndex, startCol, colSpan)) { - sides.add(Side.TOP); - } - if (ownsLeftBoundary(stylesGrid, rowIndex, startCol)) { - sides.add(Side.LEFT); - } - return sides; - } - - private Padding fillInsets(TableCellLayoutStyle[][] stylesGrid, int rowIndex, int startCol, int colSpan) { - TableCellLayoutStyle ownStyle = stylesGrid[rowIndex][startCol]; - double ownStroke = strokeWidth(ownStyle); - double topInset = topBoundaryStrokeWidth(stylesGrid, rowIndex, startCol, colSpan) / 2.0; - double rightInset = ownStroke / 2.0; - double bottomInset = ownStroke / 2.0; - double leftInset = leftBoundaryStrokeWidth(stylesGrid, rowIndex, startCol) / 2.0; - return new Padding(topInset, rightInset, bottomInset, leftInset); - } - - private double topBoundaryStrokeWidth(TableCellLayoutStyle[][] stylesGrid, int rowIndex, int startCol, int colSpan) { - if (ownsTopBoundary(stylesGrid, rowIndex, startCol, colSpan)) { - return strokeWidth(stylesGrid[rowIndex][startCol]); - } - double maxAbove = 0.0; - for (int col = startCol; col < startCol + colSpan; col++) { - maxAbove = Math.max(maxAbove, strokeWidth(stylesGrid[rowIndex - 1][col])); - } - return maxAbove; - } - - private double leftBoundaryStrokeWidth(TableCellLayoutStyle[][] stylesGrid, int rowIndex, int startCol) { - if (ownsLeftBoundary(stylesGrid, rowIndex, startCol)) { - return strokeWidth(stylesGrid[rowIndex][startCol]); - } - return strokeWidth(stylesGrid[rowIndex][startCol - 1]); - } - - private boolean ownsTopBoundary(TableCellLayoutStyle[][] stylesGrid, int rowIndex, int startCol, int colSpan) { - if (rowIndex == 0) { - return true; - } - for (int col = startCol; col < startCol + colSpan; col++) { - if (hasVisibleStroke(stylesGrid[rowIndex - 1][col])) { - return false; - } - } - return true; - } - - private boolean ownsLeftBoundary(TableCellLayoutStyle[][] stylesGrid, int rowIndex, int startCol) { - return startCol == 0 || !hasVisibleStroke(stylesGrid[rowIndex][startCol - 1]); - } - - private double strokeWidth(TableCellLayoutStyle style) { - if (style == null || style.stroke() == null) { - return 0.0; - } - return style.stroke().width(); - } - - private boolean hasVisibleStroke(TableCellLayoutStyle style) { - return strokeWidth(style) > EPS; - } - - private List toList(double[] widths) { - List result = new ArrayList<>(widths.length); - for (double width : widths) { - result.add(width); - } - return List.copyOf(result); - } - - private double sum(double[] widths) { - double total = 0.0; - for (double width : widths) { - total += width; - } - return total; - } - - private double sumRange(double[] values, int fromInclusive, int toExclusive) { - double total = 0.0; - for (int i = fromInclusive; i < toExclusive; i++) { - total += values[i]; - } - return total; - } - - private List sanitizeCellLines(TableCellContent cell) { - List sanitized = new ArrayList<>(cell.lines().size()); - for (String line : cell.lines()) { - sanitized.add(sanitizeCellLine(line)); - } - return List.copyOf(sanitized); - } - - private String sanitizeCellLine(String value) { - if (value == null) { - return ""; - } - return value.replace('\r', ' ').replace('\n', ' '); - } - - private String rowName(int rowIndex) { - return tableName() + "__row_" + rowIndex; - } - - private String cellName(int rowIndex, int columnIndex) { - return rowName(rowIndex) + "__cell_" + columnIndex; - } - - private String tableName() { - return entity.getComponent(EntityName.class) - .map(EntityName::value) - .orElse("Table"); - } - - private void validateRowsExist() { - if (rows.isEmpty()) { - throw new IllegalStateException("Table must contain at least one row."); - } - } - - private void validateRowSpans(int columnCount) { - for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) { - int spanSum = 0; - for (TableCellContent cell : rows.get(rowIndex)) { - spanSum += cell.colSpan(); - } - if (spanSum != columnCount) { - throw new IllegalStateException("Row %d has cells with colSpan sum %d but table requires %d columns." - .formatted(rowIndex, spanSum, columnCount)); - } - } - } - - private void validateNonNegativeIndex(int index, String label) { - if (index < 0) { - throw new IllegalArgumentException(label + " index cannot be negative."); - } - } - - private void ensureMutable() { - if (materialized) { - throw new IllegalStateException("Table has already been built and cannot be modified."); - } - } - - private record LogicalCell(int startColumn, int colSpan, TableCellContent content) { - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java deleted file mode 100644 index e769d3d98..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java +++ /dev/null @@ -1,466 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory; -import com.demcha.compose.engine.measurement.FontLibraryTextMeasurementSystem; -import com.demcha.compose.engine.components.content.table.TableLayoutData; -import com.demcha.compose.engine.components.content.table.TableResolvedCell; -import com.demcha.compose.engine.components.content.table.TableRowData; -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.content.table.TableCellContent; -import com.demcha.compose.engine.components.content.table.TableCellLayoutStyle; -import com.demcha.compose.engine.components.content.table.TableColumnLayout; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.core.EntityName; -import com.demcha.compose.engine.components.content.shape.Side; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.testsupport.EngineComposerHarness; -import com.demcha.compose.engine.render.pdf.PdfFont; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class TableBuilderTest { - - @Test - void shouldComputeFixedAndAutoColumnWidths() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity table = composer.componentBuilder() - .table() - .entityName("MetricsTable") - .columns(TableColumnLayout.fixed(120), TableColumnLayout.auto()) - .row("ID", "Longest visible metric label") - .row("Code", "Label") - .build(); - - TableLayoutData layoutData = table.getComponent(TableLayoutData.class).orElseThrow(); - Entity firstRow = child(table, 0); - TableRowData rowData = firstRow.getComponent(TableRowData.class).orElseThrow(); - - assertThat(layoutData.columnWidths()).hasSize(2); - assertThat(layoutData.columnWidths().get(0)).isEqualTo(120.0); - assertThat(rowData.cells().get(0).width()).isEqualTo(120.0); - assertThat(rowData.cells().get(1).width()).isEqualTo(layoutData.columnWidths().get(1)); - assertThat(layoutData.finalWidth()).isEqualTo(layoutData.columnWidths().stream().mapToDouble(Double::doubleValue).sum()); - } - } - - @Test - void shouldUseNaturalWidthWhenExplicitWidthIsNotProvided() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity table = composer.componentBuilder() - .table() - .entityName("NaturalWidthTable") - .columns(TableColumnLayout.auto(), TableColumnLayout.auto()) - .row("First", "Second") - .row("Much wider first column", "Value") - .build(); - - TableLayoutData layoutData = table.getComponent(TableLayoutData.class).orElseThrow(); - - assertThat(layoutData.finalWidth()).isEqualTo(layoutData.naturalWidth()); - assertThat(layoutData.finalWidth()).isEqualTo(layoutData.columnWidths().stream().mapToDouble(Double::doubleValue).sum()); - } - } - - @Test - void shouldFailWhenRequestedWidthIsSmallerThanNaturalWidth() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - assertThatThrownBy(() -> composer.componentBuilder() - .table() - .columns(TableColumnLayout.auto(), TableColumnLayout.auto()) - .width(60) - .row("Very long text", "Another long text") - .build()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Requested table width"); - } - } - - @Test - void shouldApplyStylePrecedenceDefaultThenColumnThenRow() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - TableCellLayoutStyle tableDefault = TableCellLayoutStyle.builder() - .fillColor(ComponentColor.LIGHT_GRAY) - .padding(Padding.of(2)) - .lineSpacing(1.0) - .build(); - - TableCellLayoutStyle columnOverride = TableCellLayoutStyle.builder() - .fillColor(ComponentColor.BLUE) - .lineSpacing(2.0) - .textStyle(TextStyle.builder() - .fontName(TextStyle.DEFAULT_STYLE.fontName()) - .size(18) - .decoration(TextStyle.DEFAULT_STYLE.decoration()) - .color(TextStyle.DEFAULT_STYLE.color()) - .build()) - .build(); - - TableCellLayoutStyle rowOverride = TableCellLayoutStyle.builder() - .fillColor(ComponentColor.RED) - .padding(Padding.of(8)) - .build(); - - Entity table = composer.componentBuilder() - .table() - .entityName("StyledTable") - .defaultCellStyle(tableDefault) - .columnStyle(0, columnOverride) - .rowStyle(0, rowOverride) - .row("A", "B") - .row("C", "D") - .build(); - - TableResolvedCell firstRowFirstCell = child(table, 0) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(0); - - TableResolvedCell secondRowFirstCell = child(table, 1) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(0); - - assertThat(firstRowFirstCell.style().fillColor()).isEqualTo(ComponentColor.RED); - assertThat(firstRowFirstCell.style().padding()).isEqualTo(Padding.of(8)); - assertThat(firstRowFirstCell.style().textStyle().size()).isEqualTo(18); - assertThat(firstRowFirstCell.style().lineSpacing()).isEqualTo(2.0); - - assertThat(secondRowFirstCell.style().fillColor()).isEqualTo(ComponentColor.BLUE); - assertThat(secondRowFirstCell.style().padding()).isEqualTo(Padding.of(2)); - assertThat(secondRowFirstCell.style().textStyle().size()).isEqualTo(18); - assertThat(secondRowFirstCell.style().lineSpacing()).isEqualTo(2.0); - } - } - - @Test - void shouldApplyCellOverrideAfterDefaultColumnAndRowStyles() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - TableCellLayoutStyle tableDefault = TableCellLayoutStyle.builder() - .fillColor(ComponentColor.LIGHT_GRAY) - .padding(Padding.of(2)) - .build(); - - TableCellLayoutStyle columnOverride = TableCellLayoutStyle.builder() - .textStyle(TextStyle.builder() - .fontName(TextStyle.DEFAULT_STYLE.fontName()) - .size(18) - .decoration(TextStyle.DEFAULT_STYLE.decoration()) - .color(TextStyle.DEFAULT_STYLE.color()) - .build()) - .build(); - - TableCellLayoutStyle rowOverride = TableCellLayoutStyle.builder() - .fillColor(ComponentColor.RED) - .padding(Padding.of(8)) - .build(); - - TableCellLayoutStyle cellOverride = TableCellLayoutStyle.builder() - .fillColor(ComponentColor.GREEN) - .textAnchor(com.demcha.compose.engine.components.layout.Anchor.centerRight()) - .build(); - - Entity table = composer.componentBuilder() - .table() - .entityName("StyledTableWithCellOverride") - .defaultCellStyle(tableDefault) - .columnStyle(0, columnOverride) - .rowStyle(0, rowOverride) - .row( - TableCellContent.text("A").withStyle(cellOverride), - TableCellContent.text("B")) - .build(); - - TableResolvedCell firstCell = child(table, 0) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(0); - - assertThat(firstCell.style().fillColor()).isEqualTo(ComponentColor.GREEN); - assertThat(firstCell.style().padding()).isEqualTo(Padding.of(8)); - assertThat(firstCell.style().textStyle().size()).isEqualTo(18); - assertThat(firstCell.style().textAnchor()).isEqualTo(com.demcha.compose.engine.components.layout.Anchor.centerRight()); - } - } - - @Test - void shouldPreserveStringRowApiAsSingleLineCellSpec() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity table = composer.componentBuilder() - .table() - .entityName("StringApiTable") - .row("Alpha", "Beta") - .build(); - - TableResolvedCell firstCell = child(table, 0) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(0); - - assertThat(firstCell.lines()).containsExactly("Alpha"); - } - } - - @Test - void shouldMeasureMultilineCellsUsingLongestLineAndLineCount() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity multilineTable = composer.componentBuilder() - .table() - .entityName("MultilineTable") - .row(TableCellContent.lines("Short", "Longest visible metric label")) - .build(); - - Entity longestLineTable = composer.componentBuilder() - .table() - .entityName("LongestLineTable") - .row("Longest visible metric label") - .build(); - - TableResolvedCell multilineCell = child(multilineTable, 0) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(0); - - TableResolvedCell singleLineCell = child(longestLineTable, 0) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(0); - - double paddingVertical = TableCellLayoutStyle.DEFAULT.padding().vertical(); - double expectedMultilineHeight = (2 * (singleLineCell.height() - paddingVertical)) + paddingVertical; - - assertThat(multilineCell.width()).isEqualTo(singleLineCell.width()); - assertThat(multilineCell.height()).isEqualTo(expectedMultilineHeight); - assertThat(multilineCell.lines()).containsExactly("Short", "Longest visible metric label"); - } - } - - @Test - void shouldIncludeCellLineSpacingInMultilineCellHeight() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - TableCellLayoutStyle style = TableCellLayoutStyle.builder() - .padding(Padding.zero()) - .lineSpacing(2.5) - .build(); - - Entity multilineTable = composer.componentBuilder() - .table() - .entityName("MultilineLineSpacingTable") - .defaultCellStyle(style) - .row(TableCellContent.lines("Short", "Longer line")) - .build(); - - Entity singleLineTable = composer.componentBuilder() - .table() - .entityName("SingleLineSpacingTable") - .defaultCellStyle(style) - .row("Short") - .build(); - - TableResolvedCell multilineCell = child(multilineTable, 0) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(0); - TableResolvedCell singleLineCell = child(singleLineTable, 0) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(0); - - assertThat(multilineCell.height()).isEqualTo((2 * singleLineCell.height()) + 2.5); - } - } - - @Test - void shouldBuildWithoutLayoutSystemWhenTextMeasurementSystemIsRegistered() { - EntityManager entityManager = new EntityManager(PdfFontLibraryFactory.standardLibrary(), false); - entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class)); - - Entity table = new TableBuilder(entityManager) - .entityName("MeasurementOnlyTable") - .row("Alpha", "Beta") - .build(); - - TableLayoutData layoutData = table.getComponent(TableLayoutData.class).orElseThrow(); - assertThat(layoutData.columnWidths()).hasSize(2); - assertThat(layoutData.finalWidth()).isGreaterThan(0.0); - } - - @Test - void shouldRejectEmptyTable() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - assertThatThrownBy(() -> composer.componentBuilder() - .table() - .entityName("EmptyTable") - .build()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("at least one row"); - } - } - - @Test - void shouldRejectRowsThatDoNotMatchResolvedColumnCount() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - assertThatThrownBy(() -> composer.componentBuilder() - .table() - .columns(TableColumnLayout.fixed(120), TableColumnLayout.auto()) - .row("Only one cell") - .build()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("requires 2 columns"); - } - } - - @Test - void shouldCreateStableRowAndCellNamesFromTableName() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity table = composer.componentBuilder() - .table() - .entityName("Orders") - .row("A", "B") - .build(); - - Entity row = child(table, 0); - TableResolvedCell cell = row.getComponent(TableRowData.class).orElseThrow().cells().get(0); - - assertThat(row.getComponent(EntityName.class)).hasValue(new EntityName("Orders__row_0")); - assertThat(cell.name()).isEqualTo("Orders__row_0__cell_0"); - } - } - - @Test - void shouldAssignBordersToCurrentCellsSoTrailingPageLinesStayVisible() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity table = composer.componentBuilder() - .table() - .entityName("BorderOwnership") - .row("A", "B") - .row("C", "D") - .build(); - - TableResolvedCell firstRowFirstCell = child(table, 0) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(0); - - TableResolvedCell firstRowSecondCell = child(table, 0) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(1); - - TableResolvedCell secondRowSecondCell = child(table, 1) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(1); - - assertThat(firstRowFirstCell.borderSides()).containsExactlyInAnyOrder(Side.TOP, Side.LEFT, Side.RIGHT, Side.BOTTOM); - assertThat(firstRowSecondCell.borderSides()).containsExactlyInAnyOrder(Side.TOP, Side.RIGHT, Side.BOTTOM); - assertThat(secondRowSecondCell.borderSides()).containsExactlyInAnyOrder(Side.RIGHT, Side.BOTTOM); - } - } - - @Test - void shouldLetCellOwnLeftBoundaryWhenInvisibleSpacerPrecedesIt() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity table = composer.componentBuilder() - .table() - .entityName("InvisibleSpacerBoundary") - .defaultCellStyle(TableCellLayoutStyle.builder() - .stroke(new Stroke(ComponentColor.BLACK, 2.0)) - .build()) - .columnStyle(1, TableCellLayoutStyle.builder() - .stroke(new Stroke(ComponentColor.WHITE, 0.0)) - .build()) - .row("Notes", "", "Total") - .build(); - - TableResolvedCell summaryCell = child(table, 0) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(2); - - assertThat(summaryCell.borderSides()).containsExactlyInAnyOrder(Side.TOP, Side.LEFT, Side.RIGHT, Side.BOTTOM); - assertThat(summaryCell.fillInsets().left()).isEqualTo(1.0); - } - } - - @Test - void shouldLetCellOwnTopBoundaryWhenPreviousRowStrokeIsInvisible() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity table = composer.componentBuilder() - .table() - .entityName("InvisiblePreviousRowBoundary") - .defaultCellStyle(TableCellLayoutStyle.builder() - .stroke(new Stroke(ComponentColor.BLACK, 2.0)) - .build()) - .rowStyle(0, TableCellLayoutStyle.builder() - .stroke(new Stroke(ComponentColor.WHITE, 0.0)) - .build()) - .row("Notes", "Total") - .row("Terms", "Paid") - .build(); - - TableResolvedCell secondRowCell = child(table, 1) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(0); - - assertThat(secondRowCell.borderSides()).containsExactlyInAnyOrder(Side.TOP, Side.LEFT, Side.RIGHT, Side.BOTTOM); - assertThat(secondRowCell.fillInsets().top()).isEqualTo(1.0); - } - } - - @Test - void shouldInsetFillUsingOwningNeighborStrokeWidths() throws Exception { - try (EngineComposerHarness composer = com.demcha.compose.testsupport.EngineComposerHarness.pdf().create()) { - Entity table = composer.componentBuilder() - .table() - .entityName("FillInsets") - .defaultCellStyle(TableCellLayoutStyle.builder() - .stroke(new Stroke(ComponentColor.BLACK, 1.0)) - .build()) - .rowStyle(0, TableCellLayoutStyle.builder() - .stroke(new Stroke(ComponentColor.BLACK, 2.0)) - .build()) - .columnStyle(0, TableCellLayoutStyle.builder() - .stroke(new Stroke(ComponentColor.BLACK, 4.0)) - .build()) - .row("A", "B") - .row("C", "D") - .build(); - - TableResolvedCell secondRowSecondCell = child(table, 1) - .getComponent(TableRowData.class) - .orElseThrow() - .cells() - .get(1); - - assertThat(secondRowSecondCell.fillInsets().top()).isEqualTo(1.0); - assertThat(secondRowSecondCell.fillInsets().left()).isEqualTo(2.0); - assertThat(secondRowSecondCell.fillInsets().right()).isEqualTo(0.5); - assertThat(secondRowSecondCell.fillInsets().bottom()).isEqualTo(0.5); - } - } - - private Entity child(Entity parent, int index) { - TableLayoutData layoutData = parent.getComponent(TableLayoutData.class).orElseThrow(); - return layoutData.rowEntities().get(index); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilder.java deleted file mode 100644 index 32ec54d69..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilder.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.testsupport.engine.assembly.container.EmptyBox; -import com.demcha.compose.engine.components.content.text.Text; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.renderable.TextComponent; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.exceptions.TextComponentException; -import lombok.SneakyThrows; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; - -/** - * Builder for single text entities. - *

- * {@code TextBuilder} produces a leaf entity rendered by {@code TextComponent}. - * It is the simplest text path in the engine and is a good fit for labels, - * short headings, metadata fields, and any text that should stay as one entity. - *

- * - *

If auto-size is enabled, the builder measures the text before registration - * and writes a {@code ContentSize} component that later drives layout.

- */ -@Slf4j -@Accessors(fluent = true) -public class TextBuilder extends EmptyBox { - private boolean autosize; - - TextBuilder(EntityManager entityManager) { - super(entityManager); - } - - /** - * Sets the text payload explicitly. - */ - public TextBuilder text(Text textComponent) { - return addComponent(textComponent); - } - - /** - * Sets the text payload from a raw string. - */ - public TextBuilder text(String text) { - return addComponent(new Text(text)); - } - - /** - * Sets the text style consumed later by measurement and rendering. - */ - public TextBuilder textStyle(TextStyle textStyle) { - return addComponent(textStyle); - } - - - /** - * Sets the text payload and asks the builder to measure the final content size. - */ - public TextBuilder textWithAutoSize(Text textComponent) { - autosize = true; - return addComponent(textComponent); - - } - - /** - * Sets the text payload from a string and asks the builder to measure the - * final content size. - */ - public TextBuilder textWithAutoSize(String text) { - autosize = true; - return addComponent(new Text(text)); - - } - - @Override - public void initialize() { - entity.addComponent(new TextComponent()); - } - - /** - * Finalizes the entity and optionally measures text before registration. - * - *

After this call the entity is ready for layout. The layout system will - * later compute placement from the measured size, margin, padding, anchor, - * and parent context.

- */ - @SneakyThrows - @Override - @SuppressWarnings("unchecked") - public Entity build() { - if (entity.hasAssignable(TextComponent.class)) { - if (autosize) { - Padding padding = entity.getComponent(Padding.class).orElse(Padding.zero()); - ContentSize measuredText = TextComponent.autoMeasureText(entity, entityManager); - double textHeight = measuredText.height() + padding.vertical(); - double textWidth = measuredText.width() + padding.horizontal(); - - log.debug("Autosized single-line text entity {} -> measuredText={} padding={} finalSize=({}, {})", - entity, measuredText, padding, textWidth, textHeight); - - entity.addComponent(new ContentSize(textWidth, textHeight)); - } - return registerBuiltEntity(); - } else { - log.error("TextComponent Component has not been initialized"); - throw new TextComponentException(TextComponent.class + " Component has not been initialized"); - } - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilderTest.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilderTest.java deleted file mode 100644 index 47f2d6816..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilderTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; - -import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory; -import com.demcha.compose.engine.measurement.FontLibraryTextMeasurementSystem; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.renderable.TextComponent; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.render.pdf.PdfFont; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.within; - -class TextBuilderTest { - - private EntityManager entityManager; - - @BeforeEach - void setUp() { - entityManager = new EntityManager(PdfFontLibraryFactory.standardLibrary(), false); - entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class)); - } - - @Test - void shouldUseLineHeightForAutosizedSingleLineHeight() { - TextStyle style = TextStyle.DEFAULT_STYLE; - Padding padding = Padding.of(5); - String text = "In-memory PDF"; - - Entity entity = new TextBuilder(entityManager) - .textWithAutoSize(text) - .textStyle(style) - .padding(padding) - .build(); - - ContentSize contentSize = entity.getComponent(ContentSize.class).orElseThrow(); - PdfFont font = (PdfFont) entityManager.getFonts().getFont(style.fontName(), PdfFont.class).orElseThrow(); - - assertThat(contentSize.width()) - .isCloseTo(font.getTextWidth(style, text) + padding.horizontal(), within(0.001)); - assertThat(contentSize.height()) - .isCloseTo(font.getLineHeight(style) + padding.vertical(), within(0.001)); - } - - @Test - void shouldMatchAutosizedHeightWithSingleLineMeasurementMetrics() throws Exception { - TextStyle style = TextStyle.DEFAULT_STYLE; - Padding padding = Padding.of(5); - - Entity entity = new TextBuilder(entityManager) - .textWithAutoSize("In-memory PDF") - .textStyle(style) - .padding(padding) - .build(); - - ContentSize contentSize = entity.getComponent(ContentSize.class).orElseThrow(); - ContentSize measuredText = TextComponent.autoMeasureText(entity, entityManager); - PdfFont font = (PdfFont) entityManager.getFonts().getFont(style.fontName(), PdfFont.class).orElseThrow(); - PdfFont.VerticalMetrics metrics = font.verticalMetrics(style); - - assertThat(measuredText.height()).isCloseTo(metrics.lineHeight(), within(0.001)); - assertThat(contentSize.height() - padding.vertical()).isCloseTo(measuredText.height(), within(0.001)); - } - - @Test - void shouldAutosizeWithoutLayoutSystemWhenMeasurementSystemIsRegistered() { - Entity entity = new TextBuilder(entityManager) - .textWithAutoSize("Layout-free measurement") - .textStyle(TextStyle.DEFAULT_STYLE) - .build(); - - assertThat(entity.getComponent(ContentSize.class)).isPresent(); - } - - @Test - void shouldExposeFullPdfLineMetricsThroughMeasurementSystem() { - TextStyle style = TextStyle.DEFAULT_STYLE; - var measurementSystem = entityManager.getSystems() - .textMeasurement() - .orElseThrow(); - - PdfFont font = (PdfFont) entityManager.getFonts().getFont(style.fontName(), PdfFont.class).orElseThrow(); - PdfFont.VerticalMetrics expected = font.verticalMetrics(style); - var actual = measurementSystem.lineMetrics(style); - - assertThat(actual.ascent()).isCloseTo(expected.ascent(), within(0.001)); - assertThat(actual.descent()).isCloseTo(expected.descent(), within(0.001)); - assertThat(actual.leading()).isCloseTo(expected.leading(), within(0.001)); - assertThat(actual.lineHeight()).isCloseTo(expected.lineHeight(), within(0.001)); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/VContainerBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/VContainerBuilder.java deleted file mode 100644 index 934cf52e3..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/VContainerBuilder.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly; -import com.demcha.compose.testsupport.engine.assembly.container.ContainerBuilder; -import com.demcha.compose.engine.components.layout.StackAxis; -import com.demcha.compose.engine.components.renderable.VContainer; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.core.EntityManager; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class VContainerBuilder extends ContainerBuilder { - /** - * Constructs a new {@code VContainerBuilder} with the specified Entity Manager. - * - * @param entityManager The {@link EntityManager} to which the container will be added. - */ - VContainerBuilder(EntityManager entityManager, Align align) { - super(entityManager,align); - } - - /** - * Initializes the container builder with the specified alignment. - * This method calls the common creation logic from the superclass and then - * adds a {@link VContainer} component to the entity. - * - * @return This builder instance for method chaining. - */ - - - - - @Override - public void initialize() { - entity.addComponent(new VContainer()); - entity.addComponent(StackAxis.VERTICAL); - } - - -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/Box.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/Box.java deleted file mode 100644 index 22d6615d8..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/Box.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly.container; - -import java.util.List; -import java.util.UUID; - -public interface Box { - List children(); -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/BuildEntity.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/BuildEntity.java deleted file mode 100644 index 6d000b156..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/BuildEntity.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly.container; - -import com.demcha.compose.engine.components.core.Entity; - -/** - * The {@code BuildEntity} interface defines a contract for classes that are responsible for building an {@link Entity} object. - * It provides methods to build the entity and retrieve the entity being built. - *

- * Implementations of this interface are typically used in builder patterns to construct complex {@link Entity} objects step-by-step. - */ -public interface BuildEntity { - - /** - * Builds the {@link Entity} object. This method is responsible for assembling all - * components and child entities, if any, into a complete {@link Entity} object. - * - * @return The fully constructed and assembled {@link Entity} object. - */ - Entity build(); - - boolean built(); - - /** - * Returns the {@link Entity} object that is currently being built by this builder. - * - * @return The entity being built. - */ - Entity entity(); -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/ContainerBuilder.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/ContainerBuilder.java deleted file mode 100644 index 15616c4b2..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/ContainerBuilder.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly.container; - - -import com.demcha.compose.testsupport.engine.assembly.HContainerBuilder; -import com.demcha.compose.testsupport.engine.assembly.VContainerBuilder; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.core.EntityManager; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; -import java.util.Set; -import java.util.UUID; - -/** - * Base class for builders that own child entities and participate in container layout. - *

- * Container builders extend the basic entity registration behavior from - * {@link EmptyBox} and add a parent/child contract. Child entities are linked - * through {@code ParentComponent}, and the layout system later uses those links - * to compute hierarchy-aware size expansion, alignment, and placement. - *

- * - * @param the concrete container builder type - * @see HContainerBuilder - * @see VContainerBuilder - * @see EmptyBox - */ -@Slf4j -public abstract class ContainerBuilder> extends EmptyBox implements Box { - - - public ContainerBuilder(EntityManager entityManager, Align align) { - super(entityManager); - entity.addComponent(align); - } - - - /** - * Returns the UUIDs of entities currently attached as children. - * - *

The list reflects builder-time hierarchy; the layout system later uses - * the same relationship to resolve geometry.

- * - * @return child entity identifiers in insertion order - */ - public List children() { - return this.entity.getChildren(); - } - - - /** - * Replaces the container alignment metadata. - * - *

This alignment is later read during container layout to determine how - * children should be arranged inside the container's inner box.

- * - * @return the current builder - */ - public T addAlin(Align align) { - log.debug("add alin to entity {}", this.entity); - this.entity.addComponent(align); - return self(); - } - - /** - * Registers the container entity after child relationships have been declared. - * - *

Container geometry is still finalized later by layout systems; the build - * step mainly persists the entity graph into the registry.

- * - * @return the built container entity - */ - @Override - public Entity build() { - log.debug("building entity {}", this.entity); - return registerBuiltEntity(); - } - - -} - - diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EmptyBox.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EmptyBox.java deleted file mode 100644 index ce5f498c0..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EmptyBox.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly.container; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.ParentComponent; -import com.demcha.compose.engine.core.EntityManager; -import lombok.Getter; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; - -/** - * Base builder for entity-producing builders that do not manage a specialized - * child layout contract of their own. - *

- * {@code EmptyBox} creates the underlying {@code Entity}, applies shared builder - * behavior inherited from {@code EntityBuilderBase}, and knows how to register - * the final entity in the shared {@code EntityManager}. Leaf builders such as - * text, image, line, and shape builders typically extend this class directly. - * Container-oriented builders also inherit from it through {@code ContainerBuilder}. - *

- * - * @param the concrete builder type for fluent chaining - */ -@Slf4j -@Getter -@Accessors(fluent = true) -public abstract class EmptyBox extends EntityBuilderBase implements BuildEntity { - - /** - * Shared entity registry that receives the built entity. - */ - protected final EntityManager entityManager; - private boolean built; - - protected EmptyBox(EntityManager entityManager) { - this.entityManager = entityManager; - this.entity = new Entity(); - autoName(); - initialize(); - log.debug("Created entity {}", self()); - } - - T addParrent(ParentComponent parent) { - var parrentEntity = entityManager.getEntity(parent.uuid()).orElseThrow(()->{ - return new IllegalStateException("No found entity with id %s".formatted(parent.uuid())); - }); - - return addParrent(parrentEntity); - } - - T addParrent(Entity parent) { - log.debug("Add parent {}", parent); - this.entity.addComponent(new ParentComponent(parent.getUuid())); - parent.getChildren().add(this.entity.getUuid()); - return self(); - } - - public T addChild(Entity child) { - log.debug("Add child {} to parent {}", child, this.entity); - child.addComponent(new ParentComponent(this.entity)); - this.entity.getChildren().add(child.getUuid()); - return self(); - } - - @Override - public boolean built() { - return built; - } - - /** - * Finalizes and registers the current entity in the {@code EntityManager}. - * - *

Most subclasses only need to attach the right components before - * delegating to this behavior.

- */ - @Override - public Entity build() { - return registerBuiltEntity(); - } - - protected final Entity registerBuiltEntity() { - entityManager.putEntity(entity); - built = true; - return entity; - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EntityBuilderBase.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EntityBuilderBase.java deleted file mode 100644 index 5f8d24b3f..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EntityBuilderBase.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly.container; - -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.core.EntityName; -import lombok.Getter; -import lombok.experimental.Accessors; -/** - * Abstract base class for entity builders. - *

- * This class provides common functionality for building {@link Entity} objects, - * including managing components, auto-naming entities, and providing a fluent API - * for method chaining. - *

- */ -@Getter -@Accessors(fluent = true) -public abstract class EntityBuilderBase implements Layout, EntityCreator { - - /** - * The {@link Entity} instance being built by this builder. - */ - @Getter - protected Entity entity; - - - - /** - * Automatically generates a default name for the underlying {@link Entity}. - * The name is composed of the simple class name of the builder and a truncated version of the entity's ID. - * - *

- * The generated name follows the pattern: {@code BuilderClassName + first 5 characters of EntityID}. - * This name is then added to the entity as an {@link EntityName} component. - *

- */ - protected void autoName() { - String simpleName = self().getClass().getSimpleName(); - String defaultName = simpleName +"__" + entity.getUuid().toString().substring(0, 5); - entity.addComponent(new EntityName(defaultName)); - } - - /** - * Adds a {@link Component} to the underlying {@link Entity}. - * This method allows for the composition of the entity with various functional components. - * - *

- * This method delegates the actual addition of the component to the {@link Entity#addComponent(Component)} method. - *

- * - * @param component The {@link Component} to be added to the entity. - * @return The current builder instance, allowing for method chaining. - */ - @Override - public B addComponent(Component component) { - entity.addComponent(component); - return self(); - } - - - /** - * Returns the current builder instance, cast to its generic type {@code B}. - * This method is crucial for enabling fluent API and method chaining in subclasses. - * - *

- * Subclasses should implement this method to return {@code this} cast to their specific builder type. - *

- * @return The current builder instance. - */ - public B self() { - return (B) this; - } - - -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EntityCreator.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EntityCreator.java deleted file mode 100644 index ed83a0822..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/EntityCreator.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly.container; - -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.EntityName; - -/** - * An interface for creating and configuring entities. - * - * @param The type of the builder itself, allowing for method chaining. - */ -public interface EntityCreator { - - - /** - * Configures the entity with the specified name. - * - * @param name The name of the entity. - * @return The builder instance for method chaining. - */ - default B entityName(EntityName name) { - addComponent(name); - return self(); - } - - /** - * Configures the entity with the specified name. - * - * @param name The name of the entity. - * @return The builder instance for method chaining. - */ - - default B entityName(String name) { - entityName(new EntityName(name)); - return self(); - } - - /** - * This method provides an initialization step for components within the entity, - * ensuring that all components are properly associated with this entity. - */ - - void initialize(); - - - /** - * add component in to our entity - * - * @param component The component to add. - * @return The builder instance for method chaining. - */ - B addComponent(Component component); - - /** - * Returns the builder instance itself. - * - * @return - */ - B self(); -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/Layout.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/Layout.java deleted file mode 100644 index 7a8f91fae..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/Layout.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly.container; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.HAnchor; -import com.demcha.compose.engine.components.layout.Layer; -import com.demcha.compose.engine.components.layout.VAnchor; -import com.demcha.compose.engine.components.layout.coordinator.Position; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; - -/** - * This interface defines a set of default methods for configuring layout-related properties - * of a component builder. It is intended to be implemented by builder classes. - * - * @param The type of the builder implementing this interface. - * - *

The interface provides methods for setting various layout properties such as: - *

    - *
  • {@code layer}: The z-order of the component.
  • - *
  • {@code position}: The absolute position (x, y) of the component.
  • - *
  • {@code anchor}: How the component is anchored within its parent.
  • - *
  • {@code size}: The width and height of the component.
  • - *
  • {@code margin}: The external spacing around the component.
  • - *
  • {@code padding}: The internal spacing within the component.
  • - *
- * Each method returns the builder instance itself, allowing for method chaining.

- */ -public interface Layout { - default B layer(Layer layer) { - entity().addComponent(layer); - return self(); - } - - default B layer(int layer) { - layer(new Layer(layer)); - return self(); - } - - default B position(Position position) { - entity().addComponent(position); - return self(); - } - - default B position(double x, double y) { - position(new Position(x, y)); - return self(); - } - - default B anchor(Anchor anchor) { - entity().addComponent(anchor); - return self(); - } - - default B anchor(HAnchor hAnchor, VAnchor vAnchor) { - anchor(new Anchor(hAnchor, vAnchor)); - return self(); - } - - default B size(ContentSize size) { - entity().addComponent(size); - return self(); - } - - - default B size(double width, double height) { - size(new ContentSize(width, height)); - return self(); - } - - default B margin(Margin margin) { - entity().addComponent(margin); - return self(); - } - - default B margin(double top, double right, double bottom, double left) { - margin(new Margin(top, right, bottom, left)); - return self(); - } - - default B padding(Padding padding) { - entity().addComponent(padding); - return self(); - } - - default B padding(double top, double right, double bottom, double left) { - padding(new Padding(top, right, bottom, left)); - return self(); - } - - Entity entity(); - - /** - * Returns the current builder instance, cast to its generic type {@code B}. - * This method is used to enable method chaining in subclasses. - */ - B self(); - -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/ShapeBuilderBase.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/ShapeBuilderBase.java deleted file mode 100644 index b12022807..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/ShapeBuilderBase.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly.container; - -import com.demcha.compose.engine.components.content.shape.Stroke; -import com.demcha.compose.engine.components.content.shape.CornerRadius; -import com.demcha.compose.engine.components.content.shape.FillColor; -import com.demcha.compose.engine.components.renderable.Rectangle; -import com.demcha.compose.engine.components.style.ComponentColor; -import com.demcha.compose.engine.core.EntityManager; - -import java.awt.*; - - -/** - * An abstract builder class for creating shapes within a Entity Manager. - * This class provides methods to add various shape-related components like rectangles, corner radii, fill colors, and strokes. - * @param The type of the concrete builder extending this abstract class, allowing for method chaining. - */ -public abstract class ShapeBuilderBase extends EmptyBox { - - public ShapeBuilderBase(EntityManager entityManager) { - super(entityManager); - } - - /** - * Adds a rectangle component to the shape. - * - * @param rectangle The Rectangle object to add. - * @return The current builder instance for method chaining. - */ - public T rectangle(Rectangle rectangle) { - return addComponent(rectangle); - } - - /** - * Adds a default rectangle component to the shape. - * - * @return The current builder instance for method chaining. - */ - public T rectangle() { - return rectangle(new Rectangle()); - } - - /** - * Sets the corner radius for the shape. - * - * @param radius The CornerRadius object specifying the corner radius. - * @return The current builder instance for method chaining. - */ - public T cornerRadius(CornerRadius radius) { - return addComponent(radius); - } - - /** - * Sets the corner radius for the shape using a double value. - * - * @param radius The double value representing the corner radius. - * @return The current builder instance for method chaining. - */ - public T cornerRadius(double radius) { - return cornerRadius(new CornerRadius(radius)); - } - - /** - * Sets the fill color for the shape. - * - * @param fillColor The FillColor object to set. - * @return The current builder instance for method chaining. - */ - public T fillColor(FillColor fillColor) { - return addComponent(fillColor); - } - - /** - * Sets the fill color for the shape using a AWT Color object. - * - * @param fillColor The AWT Color object to set. - * @return The current builder instance for method chaining. - */ - public T fillColor(Color fillColor) { - return fillColor(new FillColor(fillColor)); - } - - /** - * Sets the fill color for the shape using a ComponentColor object. - * - * @param fillColor The ComponentColor object to set. - * @return The current builder instance for method chaining. - */ - public T fillColor(ComponentColor fillColor) { - return fillColor(new FillColor(fillColor)); - } - - /** - * Sets the fill color for the shape using individual RGB integer values. - * - * @param r The red component (0-255). - * @param g The green component (0-255). - * @param b The blue component (0-255). - * @return The current builder instance for method chaining. - */ - public T fillColor(int r, int g, int b) { - return fillColor(new FillColor(new Color(r, g, b))); - } - - /** - * Sets the stroke (outline) for the shape. - * - * @param stroke The Stroke object to set. - * @return The current builder instance for method chaining. - */ - public T stroke(Stroke stroke) { - return addComponent(stroke); - } -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/StackAxis.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/StackAxis.java deleted file mode 100644 index 33d0e099d..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/StackAxis.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.demcha.compose.testsupport.engine.assembly.container; - -import com.demcha.compose.engine.components.core.Component; - -public enum StackAxis implements Component { - VERTICAL, // элементы идут сверху вниз (как колонка) - HORIZONTAL, // элементы идут слева направо (как строка) - REVERSE_VERTICAL, - REVERSE_HORIZONTAL, - DEFAULT -} diff --git a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/package-info.java b/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/package-info.java deleted file mode 100644 index 41ec84b87..000000000 --- a/render-pdf/src/test/java/com/demcha/compose/testsupport/engine/assembly/container/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Internal base builders and contracts for entity containers. - * - *

Ownership: Owned by the shared engine layout layer because these classes define parent-child materialization rules.

- *

Extension rules: Extend with care: new containers must document sizing, pagination, and child ownership semantics.

- */ -package com.demcha.compose.testsupport.engine.assembly.container; \ No newline at end of file diff --git a/src/test/java/com/demcha/compose/engine/components/content/ImageDataCacheTest.java b/src/test/java/com/demcha/compose/engine/components/content/ImageDataCacheTest.java new file mode 100644 index 000000000..b6edd1fe4 --- /dev/null +++ b/src/test/java/com/demcha/compose/engine/components/content/ImageDataCacheTest.java @@ -0,0 +1,83 @@ +package com.demcha.compose.engine.components.content; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.imageio.ImageIO; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit coverage for {@link ImageData} decoding and the shared + * {@link ImageSourceCache}: repeated loads of the same source reuse cached bytes + * and metadata instead of re-decoding. This is the image path the canonical DSL + * ({@code addImage} / {@code ImageNode}) and the fixed-layout backend rely on. + */ +class ImageDataCacheTest { + + @TempDir + Path tempDir; + + @BeforeEach + void setUp() { + ImageSourceCache.clearForTests(); + } + + @AfterEach + void tearDown() { + ImageSourceCache.clearForTests(); + } + + @Test + void repeatedPathLoadsReuseTheCachedSourceBytes() throws Exception { + Path imagePath = tempDir.resolve("cached-source.png"); + Files.write(imagePath, pngBytes(120, 60, new Color(24, 92, 160))); + + ImageData firstLoad = ImageData.create(imagePath); + + // Overwrite the file on disk; the cache is keyed by path, so the second + // load must return the first (cached) bytes and metadata, not re-read. + Files.write(imagePath, pngBytes(48, 24, new Color(182, 44, 64))); + ImageData secondLoad = ImageData.create(imagePath); + + assertThat(ImageSourceCache.sourceCacheSize()).isEqualTo(1); + assertThat(ImageSourceCache.metadataCacheSize()).isEqualTo(1); + assertThat(firstLoad.getFingerprint()).isEqualTo(secondLoad.getFingerprint()); + assertThat(secondLoad.getMetadata().width()).isEqualTo(120); + assertThat(secondLoad.getMetadata().height()).isEqualTo(60); + } + + @Test + void repeatedByteLoadsDecodeMetadataOnlyOnce() throws Exception { + byte[] bytes = pngBytes(200, 100, new Color(42, 52, 110)); + + ImageData first = ImageData.create(bytes); + ImageData second = ImageData.create(bytes); + + assertThat(first.getFingerprint()).isEqualTo(second.getFingerprint()); + assertThat(ImageSourceCache.metadataCacheSize()).isEqualTo(1); + assertThat(ImageSourceCache.metadataDecodeCount()).isEqualTo(1); + } + + private static byte[] pngBytes(int width, int height, Color color) throws Exception { + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = image.createGraphics(); + try { + graphics.setColor(color); + graphics.fillRect(0, 0, width, height); + } finally { + graphics.dispose(); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageIO.write(image, "png", out); + return out.toByteArray(); + } +} diff --git a/src/test/java/com/demcha/compose/engine/core/SystemRegistryTest.java b/src/test/java/com/demcha/compose/engine/core/SystemRegistryTest.java deleted file mode 100644 index 34b794cf4..000000000 --- a/src/test/java/com/demcha/compose/engine/core/SystemRegistryTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.demcha.compose.engine.core; - -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.measurement.TextMeasurementSystem; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -/** - * Unit coverage for the dedicated text-measurement service slot on - * {@link SystemRegistry}. This seam is what lets {@code TextMeasurementSystem} - * stay decoupled from {@link SystemECS}: the measurement system is a service - * provider exposed on demand, not a {@code process()}-driven ECS system, so it is - * held out-of-band from the {@code systems} map. - */ -class SystemRegistryTest { - - @Test - void textMeasurementIsEmptyWhenNoneRegistered() { - assertThat(new SystemRegistry().textMeasurement()).isEmpty(); - } - - @Test - void registerTextMeasurementExposesTheSameInstance() { - SystemRegistry registry = new SystemRegistry(); - TextMeasurementSystem service = new StubMeasurement(); - - registry.registerTextMeasurement(service); - - assertThat(registry.textMeasurement()).containsSame(service); - } - - @Test - void registerTextMeasurementRejectsNull() { - assertThatNullPointerException() - .isThrownBy(() -> new SystemRegistry().registerTextMeasurement(null)) - .withMessageContaining("textMeasurement"); - } - - @Test - void measurementServiceIsNotEnrolledAsAProcessDrivenSystem() { - SystemRegistry registry = new SystemRegistry(); - - registry.registerTextMeasurement(new StubMeasurement()); - - // It must stay out of the SystemECS map so the processSystems() loop never - // calls process() on it — that is the whole point of the dedicated slot. - assertThat(registry.getStream().toList()).isEmpty(); - } - - private static final class StubMeasurement implements TextMeasurementSystem { - @Override - public ContentSize measure(TextStyle style, String text) { - return new ContentSize(0.0, 0.0); - } - - @Override - public LineMetrics lineMetrics(TextStyle style) { - return new LineMetrics(0.0, 0.0, 0.0); - } - } -} diff --git a/src/test/java/com/demcha/compose/engine/pagination/ParentContainerUpdaterTest.java b/src/test/java/com/demcha/compose/engine/pagination/ParentContainerUpdaterTest.java deleted file mode 100644 index 13617c8c2..000000000 --- a/src/test/java/com/demcha/compose/engine/pagination/ParentContainerUpdaterTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.demcha.compose.engine.pagination; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.ParentComponent; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.core.EntityManager; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class ParentContainerUpdaterTest { - - @Test - void shouldIncreaseParentSizesWithoutMovingParents() { - Graph graph = graph(); - - boolean updated = ParentContainerUpdater.updateParentContainerSize(graph.child(), graph.manager(), -15); - - assertThat(updated).isFalse(); - assertThat(graph.parent().require(ContentSize.class).height()).isEqualTo(45.0); - assertThat(graph.parent().require(ComputedPosition.class).y()).isEqualTo(80.0); - assertThat(graph.grandParent().require(ContentSize.class).height()).isEqualTo(55.0); - assertThat(graph.grandParent().require(ComputedPosition.class).y()).isEqualTo(100.0); - } - - @Test - void shouldIncreaseParentSizesAndMoveAncestorsWhenUpdatingPosition() { - Graph graph = graph(); - - boolean updated = ParentContainerUpdater.updateParentContainer(graph.child(), graph.manager(), -15); - - assertThat(updated).isFalse(); - assertThat(graph.parent().require(ContentSize.class).height()).isEqualTo(45.0); - assertThat(graph.parent().require(ComputedPosition.class).y()).isEqualTo(65.0); - assertThat(graph.grandParent().require(ContentSize.class).height()).isEqualTo(55.0); - assertThat(graph.grandParent().require(ComputedPosition.class).y()).isEqualTo(85.0); - } - - @SuppressWarnings("deprecation") - @Test - void shouldDelegateDeprecatedEntityParentUpdateWrappers() { - Graph graph = graph(); - Offset offset = new Offset(); - offset.incrementY(-10); - - boolean updated = graph.child().updateParentContainer(graph.manager(), offset); - - assertThat(updated).isFalse(); - assertThat(graph.parent().require(ContentSize.class).height()).isEqualTo(40.0); - assertThat(graph.parent().require(ComputedPosition.class).y()).isEqualTo(70.0); - assertThat(graph.grandParent().require(ContentSize.class).height()).isEqualTo(50.0); - assertThat(graph.grandParent().require(ComputedPosition.class).y()).isEqualTo(90.0); - } - - @SuppressWarnings("deprecation") - @Test - void shouldDelegateDeprecatedEntitySelfResizeWrapper() { - Graph graph = graph(); - - boolean updated = graph.child().updateEntitySize(graph.manager(), -12); - - assertThat(updated).isFalse(); - assertThat(graph.child().require(ContentSize.class).height()).isEqualTo(22.0); - assertThat(graph.child().require(ComputedPosition.class).y()).isEqualTo(60.0); - assertThat(graph.parent().require(ContentSize.class).height()).isEqualTo(42.0); - assertThat(graph.parent().require(ComputedPosition.class).y()).isEqualTo(80.0); - assertThat(graph.grandParent().require(ContentSize.class).height()).isEqualTo(52.0); - } - - private static Graph graph() { - EntityManager manager = new EntityManager(); - - Entity grandParent = sizedEntity(0, 100, 200, 40); - Entity parent = sizedEntity(10, 80, 120, 30); - Entity child = sizedEntity(20, 60, 60, 10); - - parent.addComponent(new ParentComponent(grandParent.getUuid())); - child.addComponent(new ParentComponent(parent.getUuid())); - grandParent.getChildren().add(parent.getUuid()); - parent.getChildren().add(child.getUuid()); - - manager.putEntity(grandParent); - manager.putEntity(parent); - manager.putEntity(child); - return new Graph(manager, grandParent, parent, child); - } - - private static Entity sizedEntity(double x, double y, double width, double height) { - Entity entity = new Entity(); - entity.addComponent(new ComputedPosition(x, y)); - entity.addComponent(new ContentSize(width, height)); - return entity; - } - - private record Graph(EntityManager manager, Entity grandParent, Entity parent, Entity child) { - } -} diff --git a/src/test/java/com/demcha/compose/engine/render/EntityRenderOrderTest.java b/src/test/java/com/demcha/compose/engine/render/EntityRenderOrderTest.java deleted file mode 100644 index d24998bd8..000000000 --- a/src/test/java/com/demcha/compose/engine/render/EntityRenderOrderTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.demcha.compose.engine.render; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.core.EntityManager; -import org.junit.jupiter.api.Test; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -class EntityRenderOrderTest { - - @Test - void shouldSortByResolvedCoordinatesUsingDefaultZeroMargin() { - EntityManager manager = new EntityManager(); - Entity baseEntity = positionedEntity(10, 50, null); - Entity shiftedRightEntity = positionedEntity(10, 50, Margin.left(5)); - Entity raisedEntity = positionedEntity(10, 49, Margin.bottom(2)); - - manager.putEntity(baseEntity); - manager.putEntity(shiftedRightEntity); - manager.putEntity(raisedEntity); - - LinkedHashMap ordered = EntityRenderOrder.sortByRenderingPosition( - manager, - List.of(shiftedRightEntity.getUuid(), baseEntity.getUuid(), raisedEntity.getUuid())); - - assertThat(ordered.keySet()) - .containsExactly(raisedEntity.getUuid(), baseEntity.getUuid(), shiftedRightEntity.getUuid()); - } - - @Test - void shouldPreserveOriginalLayerOrderWhenRenderingCoordinatesMatch() { - EntityManager manager = new EntityManager(); - Entity firstEntity = positionedEntity(10, 50, null); - Entity secondEntity = positionedEntity(10, 50, Margin.zero()); - - manager.putEntity(firstEntity); - manager.putEntity(secondEntity); - - LinkedHashMap ordered = EntityRenderOrder.sortByRenderingPosition( - manager, - List.of(secondEntity.getUuid(), firstEntity.getUuid())); - - assertThat(ordered.keySet()) - .containsExactly(secondEntity.getUuid(), firstEntity.getUuid()); - } - - @Test - void shouldSkipMissingAndDuplicateEntityIds() { - EntityManager manager = new EntityManager(); - Entity lowerEntity = positionedEntity(0, 0, null); - Entity upperEntity = positionedEntity(0, 10, null); - UUID missingEntityId = UUID.randomUUID(); - - manager.putEntity(lowerEntity); - manager.putEntity(upperEntity); - - LinkedHashMap ordered = EntityRenderOrder.sortByRenderingPosition( - manager, - List.of(missingEntityId, lowerEntity.getUuid(), lowerEntity.getUuid(), upperEntity.getUuid())); - - assertThat(ordered.keySet()) - .containsExactly(upperEntity.getUuid(), lowerEntity.getUuid()); - assertThat(ordered).hasSize(2); - } - - private static Entity positionedEntity(double x, double y, Margin margin) { - Entity entity = new Entity(); - entity.addComponent(new ComputedPosition(x, y)); - if (margin != null) { - entity.addComponent(margin); - } - return entity; - } -}