Browse Source

Refactor semantic structure and add conformance testing framework

Marcin Ziąbek 1 month ago
parent
commit
31965ae189

+ 0 - 1
Source/QuestPDF.ConformanceTests/AssemblyInfo.cs

@@ -1 +0,0 @@
-[assembly: NUnit.Framework.Parallelizable(ParallelScope.All)]

+ 11 - 38
Source/QuestPDF.ConformanceTests/ImageTests.cs

@@ -1,23 +1,13 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF.ConformanceTests;
 
-public class ImageTests
+internal class ImageTests : ConformanceTestBase
 {
-    [Test]
-    [Ignore("For manual testing purposes only")]
-    public void GenerateAndShow()
-    {
-        GetDocumentUnderTest()
-            .WithSettings(new DocumentSettings
-            {
-                PDFUA_Conformance = PDFUA_Conformance.PDFUA_1
-            })
-            .GeneratePdfAndShow();
-    }
-
     [OneTimeSetUp]
     public void Setup()
     {
@@ -28,33 +18,11 @@ public class ImageTests
 
         ImageHelpers.ConvertImageIccColorSpaceProfileToVersion2(sourceImagePath, targetImagePath);
     }
-    
-    [Test, TestCaseSource(typeof(TestHelpers), nameof(TestHelpers.PDFA_ConformanceLevels))]
-    public void Test_PDFA(PDFA_Conformance conformance)
-    {
-        var useImageWithIcc2 = conformance is PDFA_Conformance.PDFA_1A or PDFA_Conformance.PDFA_1B;
-        
-        GetDocumentUnderTest(useImageWithIcc2)
-            .WithSettings(new DocumentSettings
-            {
-                PDFA_Conformance = conformance
-            })
-            .TestConformance();
-    }
-    
-    [Test, TestCaseSource(typeof(TestHelpers), nameof(TestHelpers.PDFUA_ConformanceLevels))]
-    public void Test_PDFUA(PDFUA_Conformance conformance)
-    {
-        GetDocumentUnderTest()
-            .WithSettings(new DocumentSettings
-            {
-                PDFUA_Conformance = conformance
-            })
-            .TestConformance();
-    }
 
-    private Document GetDocumentUnderTest(bool useImageWithIcc2 = false)
+    protected override Document GetDocumentUnderTest()
     {
+        var useImageWithIcc2 = TestContext.CurrentContext.Test.Arguments.FirstOrDefault() is PDFA_Conformance.PDFA_1A or PDFA_Conformance.PDFA_1B;
+        
         var imagePath = useImageWithIcc2 
             ? Path.Combine("Resources", "photo-icc2.jpeg") 
             : Path.Combine("Resources", "photo.jpeg");
@@ -98,4 +66,9 @@ public class ImageTests
                 Subject = "Images"
             });
     }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        throw new NotImplementedException();
+    }
 }

+ 67 - 0
Source/QuestPDF.ConformanceTests/LineTests.cs

@@ -0,0 +1,67 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class LineTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .Column(column =>
+                        {
+                            column.Spacing(25);
+
+                            column.Item().Text("Line tests");
+
+                            column.Item()
+                                .LineHorizontal(6)
+                                .LineColor(Colors.Red.Medium);
+                            
+                            column.Item()
+                                .LineHorizontal(6)
+                                .LineColor(Colors.Green.Medium)
+                                .LineDashPattern([6, 6, 12, 6]);
+                            
+                            column.Item()
+                                .Height(150)
+                                .LineVertical(6)
+                                .LineGradient([ Colors.Blue.Lighten2, Colors.Blue.Darken2 ]);
+                        });
+                });
+            })
+            .WithMetadata(new DocumentMetadata
+            {
+                Language = "en-US",
+                Title = "Conformance Test", 
+                Subject = "Table of Contents"
+            });
+    }
+    
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return new SemanticTreeNode
+        {
+            NodeId = 1,
+            Type = "Document",
+            Children =
+            {
+                new SemanticTreeNode
+                {
+                    NodeId = 2,
+                    Type = "P"
+                }
+            }
+        };
+    }
+}

