Browse Source

Add basic conformance tests (proof of concept)

Marcin Ziąbek 1 month ago
parent
commit
4b967d67df

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

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

+ 663 - 0
Source/QuestPDF.ConformanceTests/ConformanceTestDocument.cs

@@ -0,0 +1,663 @@
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests;
+
+class ConformanceTestDocument : IDocument
+{
+    private TextStyle TextStyleHeader1 = TextStyle.Default.FontSize(24).Bold().FontColor(Colors.Blue.Darken4);
+    private TextStyle TextStyleHeader2 = TextStyle.Default.FontSize(18).Bold().FontColor(Colors.Blue.Darken2);
+    private TextStyle TextStyleHeader3 = TextStyle.Default.FontSize(14).Bold().FontColor(Colors.Blue.Medium);
+
+    public DocumentMetadata GetMetadata()
+    {
+        return new DocumentMetadata
+        {
+            Language = "en-US",
+            Title = "Conformance Test"
+        };
+    }
+
+    public DocumentSettings GetSettings()
+    {
+        return new DocumentSettings
+        {
+            PDFA_Conformance = PDFA_Conformance.PDFA_3A,
+            PDFUA_Conformance = PDFUA_Conformance.PDFUA_1,
+        };
+    }
+
+    public void Compose(IDocumentContainer container)
+    {
+        container.Page(page =>
+        {
+            page.Size(PageSizes.A4);
+            page.Margin(40);
+
+            page.Header()
+                .Column(column =>
+                {
+                    column.Item()
+                        .SemanticHeader1()
+                        .Text("Conformance Test")
+                        .Style(TextStyleHeader1);
+                });
+
+            page.Content()
+                .PaddingVertical(20)
+                .Column(column =>
+                {
+                    column.Item()
+                        .SemanticSection()
+                        .Element(TableOfContentsSection);
+                    
+                    column.Item().PageBreak();
+                    
+                    column.Item()
+                        .SemanticSection()
+                        .Section("section-text")
+                        .Element(TextSection);
+                    
+                    column.Item().PageBreak();
+                    
+                    column.Item()
+                        .SemanticSection()
+                        .Section("section-styled-box")
+                        .Element(StyledBoxSection);
+                    
+                    column.Item().PageBreak();
+                    
+                    column.Item()
+                        .SemanticSection()
+                        .Section("section-styled-line")
+                        .Element(StyledLineSection);
+                    
+                    column.Item().PageBreak();
+                    
+                    column.Item()
+                        .SemanticSection()
+                        .Section("section-image")
+                        .Element(ImageSection);
+                    
+                    column.Item().PageBreak();
+                    
+                    column.Item()
+                        .SemanticSection()
+                        .Section("section-svg")
+                        .Element(SvgSection);
+                    
+                    column.Item().PageBreak();
+                    
+                    column.Item()
+                        .SemanticSection()
+                        .Section("section-list")
+                        .Element(ListSection);
+                    
+                    column.Item().PageBreak();
+                    
+                    column.Item()
+                        .SemanticSection()
+                        .Section("section-table-table")
+                        .Element(SimpleTableSection);
+                    
+                    column.Item().PageBreak();
+                    
+                    column.Item()
+                        .SemanticSection()
+                        .Section("section-table-vertical-headers")
+                        .Element(TableWithVerticalHeadersSection);
+                    
+                    column.Item().PageBreak();
+                    
+                    column.Item()
+                        .SemanticSection()
+                        .Section("section-table-horizontal-headers")
+                        .Element(TableWithHorizontalHeadersSection);
+                    
+                    column.Item().PageBreak();
+                    
+                    column.Item()
+                        .SemanticSection()
+                        .Section("section-table-spanning-cells")
+                        .Element(TableWithSpanningCellsSection);
+                });
+
+            page.Footer()
+                .AlignCenter()
+                .Text(text =>
+                {
+                    text.Span("Page ");
+                    text.CurrentPageNumber();
+                    text.Span(" of ");
+                    text.TotalPages();
+                });
+        });
+    }
+    
+    private void TableOfContentsSection(IContainer container)
+    {
+        container.Column(column =>
+        {
+            column.Spacing(6);
+
+            column.Item()
+                .SemanticHeader2()
+                .Text("Table of Contents")
+                .Style(TextStyleHeader2);
+
+            column.Item()
+                .SemanticTableOfContents()
+                .Column(toc =>
+                {
+                    void TocItem(string title, string sectionId)
+                    {
+                        toc.Item()
+                            .SemanticTableOfContentsItem()
+                            .Row(row =>
+                            {
+                                row.RelativeItem()
+                                    .SemanticLink($"Go to {title}")
+                                    .Text(text =>
+                                    {
+                                        text.SectionLink(title, sectionId).Underline();
+                                    });
+
+                                row.ConstantItem(80)
+                                    .AlignRight()
+                                    .Text(text =>
+                                    {
+                                        text.Span("page ");
+                                        text.BeginPageNumberOfSection(sectionId);
+                                    });
+                            });
+                    }
+
+                    TocItem("Text", "section-text");
+                    TocItem("Styled Box", "section-styled-box");
+                    TocItem("Styled Line", "section-styled-line");
+                    TocItem("Images", "section-image");
+                    TocItem("SVG Content", "section-svg");
+                    TocItem("List", "section-list");
+                    TocItem("Simple Table", "section-table-table");
+                    TocItem("Table With Vertical Headers", "section-table-vertical-headers");
+                    TocItem("Table With Horizontal Headers", "section-table-horizontal-headers");
+                    TocItem("Table With Spanning Cells", "section-table-spanning-cells");
+                });
+        });
+    }
+    
+    private void TextSection(IContainer container)
+    {
+        container.Column(column =>
+        {
+            column.Spacing(10);
+            
+            column.Item()
+                .SemanticHeader2()
+                .Text("Text")
+                .Style(TextStyleHeader2);
+
+            column.Item()
+                .SemanticParagraph()
+                .Text(text =>
+                {
+                    text.Span("This is a semantic paragraph with ");
+                    text.Span("bold").Style(TextStyle.Default.Bold());
+                    text.Span(" and ");
+                    text.Span("italic").Style(TextStyle.Default.Italic());
+                    text.Span(" styles. ");
+                    text.Span(Placeholders.Sentence());
+                });
+
+            column.Item()
+                .SemanticParagraph()
+                .Text(text =>
+                {
+                    text.Span(Placeholders.Paragraph());
+                });
+
+            column.Item()
+                .SemanticHeader3()
+                .Text("Multilingual and Links")
+                .Style(TextStyleHeader3);
+
+            column.Item()
+                .SemanticLanguage("pl-PL")
+                .SemanticParagraph()
+                .Text("Zażółć gęślą jaźń");
+
+            column.Item()
+                .SemanticBlockQuotation()
+                .PaddingLeft(10)
+                .BorderLeft(2)
+                .BorderColor(Colors.Grey.Lighten2)
+                .SemanticParagraph()
+                .Text(text => text.Span("\"A block-level quotation illustrating semantic tagging.\""));
+
+            column.Item()
+                .SemanticParagraph()
+                .SemanticLink("QuestPDF website")
+                .Text(t =>
+                {
+                    t.Span("Visit ");
+                    t.Hyperlink("questpdf.com", "https://www.questpdf.com");
+                    t.Span(" for documentation.");
+                });
+
+            column.Item()
+                .SemanticParagraph()
+                .Text(t =>
+                {
+                    t.Span("Inline code example: ");
+                    t.Element(e => e.SemanticCode()
+                        .Background(Colors.Grey.Lighten3)
+                        .PaddingHorizontal(4)
+                        .PaddingVertical(2)
+                        .Text("var x = 1;")
+                    );
+                });
+        });
+    }
+    
+    private void StyledBoxSection(IContainer container)
+    {
+        container.Column(column =>
+        {
+            column.Spacing(10);
+            
+            column.Item()
+                .SemanticHeader2()
+                .Text("Styled Box")
+                .Style(TextStyleHeader2);
+
+            column.Item()
+                .SemanticDivision()
+                .Background(Colors.Grey.Lighten4)
+                .Border(1)
+                .BorderColor(Colors.Grey.Darken2)
+                .Padding(12)
+                .Column(box =>
+                {
+                    box.Spacing(6);
+                    box.Item().Text(Placeholders.Paragraph());
+                    box.Item().Text(Placeholders.Sentence());
+                    box.Item()
+                        .SemanticCaption()
+                        .Text("Figure 1. A styled division used as a callout/caption.");
+                });
+        });
+    }
+    
+    private void StyledLineSection(IContainer container)
+    {
+        container.Column(column =>
+        {
+            column.Spacing(10);
+            
+            column.Item()
+                .SemanticHeader2()
+                .Text("Styled Line")
+                .Style(TextStyleHeader2);
+
+            column.Item()
+                .SemanticDivision()
+                .Column(lines =>
+                {
+                    lines.Spacing(6);
+
+                    lines.Item().SemanticCaption().Text("Thin solid line");
+                    lines.Item().LineHorizontal(1).LineColor(Colors.Blue.Medium);
+
+                    lines.Item().SemanticCaption().Text("Dashed gradient line");
+                    lines.Item().LineHorizontal(3)
+                        .LineDashPattern(new float[] { 3, 3 })
+                        .LineGradient(new[] { Colors.Red.Medium, Colors.Orange.Medium });
+                });
+        });
+    }
+    
+    private void ImageSection(IContainer container)
+    {
+        container.Column(column =>
+        {
+            column.Spacing(10);
+            
+            column.Item()
+                .SemanticHeader2()
+                .Text("Images")
+                .Style(TextStyleHeader2);
+
+            var imageData = Placeholders.Image(480, 180);
+
+            column.Item()
+                .SemanticImage("A generated placeholder raster image")
+                .Border(1)
+                .BorderColor(Colors.Grey.Lighten1)
+                .Padding(2)
+                .Image(imageData)
+                .FitArea();
+
+            column.Item()
+                .SemanticCaption()
+                .Text("Figure 2. Placeholder image with alt text and caption.");
+        });
+    }
+    
+    private void SvgSection(IContainer container)
+    {
+        container.Column(column =>
+        {
+            column.Spacing(10);
+            
+            column.Item()
+                .SemanticHeader2()
+                .Text("SVG Content")
+                .Style(TextStyleHeader2);
+
+            var svg = "<svg xmlns='http://www.w3.org/2000/svg' width='400' height='120' viewBox='0 0 400 120'>" +
+                      "<rect x='5' y='5' width='390' height='110' rx='12' ry='12' fill='#E3F2FD' stroke='#64B5F6' stroke-width='2'/>" +
+                      "<circle cx='80' cy='60' r='35' fill='#90CAF9' stroke='#1E88E5' stroke-width='2'/>" +
+                      "<text x='140' y='68' font-family='Arial' font-size='20' fill='#0D47A1'>Scalable Vector Graphic</text>" +
+                      "</svg>";
+
+            column.Item()
+                .SemanticFigure("An example SVG graphic")
+                .Border(1)
+                .BorderColor(Colors.Grey.Lighten1)
+                .Padding(2)
+                .Svg(svg)
+                .FitArea();
+
+            column.Item()
+                .SemanticCaption()
+                .Text("Figure 3. Simple inline SVG with a caption.");
+        });
+    }
+
+    private void ListSection(IContainer container)
+    {
+        container.Column(column =>
+        {
+            column.Spacing(10);
+            
+            column.Item()
+                .SemanticHeader2()
+                .Text("List")
+                .Style(TextStyleHeader2);
+
+            // Simple bulleted list with nested items and proper semantics
+            column.Item()
+                .SemanticList()
+                .Column(list =>
+                {
+                    // Item 1
+                    list.Item()
+                        .SemanticListItem()
+                        .Row(row =>
+                        {
+                            row.ConstantItem(16)
+                                .AlignCenter()
+                                .SemanticListLabel()
+                                .Text("•");
+
+                            row.RelativeItem()
+                                .SemanticListItemBody()
+                                .SemanticParagraph()
+                                .Text(Placeholders.Sentence());
+                        });
+
+                    // Item 2 with nested list
+                    list.Item()
+                        .SemanticListItem()
+                        .Column(item =>
+                        {
+                            item.Item()
+                                .Row(row =>
+                                {
+                                    row.ConstantItem(16)
+                                        .AlignCenter()
+                                        .SemanticListLabel()
+                                        .Text("•");
+
+                                    row.RelativeItem()
+                                        .SemanticListItemBody()
+                                        .SemanticParagraph()
+                                        .Text("Parent item with a nested list:");
+                                });
+
+                            // nested list
+                            item.Item()
+                                .PaddingLeft(20)
+                                .SemanticList()
+                                .Column(nested =>
+                                {
+                                    nested.Item()
+                                        .SemanticListItem()
+                                        .Row(r =>
+                                        {
+                                            r.ConstantItem(16).AlignCenter().SemanticListLabel().Text("–");
+                                            r.RelativeItem().SemanticListItemBody().SemanticParagraph().Text(Placeholders.Sentence());
+                                        });
+
+                                    nested.Item()
+                                        .SemanticListItem()
+                                        .Row(r =>
+                                        {
+                                            r.ConstantItem(16).AlignCenter().SemanticListLabel().Text("–");
+                                            r.RelativeItem().SemanticListItemBody().SemanticParagraph().Text(Placeholders.Sentence());
+                                        });
+                                });
+                        });
+
+                    // Item 3
+                    list.Item()
+                        .SemanticListItem()
+                        .Row(row =>
+                        {
+                            row.ConstantItem(16).AlignCenter().SemanticListLabel().Text("•");
+                            row.RelativeItem().SemanticListItemBody().SemanticParagraph().Text(Placeholders.Sentence());
+                        });
+                });
+        });
+    }
+    
+    private void SimpleTableSection(IContainer container)
+    {
+        container.Column(column =>
+        {
+            column.Spacing(10);
+            
+            column.Item()
+                .SemanticHeader2()
+                .Text("Simple Table")
+                .Style(TextStyleHeader2);
+
+            column.Item()
+                .SemanticTable()
+                .Border(1)
+                .BorderColor(Colors.Grey.Lighten2)
+                .Padding(2)
+                .Table(table =>
+                {
+                    table.ApplySemanticTags();
+                    table.ColumnsDefinition(cols =>
+                    {
+                        cols.RelativeColumn(3);
+                        cols.RelativeColumn(1);
+                        cols.RelativeColumn(1);
+                    });
+
+                    table.Header(header =>
+                    {
+                        header.Cell().Padding(6).Background(Colors.Grey.Lighten3).Text("Product").Style(TextStyle.Default.Bold());
+                        header.Cell().Padding(6).Background(Colors.Grey.Lighten3).AlignRight().Text("Qty").Style(TextStyle.Default.Bold());
+                        header.Cell().Padding(6).Background(Colors.Grey.Lighten3).AlignRight().Text("Price").Style(TextStyle.Default.Bold());
+                    });
+
+                    for (int i = 0; i < 6; i++)
+                    {
+                        table.Cell().Padding(6).Text(Placeholders.Label());
+                        table.Cell().Padding(6).AlignRight().Text((i + 1).ToString());
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Price());
+                    }
+                });
+        });
+    }
+    
+    private void TableWithVerticalHeadersSection(IContainer container)
+    {
+        container.Column(column =>
+        {
+            column.Spacing(10);
+            
+            column.Item()
+                .SemanticHeader2()
+                .Text("Table With Vertical Headers")
+                .Style(TextStyleHeader2);
+
+            column.Item()
+                .SemanticTable()
+                .Border(1)
+                .BorderColor(Colors.Grey.Lighten2)
+                .Padding(2)
+                .Table(table =>
+                {
+                    table.ApplySemanticTags();
+                    table.ColumnsDefinition(cols =>
+                    {
+                        cols.RelativeColumn(2); // row headers
+                        cols.RelativeColumn(1);
+                        cols.RelativeColumn(1);
+                        cols.RelativeColumn(1);
+                    });
+
+                    // top header row
+                    table.Header(header =>
+                    {
+                        header.Cell().Padding(6).Background(Colors.Grey.Lighten3).Text("");
+                        header.Cell().Padding(6).Background(Colors.Grey.Lighten3).AlignCenter().Text("Q1").Style(TextStyle.Default.Bold());
+                        header.Cell().Padding(6).Background(Colors.Grey.Lighten3).AlignCenter().Text("Q2").Style(TextStyle.Default.Bold());
+                        header.Cell().Padding(6).Background(Colors.Grey.Lighten3).AlignCenter().Text("Q3").Style(TextStyle.Default.Bold());
+                    });
+
+                    string[] regions = { "North", "South", "East", "West" };
+                    foreach (var region in regions)
+                    {
+                        table.Cell().AsSemanticHorizontalHeader().Padding(6).Background(Colors.Grey.Lighten4).Text(region).Style(TextStyle.Default.Bold());
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                    }
+                });
+        });
+    }
+    
+    private void TableWithHorizontalHeadersSection(IContainer container)
+    {
+        container.Column(column =>
+        {
+            column.Spacing(10);
+            
+            column.Item()
+                .SemanticHeader2()
+                .Text("Table With Horizontal Headers")
+                .Style(TextStyleHeader2);
+
+            column.Item()
+                .SemanticTable()
+                .Border(1)
+                .BorderColor(Colors.Grey.Lighten2)
+                .Padding(2)
+                .Table(table =>
+                {
+                    table.ApplySemanticTags();
+                    table.ColumnsDefinition(cols =>
+                    {
+                        cols.RelativeColumn(2); // metric
+                        cols.RelativeColumn(1);
+                        cols.RelativeColumn(1);
+                        cols.RelativeColumn(1);
+                    });
+
+                    table.Header(header =>
+                    {
+                        // first header row
+                        header.Cell().Row(1).Column(1).ColumnSpan(4).Padding(6).Background(Colors.Grey.Lighten3).AlignCenter().Text("Quarterly Sales").Style(TextStyle.Default.Bold());
+                        // second header row
+                        header.Cell().Row(2).Column(1).Padding(6).Background(Colors.Grey.Lighten3).Text("Metric").Style(TextStyle.Default.Bold());
+                        header.Cell().Row(2).Column(2).Padding(6).Background(Colors.Grey.Lighten3).AlignCenter().Text("Q1").Style(TextStyle.Default.Bold());
+                        header.Cell().Row(2).Column(3).Padding(6).Background(Colors.Grey.Lighten3).AlignCenter().Text("Q2").Style(TextStyle.Default.Bold());
+                        header.Cell().Row(2).Column(4).Padding(6).Background(Colors.Grey.Lighten3).AlignCenter().Text("Q3").Style(TextStyle.Default.Bold());
+                    });
+
+                    string[] metrics = { "Revenue", "Units", "Growth" };
+                    foreach (var m in metrics)
+                    {
+                        table.Cell().Padding(6).Text(m);
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                    }
+                });
+        });
+    }
+    
+    private void TableWithSpanningCellsSection(IContainer container)
+    {
+        container.Column(column =>
+        {
+            column.Spacing(10);
+            
+            column.Item()
+                .SemanticHeader2()
+                .Text("Table With Spanning Cells")
+                .Style(TextStyleHeader2);
+
+            column.Item()
+                .SemanticTable()
+                .Border(1)
+                .BorderColor(Colors.Grey.Lighten2)
+                .Padding(2)
+                .Table(table =>
+                {
+                    table.ApplySemanticTags();
+                    table.ColumnsDefinition(cols =>
+                    {
+                        cols.RelativeColumn(2); // Category / Row header
+                        cols.RelativeColumn(1);
+                        cols.RelativeColumn(1);
+                        cols.RelativeColumn(1);
+                    });
+
+                    // Complex header: Category | Metrics (Min, Max) | Total
+                    table.Header(header =>
+                    {
+                        header.Cell().Row(1).Column(1).RowSpan(2).Padding(6).Background(Colors.Grey.Lighten3).Text("Category").Style(TextStyle.Default.Bold());
+                        header.Cell().Row(1).Column(2).ColumnSpan(2).Padding(6).Background(Colors.Grey.Lighten3).AlignCenter().Text("Metrics").Style(TextStyle.Default.Bold());
+                        header.Cell().Row(1).Column(4).RowSpan(2).Padding(6).Background(Colors.Grey.Lighten3).AlignCenter().Text("Total").Style(TextStyle.Default.Bold());
+
+                        header.Cell().Row(2).Column(2).Padding(6).Background(Colors.Grey.Lighten3).AlignCenter().Text("Min").Style(TextStyle.Default.Bold());
+                        header.Cell().Row(2).Column(3).Padding(6).Background(Colors.Grey.Lighten3).AlignCenter().Text("Max").Style(TextStyle.Default.Bold());
+                    });
+
+                    // Body with row spans
+                    string[] categories = { "Hardware", "Software" };
+                    foreach (var cat in categories)
+                    {
+                        // Category spans two subrows
+                        table.Cell().RowSpan(2).AsSemanticHorizontalHeader().Padding(6).Background(Colors.Grey.Lighten4).Text(cat).Style(TextStyle.Default.Bold());
+                        // first subrow
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                        // second subrow
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                        table.Cell().Padding(6).AlignRight().Text(Placeholders.Integer());
+                    }
+                });
+        });
+    }
+}
+