BIN
Source/QuestPDF.ConformanceTests/Resources/photo.jpeg


+ 9 - 32
Source/QuestPDF.ConformanceTests/TableOfContentsTests.cs

@@ -1,42 +1,14 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF.ConformanceTests;
 
-[TestFixture]
-public class TableOfContentsTests
+internal class TableOfContentsTests : ConformanceTestBase
 {
-    [Test]
-    [Ignore("For manual testing purposes only")]
-    public void GenerateAndShow()
-    {
-        GetDocumentUnderTest().GeneratePdfAndShow();
-    }
-    
-    [Test, TestCaseSource(typeof(TestHelpers), nameof(TestHelpers.PDFA_ConformanceLevels))]
-    public void Test_PDFA(PDFA_Conformance conformance)
-    {
-        GetDocumentUnderTest()
-            .WithSettings(new DocumentSettings
-            {
-                PDFA_Conformance = conformance
-            })
-            .TestConformance();
-    }
-    
-    [Test, TestCaseSource(typeof(TestHelpers), nameof(TestHelpers.PDFUA_ConformanceLevels))]
-    public void Test_PDFUA(PDFUA_Conformance conformance)
-    {
-        GetDocumentUnderTest()
-            .WithSettings(new DocumentSettings
-            {
-                PDFUA_Conformance = conformance
-            })
-            .TestConformance();
-    }
-
-    private Document GetDocumentUnderTest()
+    protected override Document GetDocumentUnderTest()
     {
         return Document
             .Create(document =>
@@ -76,6 +48,11 @@ public class TableOfContentsTests
             });
     }
 
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        throw new NotImplementedException();
+    }
+
     private void GenerateTableOfContentsSection(IContainer container)
     {
         container

+ 58 - 0
Source/QuestPDF.ConformanceTests/TestEngine/ConformanceTestBase.cs

@@ -0,0 +1,58 @@
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests.TestEngine;
+
+[TestFixture]
+[Parallelizable(ParallelScope.All)]
+internal abstract class ConformanceTestBase
+{
+    public static readonly IEnumerable<PDFA_Conformance> PDFA_ConformanceLevels = Enum.GetValues<PDFA_Conformance>().Skip(1);
+    public static readonly IEnumerable<PDFUA_Conformance> PDFUA_ConformanceLevels = Enum.GetValues<PDFUA_Conformance>().Skip(1);
+    
+    [Test]
+    [Explicit("Manual debugging only (override to enable)")]
+    public void GenerateAndShow()
+    {
+        GetDocumentUnderTest()
+            .WithSettings(new DocumentSettings
+            {
+                PDFA_Conformance = PDFA_Conformance.PDFA_3A
+            })
+            .GeneratePdfAndShow();
+    }
+    
+    [Test, TestCaseSource(nameof(PDFA_ConformanceLevels))]
+    public void Test_PDFA(PDFA_Conformance conformance)
+    {
+        GetDocumentUnderTest()
+            .WithSettings(new DocumentSettings
+            {
+                PDFA_Conformance = conformance
+            })
+            .TestConformanceWithVeraPdf();
+    }
+    
+    [Test, TestCaseSource(nameof(PDFUA_ConformanceLevels))]
+    public void Test_PDFUA(PDFUA_Conformance conformance)
+    {
+        GetDocumentUnderTest()
+            .WithSettings(new DocumentSettings
+            {
+                PDFUA_Conformance = conformance
+            })
+            .TestConformanceWithVeraPdf();
+    }
+    
+    [Test]
+    public void TestSemanticMeaning()
+    {
+        var expectedSemanticTree = GetExpectedSemanticTree();
+        GetDocumentUnderTest().TestSemanticTree(expectedSemanticTree);
+    }
+
+    protected abstract Document GetDocumentUnderTest();
+    
+    protected abstract SemanticTreeNode? GetExpectedSemanticTree();
+}

+ 1 - 1
Source/QuestPDF.ConformanceTests/ImageHelpers.cs → Source/QuestPDF.ConformanceTests/TestEngine/ImageHelpers.cs

@@ -1,6 +1,6 @@
 using ImageMagick;
 
-namespace QuestPDF.ConformanceTests;
+namespace QuestPDF.ConformanceTests.TestEngine;
 
 public static class ImageHelpers
 {

+ 191 - 0
Source/QuestPDF.ConformanceTests/TestEngine/SemanticAwareDrawingCanvas.cs

@@ -0,0 +1,191 @@
+using QuestPDF.Drawing;
+using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+using QuestPDF.Skia.Text;
+
+namespace QuestPDF.ConformanceTests.TestEngine;
+
+internal class SemanticAwareDocumentCanvas : IDocumentCanvas
+{
+    internal SemanticTreeNode? SemanticTree { get; private set; }
+    private SemanticAwareDrawingCanvas DrawingCanvas { get; } = new();
+    
+    public void SetSemanticTree(SemanticTreeNode? semanticTree)
+    {
+        SemanticTree = semanticTree;
+    }
+
+    public void BeginDocument()
+    {
+        
+    }
+
+    public void EndDocument()
+    {
+        
+    }
+
+    public void BeginPage(Size size)
+    {
+        
+    }
+
+    public void EndPage()
+    {
+        
+    }
+
+    public IDrawingCanvas GetDrawingCanvas()
+    {
+        return DrawingCanvas;
+    }
+}
+
+internal class SemanticAwareDrawingCanvas : IDrawingCanvas
+{
+    private int CurrentSemanticNodeId { get; set; }
+    
+    public DocumentPageSnapshot GetSnapshot()
+    {
+        return new DocumentPageSnapshot();
+    }
+
+    public void DrawSnapshot(DocumentPageSnapshot snapshot)
+    {
+        
+    }
+
+    public void Save()
+    {
+        
+    }
+
+    public void Restore()
+    {
+        
+    }
+
+    public void SetZIndex(int index)
+    {
+        
+    }
+
+    public int GetZIndex()
+    {
+        return 0;
+    }
+
+    public SkCanvasMatrix GetCurrentMatrix()
+    {
+        return SkCanvasMatrix.Identity;
+    }
+
+    public void SetMatrix(SkCanvasMatrix matrix)
+    {
+        
+    }
+
+    public void Translate(Position vector)
+    {
+        
+    }
+
+    public void Scale(float scaleX, float scaleY)
+    {
+        
+    }
+
+    public void Rotate(float angle)
+    {
+        
+    }
+
+    public void DrawLine(Position start, Position end, SkPaint paint)
+    {
+        if (CurrentSemanticNodeId != SkSemanticNodeSpecialId.LayoutArtifact)
+            Assert.Fail("Detected a line drawing operation outside of layout artifact");
+    }
+
+    public void DrawRectangle(Position vector, Size size, SkPaint paint)
+    {
+        if (CurrentSemanticNodeId != SkSemanticNodeSpecialId.BackgroundArtifact)
+            Assert.Fail("Detected a rectangle drawing operation outside of layout artifact");
+    }
+
+    public void DrawComplexBorder(SkRoundedRect innerRect, SkRoundedRect outerRect, SkPaint paint)
+    {
+        if (CurrentSemanticNodeId != SkSemanticNodeSpecialId.LayoutArtifact)
+            Assert.Fail("Detected a complex-border drawing operation outside of layout artifact");
+    }
+
+    public void DrawShadow(SkRoundedRect shadowRect, SkBoxShadow shadow)
+    {
+        if (CurrentSemanticNodeId != SkSemanticNodeSpecialId.BackgroundArtifact)
+            Assert.Fail("Detected a shadow drawing operation outside of background artifact");
+    }
+
+    public void DrawParagraph(SkParagraph paragraph, int lineFrom, int lineTo)
+    {
+        
+    }
+
+    public void DrawImage(SkImage image, Size size)
+    {
+        
+    }
+
+    public void DrawPicture(SkPicture picture)
+    {
+        
+    }
+
+    public void DrawSvgPath(string path, Color color)
+    {
+        
+    }
+
+    public void DrawSvg(SkSvgImage svgImage, Size size)
+    {
+        
+    }
+
+    public void DrawOverflowArea(SkRect area)
+    {
+        
+    }
+
+    public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace)
+    {
+        
+    }
+
+    public void ClipRectangle(SkRect clipArea)
+    {
+        
+    }
+
+    public void ClipRoundedRectangle(SkRoundedRect clipArea)
+    {
+        
+    }
+
+    public void DrawHyperlink(Size size, string url, string? description)
+    {
+        
+    }
+
+    public void DrawSectionLink(Size size, string sectionName, string? description)
+    {
+        
+    }
+
+    public void DrawSection(string sectionName)
+    {
+        
+    }
+
+    public void SetSemanticNodeId(int nodeId)
+    {
+        CurrentSemanticNodeId = nodeId;
+    }
+}

+ 60 - 0
Source/QuestPDF.ConformanceTests/TestEngine/SemanticTreeTestRunner.cs

@@ -0,0 +1,60 @@
+using QuestPDF.Drawing;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests.TestEngine;
+
+internal static class SemanticTreeTestRunner
+{
+    public static void TestSemanticTree(this IDocument document, SemanticTreeNode? semanticTreeRootNode)
+    {
+        Settings.EnableCaching = false;
+        Settings.EnableDebugging = false;
+
+        var canvas = new SemanticAwareDocumentCanvas();
+        var settings = new DocumentSettings { PDFA_Conformance = PDFA_Conformance.PDFA_3A };
+        DocumentGenerator.RenderDocument(canvas, document, settings);
+
+        CompareSemanticTrees(canvas.SemanticTree, semanticTreeRootNode);
+    }
+
+    private static void CompareSemanticTrees(SemanticTreeNode? actual, SemanticTreeNode? expected)
+    {
+        if (expected == null && actual == null)
+            return;
+
+        if (expected == null)
+            Assert.Fail($"Expected null but got node of type '{actual?.Type}'");
+
+        if (actual == null)
+            Assert.Fail($"Expected node of type '{expected.Type}' but got null");
+
+        Assert.That(actual.Type, Is.EqualTo(expected.Type), $"Node type mismatch");
+        Assert.That(actual.Alt, Is.EqualTo(expected.Alt), $"Alt mismatch for node type '{expected.Type}'");
+        Assert.That(actual.Lang, Is.EqualTo(expected.Lang), $"Lang mismatch for node type '{expected.Type}'");
+
+        CompareAttributes(actual.Attributes, expected.Attributes, expected.Type);
+
+        Assert.That(actual.Children.Count, Is.EqualTo(expected.Children.Count), $"Children count mismatch for node type '{expected.Type}'");
+
+        foreach (var (actualChild, expectedChild) in actual.Children.Zip(expected.Children))
+            CompareSemanticTrees(actualChild, expectedChild);
+        
+        static void CompareAttributes(ICollection<SemanticTreeNode.Attribute> actual, ICollection<SemanticTreeNode.Attribute> expected, string nodeType)
+        {
+            Assert.That(actual.Count, Is.EqualTo(expected.Count), $"Attribute count mismatch for node type '{nodeType}'");
+
+            var actualList = actual.OrderBy(a => a.Owner).ThenBy(a => a.Name).ToList();
+            var expectedList = expected.OrderBy(a => a.Owner).ThenBy(a => a.Name).ToList();
+
+            for (var i = 0; i < expectedList.Count; i++)
+            {
+                var actualAttr = actualList[i];
+                var expectedAttr = expectedList[i];
+
+                Assert.That(actualAttr.Owner, Is.EqualTo(expectedAttr.Owner), $"Attribute owner mismatch for node type '{nodeType}'");
+                Assert.That(actualAttr.Name, Is.EqualTo(expectedAttr.Name), $"Attribute name mismatch for node type '{nodeType}'");
+                Assert.That(actualAttr.Value, Is.EqualTo(expectedAttr.Value), $"Attribute value mismatch for '{expectedAttr.Owner}:{expectedAttr.Name}' in node type '{nodeType}'");
+            }
+        }
+    }
+}

+ 3 - 4
Source/QuestPDF.ConformanceTests/ConformanceToolRunner.cs → Source/QuestPDF.ConformanceTests/TestEngine/VeraPdfConformanceTestRunner.cs

@@ -4,9 +4,9 @@ using System.Text.Json;
 using QuestPDF.Fluent;
 using QuestPDF.Infrastructure;
 
-namespace QuestPDF.ConformanceTests;
+namespace QuestPDF.ConformanceTests.TestEngine;
 
-public static class ConformanceToolRunner
+public static class VeraPdfConformanceTestRunner
 {
     public class ValidationResult
     {
@@ -40,9 +40,8 @@ public static class ConformanceToolRunner
             return errorMessage.ToString();
         }
     }
-
     
-    public static void TestConformance(this IDocument document)
+    public static void TestConformanceWithVeraPdf(this IDocument document)
     {
         var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.pdf");
         document.GeneratePdf(filePath);

+ 0 - 15
Source/QuestPDF.ConformanceTests/TestHelpers.cs

@@ -1,15 +0,0 @@
-using ImageMagick;
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.ConformanceTests;
-
-public class TestHelpers
-{
-    public static readonly IEnumerable<PDFA_Conformance> PDFA_ConformanceLevels = Enum.GetValues<PDFA_Conformance>().Skip(1);
-    public static readonly IEnumerable<PDFUA_Conformance> PDFUA_ConformanceLevels = Enum.GetValues<PDFUA_Conformance>().Skip(1);
-
-    static TestHelpers()
-    {
-        QuestPDF.Settings.License = LicenseType.Community;
-    }
-}

+ 2 - 3
Source/QuestPDF.ConformanceTests/TestsSetup.cs

@@ -1,12 +1,11 @@
-using QuestPDF.Helpers;
+using System.Runtime.CompilerServices;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF.ConformanceTests
 {
-    [SetUpFixture]
     public class TestsSetup
     {
-        [OneTimeSetUp]
+        [ModuleInitializer]
         public static void Setup()
         {
             QuestPDF.Settings.License = LicenseType.Community;

+ 1 - 1
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -102,7 +102,7 @@ namespace QuestPDF.Drawing
             return canvas.GetContent();
         }
 
-        private static void RenderDocument(IDocumentCanvas canvas, IDocument document, DocumentSettings settings)
+        internal static void RenderDocument(IDocumentCanvas canvas, IDocument document, DocumentSettings settings)
         {
             if (document is MergedDocument mergedDocument)
             {

+ 1 - 1
Source/QuestPDF/Drawing/SemanticTreeManager.cs

@@ -2,7 +2,7 @@ using System.Collections.Generic;
 
 namespace QuestPDF.Drawing;
 
-class SemanticTreeNode
+internal class SemanticTreeNode
 {
     public int NodeId { get; set; }
     public string Type { get; set; } = "";

+ 1 - 0
Source/QuestPDF/QuestPDF.csproj

@@ -39,6 +39,7 @@
         <InternalsVisibleTo Include="QuestPDF.UnitTests" />
         <InternalsVisibleTo Include="QuestPDF.LayoutTests" />
         <InternalsVisibleTo Include="QuestPDF.VisualTests" />
+        <InternalsVisibleTo Include="QuestPDF.ConformanceTests" />
     </ItemGroup>
 
     <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">