+ 114 - 0
Source/QuestPDF.ConformanceTests/ConformanceToolRunner.cs

@@ -0,0 +1,114 @@
+using System.Diagnostics;
+using System.Text;
+using System.Text.Json;
+using QuestPDF.Fluent;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests;
+
+public static class ConformanceToolRunner
+{
+    public class ValidationResult
+    {
+        public bool IsDocumentValid => !FailedRules.Any();
+        public ICollection<FailedRule> FailedRules { get; set; } = [];
+    
+        public class FailedRule
+        {
+            public string Profile { get; set; }
+            public string Specification { get; set; }
+            public string Clause { get; set; }
+            public string Description { get; set; }
+        }
+
+        public string GetErrorMessage()
+        {
+            if (!FailedRules.Any())
+                return string.Empty;
+        
+            var errorMessage = new StringBuilder();
+            
+            foreach (var failedRule in FailedRules)
+            {
+                errorMessage.AppendLine($"🟥\t{failedRule.Profile}");
+                errorMessage.AppendLine($"\t{failedRule.Specification}");
+                errorMessage.AppendLine($"\t{failedRule.Clause}");
+                errorMessage.AppendLine($"\t{failedRule.Description}");
+                errorMessage.AppendLine();
+            }
+
+            return errorMessage.ToString();
+        }
+    }
+
+    
+    public static void TestConformance(this IDocument document)
+    {
+        var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.pdf");
+        document.GeneratePdf(filePath);
+        
+        var result = RunVeraPDF(filePath);
+
+        if (!result.IsDocumentValid)
+        {
+            Console.WriteLine(result.GetErrorMessage());
+            Assert.Fail();
+        }
+        
+        File.Delete(filePath);
+    }
+    
+    private static ValidationResult RunVeraPDF(string pdfFilePath)
+    {
+        if (!File.Exists(pdfFilePath))
+            throw new FileNotFoundException($"PDF file not found: {pdfFilePath}");
+        
+        var arguments = $"--format json \"{pdfFilePath}\"";
+
+        var process = new Process
+        {
+            StartInfo = new ProcessStartInfo
+            {
+                FileName = "verapdf",
+                Arguments = arguments,
+                RedirectStandardOutput = true,
+                RedirectStandardError = true,
+                UseShellExecute = false,
+                CreateNoWindow = true
+            }
+        };
+
+        process.Start();
+        var output = process.StandardOutput.ReadToEnd();
+        process.WaitForExit();
+
+        var result = new ValidationResult();
+
+        var profileResults = JsonDocument
+            .Parse(output)
+            .RootElement
+            .GetProperty("report")
+            .GetProperty("jobs")[0]
+            .GetProperty("validationResult");
+        
+        foreach (var profileValidationResult in profileResults.EnumerateArray())
+        {
+            var failedRules = profileValidationResult
+                .GetProperty("details")
+                .GetProperty("ruleSummaries");
+
+            foreach (var failedRule in failedRules.EnumerateArray())
+            {
+                result.FailedRules.Add(new ValidationResult.FailedRule
+                {
+                    Profile = profileValidationResult.GetProperty("profileName").GetString().Split(" ").First(),
+                    Specification = failedRule.GetProperty("specification").GetString(),
+                    Clause = failedRule.GetProperty("clause").GetString(),
+                    Description = failedRule.GetProperty("description").GetString()
+                });
+            }
+        }
+
+        return result;
+    }
+}

+ 48 - 0
Source/QuestPDF.ConformanceTests/ImageHelpers.cs

@@ -0,0 +1,48 @@
+using ImageMagick;
+
+namespace QuestPDF.ConformanceTests;
+
+public static class ImageHelpers
+{
+    public static void ConvertImageIccColorSpaceProfileToVersion2(Stream inputStream, Stream outputStream)
+    {
+        using var image = new MagickImage(inputStream);
+        var iccVersion = GetIccProfileVersion();
+
+        if (iccVersion == 2)
+        {
+            image.Write(outputStream);
+            return;
+        }
+
+        if (iccVersion != null)
+            image.RemoveProfile("icc");
+        
+        image.ColorSpace = ColorSpace.sRGB;
+        image.SetProfile(ColorProfile.SRGB);
+        
+        image.Write(outputStream);
+
+        int? GetIccProfileVersion()
+        {
+            var imageProfile = image.GetProfile("icc");
+ 
+            if (imageProfile == null)
+                return null;
+            
+            var imageProfileRaw = imageProfile.ToByteArray();
+
+            if (imageProfileRaw.Length < 12)
+                return null;
+            
+            return imageProfileRaw[8];
+        }
+    }
+
+    public static void ConvertImageIccColorSpaceProfileToVersion2(string inputPath, string outputPath)
+    {
+        using var inputStream = File.OpenRead(inputPath);
+        using var outputStream = File.OpenWrite(outputPath);
+        ConvertImageIccColorSpaceProfileToVersion2(inputStream, outputStream);
+    }
+}

+ 101 - 0
Source/QuestPDF.ConformanceTests/ImageTests.cs

@@ -0,0 +1,101 @@
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests;
+
+public class ImageTests
+{
+    [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()
+    {
+        // PDF/A-1a and PDF/A-1b require ICC profile version 2
+        // prepare an image with ICC profile version 2
+        var sourceImagePath = Path.Combine("Resources", "photo.jpeg");
+        var targetImagePath = Path.Combine("Resources", "photo-icc2.jpeg");
+
+        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)
+    {
+        var imagePath = useImageWithIcc2 
+            ? Path.Combine("Resources", "photo-icc2.jpeg") 
+            : Path.Combine("Resources", "photo.jpeg");
+        
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .PaddingVertical(30)
+                        .Column(column =>
+                        {
+                            column.Item()
+                                .ExtendVertical()
+                                .AlignMiddle()
+                                .SemanticHeader1()
+                                .Text("Conformance Test:\nImages")
+                                .FontSize(36)
+                                .Bold()
+                                .FontColor(Colors.Blue.Darken2);
+
+                            column.Item().PageBreak();
+
+                            column.Item()
+                                .SemanticImage("Sample image description")
+                                .Column(column =>
+                                {
+                                    column.Item().Width(300).Image(imagePath);
+                                    column.Item().SemanticCaption().Text("Sample image caption");
+                                });
+                        });
+                });
+            })
+            .WithMetadata(new DocumentMetadata
+            {
+                Language = "en-US",
+                Title = "Conformance Test", 
+                Subject = "Images"
+            });
+    }
+}

+ 38 - 0
Source/QuestPDF.ConformanceTests/QuestPDF.ConformanceTests.csproj

@@ -0,0 +1,38 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+
+        <IsPackable>false</IsPackable>
+        <IsTestProject>true</IsTestProject>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="coverlet.collector" Version="6.0.0"/>
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
+        <PackageReference Include="NUnit" Version="3.14.0"/>
+        <PackageReference Include="NUnit.Analyzers" Version="3.9.0"/>
+        <PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
+        <PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.8.2" />
+        <PackageReference Include="Magick.NET.Core" Version="14.8.2" />
+    </ItemGroup>
+
+    <ItemGroup>
+        <Using Include="NUnit.Framework"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
+    </ItemGroup>
+
+    <ItemGroup>
+        <None Include="Resources\**\*.*">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="Resources\photo.jpg">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+    </ItemGroup>
+</Project>

+ 157 - 0
Source/QuestPDF.ConformanceTests/TableOfContentsTests.cs

@@ -0,0 +1,157 @@
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests;
+
+[TestFixture]
+public class TableOfContentsTests
+{
+    [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()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .PaddingVertical(30)
+                        .Column(column =>
+                        {
+                            column.Item()
+                                .ExtendVertical()
+                                .AlignMiddle()
+                                .SemanticHeader1()
+                                .Text("Conformance Test:\nTable of Contents")
+                                .FontSize(36)
+                                .Bold()
+                                .FontColor(Colors.Blue.Darken2);
+
+                            column.Item().PageBreak();
+
+                            column.Item().Element(GenerateTableOfContentsSection);
+
+                            column.Item().PageBreak();
+
+                            column.Item().Element(GeneratePlaceholderContentSection);
+                        });
+                });
+            })
+            .WithMetadata(new DocumentMetadata
+            {
+                Language = "en-US",
+                Title = "Conformance Test", 
+                Subject = "Table of Contents"
+            });
+    }
+
+    private void GenerateTableOfContentsSection(IContainer container)
+    {
+        container
+            .SemanticSection()
+            .Column(column =>
+            {
+                column.Spacing(15);
+                
+                column
+                    .Item()
+                    .Text("Table of Contents")
+                    .Bold()
+                    .FontSize(20)
+                    .FontColor(Colors.Blue.Medium);
+                
+                column.Item()
+                    .SemanticTableOfContents()
+                    .Column(column =>
+                    {
+                        column.Spacing(5);
+                        
+                        foreach (var i in Enumerable.Range(1, 10))
+                        {
+                            column.Item()
+                                .SemanticTableOfContentsItem()
+                                .SemanticLink($"Link to section {i}")
+                                .SectionLink($"section-{i}")
+                                .Row(row =>
+                                {
+                                    row.ConstantItem(25).Text($"{i}.");
+                                    row.AutoItem().Text(Placeholders.Label());
+                                    row.RelativeItem().PaddingHorizontal(2).TranslateY(11).LineHorizontal(1).LineDashPattern([1, 3]);
+                                    row.AutoItem().Text(text => text.BeginPageNumberOfSection($"section-{i}"));
+                                });
+                        }
+                    });
+            });
+    }
+    
+    private void GeneratePlaceholderContentSection(IContainer container)
+    {
+        container
+            .Column(column =>
+            {
+                foreach (var i in Enumerable.Range(1, 10))
+                {
+                    column.Item()
+                        .SemanticSection()
+                        .Section($"section-{i}")
+                        .Column(column =>
+                        {
+                            column.Spacing(15);
+                            
+                            column.Item()
+                                .SemanticHeader2()
+                                .Text($"Section {i}")
+                                .Bold()
+                                .FontSize(20)
+                                .FontColor(Colors.Blue.Medium);
+                            
+                            column.Item().Text(Placeholders.Paragraph());
+                            
+                            foreach (var j in Enumerable.Range(1, i))
+                            {
+                                column.Item()
+                                    .ArtifactOther()
+                                    .Width(200)
+                                    .Height(150)
+                                    .CornerRadius(10)
+                                    .Background(Placeholders.BackgroundColor());
+                            }
+                        });
+                    
+                    if (i < 10)
+                        column.Item().PageBreak();
+                }
+            });
+    }
+}

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

@@ -0,0 +1,15 @@
+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;
+    }
+}

+ 16 - 0
Source/QuestPDF.ConformanceTests/TestsSetup.cs

@@ -0,0 +1,16 @@
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests
+{
+    [SetUpFixture]
+    public class TestsSetup
+    {
+        [OneTimeSetUp]
+        public static void Setup()
+        {
+            QuestPDF.Settings.License = LicenseType.Community;
+            QuestPDF.Settings.UseEnvironmentFonts = false;
+        }
+    }
+}

+ 6 - 0
Source/QuestPDF.sln

@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.DocumentationExamp
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.VisualTests", "QuestPDF.VisualTests\QuestPDF.VisualTests.csproj", "{D7EB8ACD-4F99-439C-898F-EBFF0AFE367E}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.VisualTests", "QuestPDF.VisualTests\QuestPDF.VisualTests.csproj", "{D7EB8ACD-4F99-439C-898F-EBFF0AFE367E}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.ConformanceTests", "QuestPDF.ConformanceTests\QuestPDF.ConformanceTests.csproj", "{FB8C2DE3-5866-49F8-844C-9510F1A1BC72}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Debug|Any CPU = Debug|Any CPU
@@ -59,5 +61,9 @@ Global
 		{D7EB8ACD-4F99-439C-898F-EBFF0AFE367E}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{D7EB8ACD-4F99-439C-898F-EBFF0AFE367E}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{D7EB8ACD-4F99-439C-898F-EBFF0AFE367E}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{D7EB8ACD-4F99-439C-898F-EBFF0AFE367E}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{D7EB8ACD-4F99-439C-898F-EBFF0AFE367E}.Release|Any CPU.Build.0 = Release|Any CPU
 		{D7EB8ACD-4F99-439C-898F-EBFF0AFE367E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{FB8C2DE3-5866-49F8-844C-9510F1A1BC72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{FB8C2DE3-5866-49F8-844C-9510F1A1BC72}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FB8C2DE3-5866-49F8-844C-9510F1A1BC72}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FB8C2DE3-5866-49F8-844C-9510F1A1BC72}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 EndGlobal
 EndGlobal