Browse Source

Feat semantic structure (#1355)

* Initial implementation of semantic structure (accessability, tagging, bookmarks, etc.)

* Semantic: improved support for text spans, languages and alt text

* Add example for semantic text (temporary)

* Add TODOs for special values related to note IDs in SemanticExtensions

* Minor improvements

* Remove unneeded UpdateAlternativeText method and related logic from SemanticTag

* Add TODOs for handling operations during FreeCanvas drawing and semantic node ID caching in DocumentGenerator

* Add semantic artifact methods and special IDs for improved structure

* Add semantic header methods for improved document structure

* 2025.7.1 release

* Update README.md

* Optimize concurrency for text-heavy document generation and remove unused semaphore to improve performance. Update release notes accordingly.

* Disable standard ligatures by default to improve text copying and accessibility

* Adjust VisualTest related to drawing test which started to fail after disabling font ligatures in default settings

* Enhance semantic structure by adding artifact backgrounds and pagination markers to layers in Page.cs

* Add semantic book content and examples for programming terms

* Fix: semantic tag tree should not include layout artifacts

* Add documentation for Layout Artifacts

* Refactor: enhance document structure debugging with semantic document pointer

* Refactor: simplify semantic tag handling and remove redundant artifact backgrounds in layers

* Improve the developer experience by updating the behavior of the GeneratePdfAndShow method to always create a uniquely named file, ensuring that certain PDF viewers automatically refresh the preview

* Move functionalities related to layout artifacts into a separate element

* Rename LayoutArtifact and LayoutArtifactExtensions to ArtifactTag and ArtifactExtensions for improved clarity and consistency

* Redesign and optimize the process of collecting semantic tree

* Enhance SetSemanticTree method to accept nullable SemanticTreeNode, improving null handling across document canvases

* Add support for merged documents (previously removed during semantic tree implementation)

* Refactor SemanticExtensions to remove unnecessary SemanticDocument method and add SemanticIndex and SemanticLink methods for improved semantic structure

* Clean up the QuestPDF.Companion.TestRunner file

* Refactor SemanticHeader methods to remove title parameter and update header text dynamically

* Experimental: add support for automated semantic tagging of the table container (not fully working, does not cover TR and TD)

* Fix: reset semantic node ID after drawing to ensure proper semantic tree management

* Implement improved support for semantic tables

* Add support for table cell attributes (colspan + rowspan)

* Add remaining attributes types for pdf tags

* Update native dependencies to support newly added semantic features on all platforms

* 2025.12.0-alpha0

* Fix exception triggered when generating a PDF without defining a Page in the Fluent API

* 2025.12.0-alpha0

* Adjust text-related visual tests

* Adjust DPI-related unit test (file size changed after adding semantic structure)

* Fix false positive exception regarding not-disposing SkPdfTag instances

* Fix build: cannot execute GeneratePdfAndShow on CI

* Fix exception triggered when generating a PDF without defining a Page in the Fluent API

* Simplify layout testing engine

* Add unit tests for length-unit conversion functionality

* Enhance the layout testing approach

* Add layout tests for the Row element

* Improve Row drawing behavior

* Add tests for the Translate element

* Adjust companion hint formatting for the Translate element

* Fix formatting of companion hint values in the Translate element

* Fix companion hint formatting for the Padding element

* Fix measurement corner case for the Padding element, where the negative padding could result with negative SpacePlan

* Add more tests for the Padding element

* Improve drawing behaviour of the StopPaging element when its child wraps to the next page

* Add layout tests for the StopPaging element

* Small enhancements to the layout testing engine

* Extend the layout testing engine to check if the canvas matrix has been correctly restored after drawing the tested element

* Fix layout tests for the Row element

* Add tests for the SimpleRotate element

* Add tests for the Rotate element

* Remove unused namespaces from ElementObserver.cs

* Remove old tests of the Rotate element

* Enhance layout testing engine

* Add tests for the Line element

* Add tests for the Row element

* 2025.7.2

* 2025.12.0-alpha0

* Fix exception triggered when generating a PDF without defining a Page in the Fluent API

* Add support for choosing PDF/A and PDF/UA conformance levels

* Improve support for links (URL hyperlinks and internal links to named destinations) in accordance with PDF/UA-1 requirements.

* Improve semantic tagging for simple tables (ones that do not contain cells spanning multiple rows/columns)

* Improve semantic meaning of decorative elements: Line and StyledBox

* Simplify SemanticTag handling Canvas.SetSemanticNodeId

* Update Skia native dependency with questpdf-specific changes related to PDF/A and PDF/UA standards, as well as semantic links

* Adjust visual testing images

* 2025.12.0-alpha1

* Fix updating header SemanticTag text to prevent potential stack overflow

* Improve visual representation of SemanticTags and ArtifactTags in the Companion App

* Mark content that is repeated on consequtive pages (e.g. page header, table header) as artifacts

* Improve semantic handling of complex tables (containing cells that span multiple columns or rows)

* Mark page background content as artifact

* Mark page foreground as watermark artifact

* Mark MultiColumn spacer element as layout artifact

* Fix semantic header lookup logic for table cells

* Simplify header value construction by removing distinct and order operations

* Enhance the RepeatContent to handle semantic meaning

* Enhance the RepeatContent to handle semantic meaning

* Refactor how artifacts are handled in semantic context

* Refactor RepeatContent to use RepeatContextType and add methods for page header and footer

* Fix handling of semantic content in TextBlock items

* Add semantic auto-tagging for paragraphs

* 2025.12.0-alpha2

* Fix ResetState methods in ContinuousBlock and SolidBlock classes

* Optimize the process of automatically applying semantic paragraphs

* Fix disposal comment for SemanticTag in PdfDocumentCanvas

* Update disposal comments in PdfDocumentCanvas to clarify lifetime management for SemanticTag and WriteStream

* Updated Skia native dependency to m142 + Improve support with PDF/A-1a and PDF/A-1b

* Add support for PDF/A-1a and PDF/A-1b

* Enhance semantic tagging support by introducing conditional management of SemanticTreeManager and updating content configuration methods

* Update ReleaseNotes

* Add validation for scale factors in ScaleExtensions

* Fix formatting in GetCompanionHint in Scale element

* Add tests for Scale element

* Add ability to generate ActualOutput in the VisualTest engine

* Add tests for text-related Fluent API

* Add tests for font feature enabling and disabling in TextStyle

* Add tests for text direction handling in TextSpan and TextStyle

* Add tests for text decoration features in TextSpan and TextStyle

* Improve StyledBox implementation and add more tests

* Refactor PageSize properties to use expression-bodied members

* Load Latin words from a resource file instead of hardcoding them (removes false positives from mutation testing)

* Load MIME type mappings from embedded CSV resource instead of hardcoding (removes false positives from mutation testing)

* Remove unused helper methods

* Update test project dependencies

* Add [ExcludeFromCodeCoverage] attribute to obsolete methods for improved test coverage reporting

* Update README.md

* Refactor assertions in tests to use Is.Zero and Is.False for improved clarity

* Add comprehensive tests for TextStyle and TextSpan styling features

* Add visual tests for TextStyle features including font color, background color, font size, line height, and font decoration

* Update Microsoft.NET.Test.Sdk package to version 18.0.0 in project files

* Fix build by downgrading Microsoft.NET.Test.Sdk package to version 17.13.0

* Refactor file generation methods to use TemporaryStorage for path management

* Fix build

* Refactor: enhance document structure debugging with semantic document pointer

* Refactor: streamline document container debugging and semantic part handling

* Refactor: port changes from the 2025.7.3 release to ReleaseNotes

* Update main.yml

* Fix indentation in GitHub Actions workflow

* Add curl installation to workflow

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Fix automatic tagging Text elements

* Update main.yml

* Add basic conformance tests (proof of concept)

* Refactor semantic structure and add conformance testing framework

* Add Semantic Test Scaffolding

* Fix unnecessary space at the end of the SemanticTag Alt attribute in header content

* Adjust asserts in SemanticAwareDrawingCanvas to better handle StyledBox logic

* Create SemanticTree builder to make related assertions easier to follow and more concise

* Refactor SemanticTree comparison logic for improved clarity and accuracy

* Add metadata retrieval to ConformanceTestBase for enhanced document context

* Update NUnit packages for improved functionality and compatibility

* Add error message and context to validation results in Semantic Tests

* Add tests for tables without headers in Conformance Tests

* Implement table with horizontal headers in Conformance Tests

* Implement table with vertical headers in Conformance Tests

* Update NUnit.Analyzers and NUnit3TestAdapter to latest versions

* Enhance line tests with semantic structure and improved formatting

* Add SVG conformance tests

* Fix registration of semantic content when it is present in artifact context

* Implement conformance tests for the Header element

* Implement conformance tests for the Footer element

* Refactor TableOfContentsTests conformance test to improve structure and add expected semantic tree

* Fix flaky Image conformance tests

* Implement conformance tests for Table with footer

* Refactor ApplySemanticParagraphs method to simplify logic and remove unnecessary checks

* Fix incorrectly disabled code

* Reorganize conformance tests for Table element

* Add conformance tests for Table with header cells spanning multiple columns

* Add conformance tests for Table with header cells spanning multiple rows

* Add NodeId assertion and Id method to SemanticTreeNode

* Update TestSemanticMeaning to include PDFUA_1 conformance settings

* Fix semantic tagging in Table element

* Add conformance tests for lists with nested items

* Remove unnecessary conformance test

* Implement styled boxes conformance test with semantic structure

* Add conformance tests for multi-column layout with semantic validation

* Remove unnecessary conformance test

* Update main.yml

* Update main.yml

* Change alias for Mustang CLI and clean up workflow

Updated alias for Mustang CLI and removed version check.

* Fix conformance test for images

* Update .gitignore to include macOS specific files

* 2025.12.0-alpha3

* Update main.yml

* Add Mustang conformance test runner

* Add conformance testing method to VeraPdfConformanceTestRunner

* Add ZUGFeRD conformance testing

* Refactor semantic tagging in DocumentContainer and SemanticExtensions

* Remove obsolete SemanticHeader method from SemanticExtensions

* Refactor semantic table methods for improved accessibility and structure

* Add conformance tests for the Decoration element

* Improve way how decoration element in visible in hierarchy tree in Companion App

* Improve way how Table element in visible in hierarchy tree in Companion App

* Align semantic behavior of the Repeat element

* Enhance in-code documentation for Semantic Fluent API

* Simplify public API related to semantic content and artifacts

* Add accessibility examples for creating accessible PDF documents

* 2025.7.2

* 2025.7.3

* Enhance PDF generation and update XML metadata for ZUGFeRD compliance

* Fix DocumentOperation.ExtendMetadata operation by preserving /Metadata PDF tag attributes, and enhancing compatibility with the mustang validation tool

* Fix build

* Update README.md

* Fix tests preventing build from succeding + 2025.7.4 release

* Add test for disabling uniform item height in row layout

* Add test for disabling uniform item width in column layout

* Fix typo in DateTimeString closing tag in resource-factur-x.xml

* Refactor: enhance document structure debugging with semantic document pointer

* Adjust rebase artifact

* Add InternalsVisibleTo for QuestPDF.ConformanceTests

* 2025.12.0-alpha4

* Enhance image test layout by adding padding to the caption

* Add IgnoreTests for semantic image handling in documents
Marcin Ziąbek 1 week ago
parent
commit
f97859d4e5
96 changed files with 5590 additions and 157 deletions
  1. 34 0
      .github/workflows/main.yml
  2. 5 1
      .gitignore
  3. 93 0
      Source/QuestPDF.ConformanceTests/DecorationTests.cs
  4. 79 0
      Source/QuestPDF.ConformanceTests/FooterTests.cs
  5. 77 0
      Source/QuestPDF.ConformanceTests/HeaderTests.cs
  6. 53 0
      Source/QuestPDF.ConformanceTests/IgnoreTests.cs
  7. 78 0
      Source/QuestPDF.ConformanceTests/ImageTests.cs
  8. 85 0
      Source/QuestPDF.ConformanceTests/LineTests.cs
  9. 143 0
      Source/QuestPDF.ConformanceTests/ListTests.cs
  10. 67 0
      Source/QuestPDF.ConformanceTests/MultiColumnTests.cs
  11. 38 0
      Source/QuestPDF.ConformanceTests/QuestPDF.ConformanceTests.csproj
  12. 0 0
      Source/QuestPDF.ConformanceTests/Resources/image.svg
  13. BIN
      Source/QuestPDF.ConformanceTests/Resources/photo.jpeg
  14. 191 0
      Source/QuestPDF.ConformanceTests/Resources/zugferd-factur-x.xml
  15. 48 0
      Source/QuestPDF.ConformanceTests/Resources/zugferd-xmp-metadata.xml
  16. 87 0
      Source/QuestPDF.ConformanceTests/StyledBoxTests.cs
  17. 51 0
      Source/QuestPDF.ConformanceTests/SvgTests.cs
  18. 110 0
      Source/QuestPDF.ConformanceTests/Table/TableWithFooterTests.cs
  19. 223 0
      Source/QuestPDF.ConformanceTests/Table/TableWithHeaderCellsSpanningMultipleColumnsTests.cs
  20. 232 0
      Source/QuestPDF.ConformanceTests/Table/TableWithHeaderCellsSpanningMultipleRowsTests.cs
  21. 112 0
      Source/QuestPDF.ConformanceTests/Table/TableWithHorizontalHeadersTests.cs
  22. 111 0
      Source/QuestPDF.ConformanceTests/Table/TableWithVerticalHeadersTests.cs
  23. 102 0
      Source/QuestPDF.ConformanceTests/Table/TableWithoutHeadersTests.cs
  24. 164 0
      Source/QuestPDF.ConformanceTests/TableOfContentsTests.cs
  25. 78 0
      Source/QuestPDF.ConformanceTests/TestEngine/ConformanceTestBase.cs
  26. 48 0
      Source/QuestPDF.ConformanceTests/TestEngine/ImageHelpers.cs
  27. 80 0
      Source/QuestPDF.ConformanceTests/TestEngine/MustangConformanceTestRunner.cs
  28. 191 0
      Source/QuestPDF.ConformanceTests/TestEngine/SemanticAwareDrawingCanvas.cs
  29. 156 0
      Source/QuestPDF.ConformanceTests/TestEngine/SemanticTreeTestRunner.cs
  30. 132 0
      Source/QuestPDF.ConformanceTests/TestEngine/VeraPdfConformanceTestRunner.cs
  31. 15 0
      Source/QuestPDF.ConformanceTests/TestsSetup.cs
  32. 69 0
      Source/QuestPDF.ConformanceTests/ZugferdTests.cs
  33. 83 0
      Source/QuestPDF.DocumentationExamples/AccessibilityExamples.cs
  34. 2 2
      Source/QuestPDF.DocumentationExamples/QuestPDF.DocumentationExamples.csproj
  35. 709 0
      Source/QuestPDF.DocumentationExamples/Resources/semantic-book-content.json
  36. 265 0
      Source/QuestPDF.DocumentationExamples/SemanticExamples.cs
  37. 2 2
      Source/QuestPDF.LayoutTests/QuestPDF.LayoutTests.csproj
  38. 1 6
      Source/QuestPDF.LayoutTests/TestEngine/ContinuousBlock.cs
  39. 1 6
      Source/QuestPDF.LayoutTests/TestEngine/SolidBlock.cs
  40. 1 1
      Source/QuestPDF.ReportSample/QuestPDF.ReportSample.csproj
  41. 1 1
      Source/QuestPDF.UnitTests/ImageTests.cs
  42. 2 2
      Source/QuestPDF.UnitTests/QuestPDF.UnitTests.csproj
  43. 5 2
      Source/QuestPDF.UnitTests/TestEngine/MockCanvas.cs
  44. 5 2
      Source/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs
  45. 2 2
      Source/QuestPDF.VisualTests/QuestPDF.VisualTests.csproj
  46. 2 2
      Source/QuestPDF.ZUGFeRD/QuestPDF.ZUGFeRD.csproj
  47. 6 0
      Source/QuestPDF.sln
  48. 5 0
      Source/QuestPDF/Drawing/DocumentCanvases/CompanionDocumentCanvas.cs
  49. 5 0
      Source/QuestPDF/Drawing/DocumentCanvases/FreeDocumentCanvas.cs
  50. 5 0
      Source/QuestPDF/Drawing/DocumentCanvases/ImageDocumentCanvas.cs
  51. 83 17
      Source/QuestPDF/Drawing/DocumentCanvases/PdfDocumentCanvas.cs
  52. 5 0
      Source/QuestPDF/Drawing/DocumentCanvases/SvgDocumentCanvas.cs
  53. 5 0
      Source/QuestPDF/Drawing/DocumentCanvases/XpsDocumentCanvas.cs
  54. 2 2
      Source/QuestPDF/Drawing/DocumentContainer.cs
  55. 138 46
      Source/QuestPDF/Drawing/DocumentGenerator.cs
  56. 7 2
      Source/QuestPDF/Drawing/DrawingCanvases/FreeDrawingCanvas.cs
  57. 9 4
      Source/QuestPDF/Drawing/DrawingCanvases/ProxyDrawingCanvas.cs
  58. 22 4
      Source/QuestPDF/Drawing/DrawingCanvases/SkiaDrawingCanvas.cs
  59. 1 1
      Source/QuestPDF/Drawing/Proxy/LayoutProxy.cs
  60. 107 0
      Source/QuestPDF/Drawing/SemanticTreeManager.cs
  61. 26 0
      Source/QuestPDF/Elements/ArtifactTag.cs
  62. 12 3
      Source/QuestPDF/Elements/Dynamic.cs
  63. 2 1
      Source/QuestPDF/Elements/Hyperlink.cs
  64. 9 2
      Source/QuestPDF/Elements/Lazy.cs
  65. 1 0
      Source/QuestPDF/Elements/Line.cs
  66. 6 1
      Source/QuestPDF/Elements/Page.cs
  67. 60 3
      Source/QuestPDF/Elements/RepeatContent.cs
  68. 2 1
      Source/QuestPDF/Elements/SectionLink.cs
  69. 112 0
      Source/QuestPDF/Elements/SemanticTag.cs
  70. 12 0
      Source/QuestPDF/Elements/StyledBox.cs
  71. 203 1
      Source/QuestPDF/Elements/Table/Table.cs
  72. 3 0
      Source/QuestPDF/Elements/Table/TableCell.cs
  73. 2 2
      Source/QuestPDF/Elements/Text/TextBlock.cs
  74. 8 2
      Source/QuestPDF/Fluent/DecorationExtensions.cs
  75. 16 0
      Source/QuestPDF/Fluent/ElementExtensions.cs
  76. 6 2
      Source/QuestPDF/Fluent/MultiColumnExtensions.cs
  77. 324 0
      Source/QuestPDF/Fluent/SemanticExtensions.cs
  78. 54 14
      Source/QuestPDF/Fluent/TableExtensions.cs
  79. 39 6
      Source/QuestPDF/Infrastructure/DocumentSettings.cs
  80. 6 1
      Source/QuestPDF/Infrastructure/IDocumentCanvas.cs
  81. 4 2
      Source/QuestPDF/Infrastructure/IDrawingCanvas.cs
  82. 8 0
      Source/QuestPDF/Infrastructure/ISemanticAware.cs
  83. 2 1
      Source/QuestPDF/QuestPDF.csproj
  84. 36 1
      Source/QuestPDF/Resources/ReleaseNotes.txt
  85. BIN
      Source/QuestPDF/Runtimes/linux-arm64/native/libQuestPdfSkia.so
  86. BIN
      Source/QuestPDF/Runtimes/linux-musl-x64/native/libQuestPdfSkia.so
  87. BIN
      Source/QuestPDF/Runtimes/linux-x64/native/libQuestPdfSkia.so
  88. BIN
      Source/QuestPDF/Runtimes/osx-arm64/native/libQuestPdfSkia.dylib
  89. BIN
      Source/QuestPDF/Runtimes/osx-x64/native/libQuestPdfSkia.dylib
  90. BIN
      Source/QuestPDF/Runtimes/win-x64/native/QuestPdfSkia.dll
  91. BIN
      Source/QuestPDF/Runtimes/win-x86/native/QuestPdfSkia.dll
  92. 24 6
      Source/QuestPDF/Skia/SkCanvas.cs
  93. 1 1
      Source/QuestPDF/Skia/SkNativeDependencyCompatibilityChecker.cs
  94. 26 2
      Source/QuestPDF/Skia/SkPdfDocument.cs
  95. 139 0
      Source/QuestPDF/Skia/SkPdfTag.cs
  96. 14 0
      Source/QuestPDF/Skia/SkSemanticNodeSpecialId.cs

+ 34 - 0
.github/workflows/main.yml

@@ -51,6 +51,10 @@ jobs:
           # required by actions/setup-dotnet
           apt install bash wget --yes
 
+          # required by conformance testing tools: veraPDF and mustang
+          apt install unzip default-jre --yes
+          java -version
+
 
       - name: Install Build Tools (Alpine)
         if: matrix.runtime.name == 'linux-musl-x64'
@@ -72,6 +76,36 @@ jobs:
           dotnet-version: '8.0.x'
 
 
+      - name: Install veraPDF - PDF conformance testing tool
+        if: matrix.runtime.name == 'linux-x64'
+        run: |
+          mkdir -p ~/verapdf
+          cd ~/verapdf
+          
+          wget -q https://software.verapdf.org/rel/verapdf-installer.zip
+          unzip -q verapdf-installer.zip
+          
+          rm verapdf-installer.zip
+          mv verapdf* verapdf 
+
+          cd verapdf
+          printf "1\n\nO\n1\nY\nY\nN\n1\nY\n\n" | ./verapdf-install
+          alias verapdf='/root/verapdf/verapdf'
+          verapdf --version
+
+
+      - name: Install mustang - ZUGFeRD conformance testing tool
+        if: matrix.runtime.name == 'linux-x64'
+        run: |
+          mkdir -p ~/mustang
+          cd ~/mustang
+          
+          wget https://repo1.maven.org/maven2/org/mustangproject/Mustang-CLI/2.20.0/Mustang-CLI-2.20.0.jar -O mustang-cli.jar
+
+          alias mustang='java -jar ~/mustang/mustang-cli.jar'
+          mustang --help
+
+
       - name: Build and test solution
         shell: bash
         working-directory: ./Source

+ 5 - 1
.gitignore

@@ -219,4 +219,8 @@ _Pvt_Extensions
 
 # Project Rider
 *.iml
-.idea
+.idea
+
+# macOS
+.DS_Store
+**/.DS_Store

+ 93 - 0
Source/QuestPDF.ConformanceTests/DecorationTests.cs

@@ -0,0 +1,93 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class DecorationTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        QuestPDF.Settings.EnableDebugging = true;
+        
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Size(600, 975);
+                    page.Margin(50);
+
+                    page.Content()
+                        .Decoration(decoration =>
+                        {
+                            decoration.Before()
+                                .Column(column =>
+                                {
+                                    column.Item()
+                                        .ShowOnce()
+                                        .Height(50)
+                                        .Width(200)
+                                        .SemanticImage("First page: decoration before")
+                                        .Image(Placeholders.Image);
+                                    
+                                    column.Item()
+                                        .SkipOnce()
+                                        .Text("Second page: decoration before");
+                                });
+                            
+                            decoration
+                                .Content()
+                                .PaddingVertical(25)
+                                .Column(column =>
+                                {
+                                    column.Spacing(25);
+                                    
+                                    foreach (var i in Enumerable.Range(1, 15))
+                                    {
+                                        column.Item()
+                                            .Width(200)
+                                            .Height(50)
+                                            .Background(Colors.Grey.Lighten3)
+                                            .AlignCenter()
+                                            .AlignMiddle()
+                                            .Text($"Item {i}");
+                                    }
+                                });
+                            
+                            decoration.After()
+                                .Column(column =>
+                                {
+                                    column.Item()
+                                        .ShowOnce()
+                                        .Height(50)
+                                        .Width(200)
+                                        .SemanticImage("First page: decoration after")
+                                        .Image(Placeholders.Image);
+                                    
+                                    column.Item()
+                                        .SkipOnce()
+                                        .Text("Second page: decoration after");
+                                });
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("Figure", figure => figure.Alt("First page: decoration before"));
+                
+            foreach (var i in Enumerable.Range(1, 10))
+                root.Child("P");
+                
+            root.Child("Figure", figure => figure.Alt("First page: decoration after"));
+            
+            foreach (var i in Enumerable.Range(1, 5))
+                root.Child("P");
+        });
+    }
+}

+ 79 - 0
Source/QuestPDF.ConformanceTests/FooterTests.cs

@@ -0,0 +1,79 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class FooterTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .PaddingBottom(25)
+                        .Column(column =>
+                        {
+                            column.Spacing(25);
+
+                            column.Item()
+                                .SemanticHeader1()
+                                .Text("Conformance Test: Footer")
+                                .FontSize(24)
+                                .Bold()
+                                .FontColor(Colors.Blue.Darken2);
+
+                            column.Item()
+                                .Text("Footer content should not be present in the semantic tree.");
+
+                            column.Item()
+                                .SemanticDivision()
+                                .Column(column =>
+                                {
+                                    foreach (var i in Enumerable.Range(1, 12))
+                                    {
+                                        column.Item()
+                                            .Width(200)
+                                            .Height(100)
+                                            .Background(Colors.Grey.Lighten2)
+                                            .AlignCenter()
+                                            .AlignMiddle()
+                                            .Text($"Item {i}");
+                                    }
+                                });
+                        });
+
+                    page.Footer()
+                        .AlignCenter()
+                        .Text(text =>
+                        {
+                            text.CurrentPageNumber();
+                            text.Span(" / ");
+                            text.TotalPages();
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("H1", h1 => h1.Alt("Conformance Test: Footer"));
+            
+            root.Child("P");
+
+            root.Child("Div", div =>
+            {
+                foreach (var i in Enumerable.Range(1, 12))
+                    div.Child("P");
+            });
+        });
+    }
+}

+ 77 - 0
Source/QuestPDF.ConformanceTests/HeaderTests.cs

@@ -0,0 +1,77 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class HeaderTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Header()
+                        .Column(column =>
+                        {
+                            column.Spacing(25);
+                            
+                            column.Item()
+                                .SemanticHeader1()
+                                .Text("Conformance Test: Header")
+                                .FontSize(24)
+                                .Bold()
+                                .FontColor(Colors.Blue.Darken2);
+
+                            column.Item()
+                                .ShowOnce()
+                                .Text("Only the first page of the Header should be present in the semantic tree.");
+                            
+                            column.Item()
+                                .SkipOnce()
+                                .Text("This item should NOT be present in the semantic tree.");
+                        });
+                    
+                    page.Content()
+                        .PaddingTop(25)
+                        .SemanticDivision()
+                        .Column(column =>
+                        {
+                            column.Spacing(25);
+                            
+                            foreach (var i in Enumerable.Range(1, 12))
+                            {
+                                column.Item()
+                                    .Width(200)
+                                    .Height(100)
+                                    .Background(Colors.Grey.Lighten2)
+                                    .AlignCenter()
+                                    .AlignMiddle()
+                                    .Text($"Item {i}");
+                            }
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("H1", h1 => h1.Alt("Conformance Test: Header"));
+            
+            root.Child("P");
+
+            root.Child("Div", div =>
+            {
+                foreach (var i in Enumerable.Range(1, 12))
+                    div.Child("P");
+            });
+        });
+    }
+}

+ 53 - 0
Source/QuestPDF.ConformanceTests/IgnoreTests.cs

@@ -0,0 +1,53 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class IgnoreTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        var photo = File.ReadAllBytes("Resources/photo.jpeg");
+        
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .PaddingVertical(30)
+                        .Column(column =>
+                        {
+                            column.Spacing(25);
+
+                            column.Item().Text("This photo has semantic meaning:");
+                            
+                            column.Item()
+                                .SemanticImage("A beautiful landscape")
+                                .Image(photo);
+                            
+                            column.Item().Text("While this one doesn't:");
+                            
+                            column.Item()
+                                .SemanticIgnore()
+                                .Image(photo);
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("P");
+            root.Child("Figure", figure => figure.Alt("A beautiful landscape"));
+            root.Child("P");
+        });
+    }
+}

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

@@ -0,0 +1,78 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class ImageTests : ConformanceTestBase
+{
+    [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);
+    }
+
+    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");
+
+        var imageData = File.ReadAllBytes(imagePath);
+        
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .PaddingVertical(30)
+                        .Column(column =>
+                        {
+                            column.Spacing(25);
+
+                            column.Item()
+                                .SemanticHeader1()
+                                .Text("Conformance Test: Images")
+                                .FontSize(24)
+                                .Bold()
+                                .FontColor(Colors.Blue.Darken2);
+                            
+                            column.Item()
+                                .Width(300)
+                                .SemanticImage("Sample image description")
+                                .Column(column =>
+                                {
+                                    column.Item().Image(imageData);
+                                    column.Item().PaddingTop(5).AlignCenter().SemanticCaption().Text("Sample image caption");
+                                });
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("H1", h1 => h1.Alt("Conformance Test: Images"));
+
+            root.Child("Figure", figure =>
+            {
+                figure.Alt("Sample image description");
+                figure.Child("Caption", caption => caption.Child("P"));
+            });
+        });
+    }
+}

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

@@ -0,0 +1,85 @@
+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()
+                                .SemanticHeader1()
+                                .Text("Conformance Test: Line Elements")
+                                .FontSize(24)
+                                .Bold()
+                                .FontColor(Colors.Blue.Darken2);
+
+                            column.Item()
+                                .Text("Line elements should be rendered but semantically treated as artifacts.");
+
+                            column.Item()
+                                .LineHorizontal(2)
+                                .LineColor(Colors.Red.Medium);
+
+                            column.Item()
+                                .Text(Placeholders.LoremIpsum());
+
+                            column.Item()
+                                .LineHorizontal(4)
+                                .LineColor(Colors.Green.Medium)
+                                .LineDashPattern([6, 6, 12, 6]);
+    
+                            column.Item()
+                                .SemanticDivision()
+                                .Background(Colors.Grey.Lighten3).Row(row =>
+                                {
+                                    row.RelativeItem()
+                                        .PaddingVertical(25)
+                                        .AlignRight()
+                                        .Text("Text on the left side");
+                                    
+                                    row.AutoItem()
+                                        .PaddingHorizontal(25)
+                                        .LineVertical(4)
+                                        .LineGradient([ Colors.Blue.Lighten2, Colors.Blue.Darken2 ]);
+                                    
+                                    row.RelativeItem()
+                                        .PaddingVertical(25)
+                                        .Text("Text on the right side");
+                                });
+                        });
+                });
+            });
+    }
+    
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("H1", h1 => h1.Alt("Conformance Test: Line Elements"));
+            
+            root.Child("P");
+            root.Child("P");
+            root.Child("Div", div =>
+            {
+                div.Child("P");
+                div.Child("P");
+            });
+        });
+    }
+}

+ 143 - 0
Source/QuestPDF.ConformanceTests/ListTests.cs

@@ -0,0 +1,143 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class ListTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .PaddingVertical(30)
+                        .SemanticSection()
+                        .Column(column =>
+                        {
+                            column.Spacing(15);
+
+                            column.Item()
+                                .SemanticHeader1()
+                                .Text("Conformance Test: Lists")
+                                .FontSize(36)
+                                .Bold()
+                                .FontColor(Colors.Blue.Darken2);
+
+                            column.Item()
+                                .SemanticList()
+                                .Column(listColumn =>
+                                {
+                                    listColumn.Spacing(10);
+
+                                    listColumn.Item()
+                                        .SemanticListItem()
+                                        .Row(row =>
+                                        {
+                                            row.ConstantItem(20).SemanticListLabel().Text("1.");
+
+                                            row.RelativeItem()
+                                                .SemanticListItemBody()
+                                                .Column(bodyColumn =>
+                                                {
+                                                    bodyColumn.Spacing(8);
+
+                                                    bodyColumn.Item().Text(Placeholders.Sentence());
+
+                                                    bodyColumn.Item()
+                                                        .SemanticList()
+                                                        .Column(nestedColumn =>
+                                                        {
+                                                            nestedColumn.Spacing(10);
+
+                                                            foreach (var i in Enumerable.Range(1, 4))
+                                                            {
+                                                                nestedColumn.Item()
+                                                                    .SemanticListItem()
+                                                                    .Row(nestedRow =>
+                                                                    {
+                                                                        nestedRow.ConstantItem(10)
+                                                                            .SemanticListLabel()
+                                                                            .Text("-");
+
+                                                                        nestedRow.RelativeItem()
+                                                                            .SemanticListItemBody()
+                                                                            .Text(Placeholders.Sentence());
+                                                                    });
+                                                            }
+                                                        });
+                                                });
+                                        });
+
+                                    foreach (var i in Enumerable.Range(2, 5))
+                                    {
+                                        listColumn.Item()
+                                            .SemanticListItem()
+                                            .Row(row =>
+                                            {
+                                                row.ConstantItem(20)
+                                                    .SemanticListLabel()
+                                                    .Text($"{i}.");
+
+                                                row.RelativeItem()
+                                                    .SemanticListItemBody()
+                                                    .Text(Placeholders.Sentence());
+                                            });
+                                    }
+                                });
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("Sect", sect =>
+            {
+                sect.Child("H1", h1 => h1.Alt("Conformance Test: Lists"));
+
+                sect.Child("L", list =>
+                {
+                    list.Child("LI", listItem =>
+                    {
+                        listItem.Child("Lbl");
+                        listItem.Child("LBody", lBody =>
+                        {
+                            lBody.Child("P");
+
+                            lBody.Child("L", nestedList =>
+                            {
+                                foreach (var i in Enumerable.Range(1, 4))
+                                {
+                                    nestedList.Child("LI", nestedItem =>
+                                    {
+                                        nestedItem.Child("Lbl");
+                                        nestedItem.Child("LBody", listBody => listBody.Child("P"));
+                                    });
+                                }
+                            });
+                        });
+                    });
+
+                    foreach (var i in Enumerable.Range(2, 5))
+                    {
+                        list.Child("LI", listItem =>
+                        {
+                            listItem.Child("Lbl");
+                            listItem.Child("LBody", listBody => listBody.Child("P"));
+                        });
+                    }
+                });
+            });
+        });
+    }
+}

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

@@ -0,0 +1,67 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class MultiColumnTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .MultiColumn(multiColumn =>
+                        {
+                            multiColumn.Spacing(75);
+                            
+                            multiColumn
+                                .Spacer()
+                                .PaddingHorizontal(25)
+                                .Background(Colors.Blue.Lighten4)
+                                .RotateLeft()
+                                .AlignMiddle()
+                                .AlignCenter()
+                                .Text("This text should not be a part of the semantic tree")
+                                .FontColor(Colors.Blue.Darken4)
+                                .Bold();
+                                
+                            multiColumn
+                                .Content()
+                                .Column(column =>
+                                {
+                                    column.Spacing(25);
+                                    
+                                    foreach (var i in Enumerable.Range(1, 25))
+                                    {
+                                        column.Item()
+                                            .AlignCenter()
+                                            .Background(Colors.Grey.Lighten3)
+                                            .Padding(10)
+                                            .Text(text =>
+                                            {
+                                                text.Span($"Chapter {i}: ").Bold();
+                                                text.Span(Placeholders.LoremIpsum());
+                                            });
+                                    }
+                                });
+                        });
+                });
+            });
+    }
+    
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            foreach (var i in Enumerable.Range(1, 25))
+                root.Child("P");
+        });
+    }
+}

+ 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.13.0" />
+        <PackageReference Include="NUnit" Version="3.14.0"/>
+        <PackageReference Include="NUnit.Analyzers" Version="4.11.0">
+          <PrivateAssets>all</PrivateAssets>
+          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+        </PackageReference>
+        <PackageReference Include="NUnit3TestAdapter" Version="5.2.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>
+    </ItemGroup>
+</Project>

File diff suppressed because it is too large
+ 0 - 0
Source/QuestPDF.ConformanceTests/Resources/image.svg


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


+ 191 - 0
Source/QuestPDF.ConformanceTests/Resources/zugferd-factur-x.xml

@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
+    <rsm:ExchangedDocumentContext>
+        <ram:GuidelineSpecifiedDocumentContextParameter>
+            <ram:ID>urn:cen.eu:en16931:2017</ram:ID>
+        </ram:GuidelineSpecifiedDocumentContextParameter>
+    </rsm:ExchangedDocumentContext>
+    <rsm:ExchangedDocument>
+        <ram:ID>RE-20201121/508</ram:ID>
+        <ram:TypeCode>380</ram:TypeCode>
+        <ram:IssueDateTime>
+            <udt:DateTimeString format="102">20201121</udt:DateTimeString>f
+        </ram:IssueDateTime>
+    </rsm:ExchangedDocument>
+    <rsm:SupplyChainTradeTransaction>
+        <ram:IncludedSupplyChainTradeLineItem>
+            <ram:AssociatedDocumentLineDocument>
+                <ram:LineID>1</ram:LineID>
+            </ram:AssociatedDocumentLineDocument>
+            <ram:SpecifiedTradeProduct>
+                <ram:Name>Design (hours)</ram:Name>
+                <ram:Description>Of a sample invoice</ram:Description>
+            </ram:SpecifiedTradeProduct>
+            <ram:SpecifiedLineTradeAgreement>
+                <ram:GrossPriceProductTradePrice>
+                    <ram:ChargeAmount>160.0000</ram:ChargeAmount>
+                    <ram:BasisQuantity unitCode="HUR">1.0000</ram:BasisQuantity>
+                </ram:GrossPriceProductTradePrice>
+                <ram:NetPriceProductTradePrice>
+                    <ram:ChargeAmount>160.0000</ram:ChargeAmount>
+                    <ram:BasisQuantity unitCode="HUR">1.0000</ram:BasisQuantity>
+                </ram:NetPriceProductTradePrice>
+            </ram:SpecifiedLineTradeAgreement>
+            <ram:SpecifiedLineTradeDelivery>
+                <ram:BilledQuantity unitCode="HUR">1.0000</ram:BilledQuantity>
+            </ram:SpecifiedLineTradeDelivery>
+            <ram:SpecifiedLineTradeSettlement>
+                <ram:ApplicableTradeTax>
+                    <ram:TypeCode>VAT</ram:TypeCode>
+                    <ram:CategoryCode>S</ram:CategoryCode>
+                    <ram:RateApplicablePercent>7.00</ram:RateApplicablePercent>
+                </ram:ApplicableTradeTax>
+                <ram:SpecifiedTradeSettlementLineMonetarySummation>
+                    <ram:LineTotalAmount>160.00</ram:LineTotalAmount>
+                </ram:SpecifiedTradeSettlementLineMonetarySummation>
+            </ram:SpecifiedLineTradeSettlement>
+        </ram:IncludedSupplyChainTradeLineItem>
+        <ram:IncludedSupplyChainTradeLineItem>
+            <ram:AssociatedDocumentLineDocument>
+                <ram:LineID>2</ram:LineID>
+            </ram:AssociatedDocumentLineDocument>
+            <ram:SpecifiedTradeProduct>
+                <ram:Name>Ballons</ram:Name>
+                <ram:Description>various colors, ~2000ml</ram:Description>
+            </ram:SpecifiedTradeProduct>
+            <ram:SpecifiedLineTradeAgreement>
+                <ram:GrossPriceProductTradePrice>
+                    <ram:ChargeAmount>0.7900</ram:ChargeAmount>
+                    <ram:BasisQuantity unitCode="H87">1.0000</ram:BasisQuantity>
+                </ram:GrossPriceProductTradePrice>
+                <ram:NetPriceProductTradePrice>
+                    <ram:ChargeAmount>0.7900</ram:ChargeAmount>
+                    <ram:BasisQuantity unitCode="H87">1.0000</ram:BasisQuantity>
+                </ram:NetPriceProductTradePrice>
+            </ram:SpecifiedLineTradeAgreement>
+            <ram:SpecifiedLineTradeDelivery>
+                <ram:BilledQuantity unitCode="H87">400.0000</ram:BilledQuantity>
+            </ram:SpecifiedLineTradeDelivery>
+            <ram:SpecifiedLineTradeSettlement>
+                <ram:ApplicableTradeTax>
+                    <ram:TypeCode>VAT</ram:TypeCode>
+                    <ram:CategoryCode>S</ram:CategoryCode>
+                    <ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
+                </ram:ApplicableTradeTax>
+                <ram:SpecifiedTradeSettlementLineMonetarySummation>
+                    <ram:LineTotalAmount>316.00</ram:LineTotalAmount>
+                </ram:SpecifiedTradeSettlementLineMonetarySummation>
+            </ram:SpecifiedLineTradeSettlement>
+        </ram:IncludedSupplyChainTradeLineItem>
+        <ram:IncludedSupplyChainTradeLineItem>
+            <ram:AssociatedDocumentLineDocument>
+                <ram:LineID>3</ram:LineID>
+            </ram:AssociatedDocumentLineDocument>
+            <ram:SpecifiedTradeProduct>
+                <ram:Name>Hot air „heiße Luft“ (litres)</ram:Name>
+                <ram:Description/>
+            </ram:SpecifiedTradeProduct>
+            <ram:SpecifiedLineTradeAgreement>
+                <ram:GrossPriceProductTradePrice>
+                    <ram:ChargeAmount>0.0250</ram:ChargeAmount>
+                    <ram:BasisQuantity unitCode="LTR">1.0000</ram:BasisQuantity>
+                </ram:GrossPriceProductTradePrice>
+                <ram:NetPriceProductTradePrice>
+                    <ram:ChargeAmount>0.0250</ram:ChargeAmount>
+                    <ram:BasisQuantity unitCode="LTR">1.0000</ram:BasisQuantity>
+                </ram:NetPriceProductTradePrice>
+            </ram:SpecifiedLineTradeAgreement>
+            <ram:SpecifiedLineTradeDelivery>
+                <ram:BilledQuantity unitCode="LTR">800.0000</ram:BilledQuantity>
+            </ram:SpecifiedLineTradeDelivery>
+            <ram:SpecifiedLineTradeSettlement>
+                <ram:ApplicableTradeTax>
+                    <ram:TypeCode>VAT</ram:TypeCode>
+                    <ram:CategoryCode>S</ram:CategoryCode>
+                    <ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
+                </ram:ApplicableTradeTax>
+                <ram:SpecifiedTradeSettlementLineMonetarySummation>
+                    <ram:LineTotalAmount>20.00</ram:LineTotalAmount>
+                </ram:SpecifiedTradeSettlementLineMonetarySummation>
+            </ram:SpecifiedLineTradeSettlement>
+        </ram:IncludedSupplyChainTradeLineItem>
+        <ram:ApplicableHeaderTradeAgreement>
+            <ram:BuyerReference>AB321</ram:BuyerReference>
+            <ram:SellerTradeParty>
+                <ram:Name>Bei Spiel GmbH</ram:Name>
+                <ram:PostalTradeAddress>
+                    <ram:PostcodeCode>12345</ram:PostcodeCode>
+                    <ram:LineOne>Ecke 12</ram:LineOne>
+                    <ram:CityName>Stadthausen</ram:CityName>
+                    <ram:CountryID>DE</ram:CountryID>
+                </ram:PostalTradeAddress>
+                <ram:SpecifiedTaxRegistration>
+                    <ram:ID schemeID="VA">DE136695976</ram:ID>
+                </ram:SpecifiedTaxRegistration>
+            </ram:SellerTradeParty>
+            <ram:BuyerTradeParty>
+                <ram:ID>2</ram:ID>
+                <ram:Name>Theodor Est</ram:Name>
+                <ram:PostalTradeAddress>
+                    <ram:PostcodeCode>88802</ram:PostcodeCode>
+                    <ram:LineOne>Bahnstr. 42</ram:LineOne>
+                    <ram:CityName>Spielkreis</ram:CityName>
+                    <ram:CountryID>DE</ram:CountryID>
+                </ram:PostalTradeAddress>
+            </ram:BuyerTradeParty>
+        </ram:ApplicableHeaderTradeAgreement>
+        <ram:ApplicableHeaderTradeDelivery>
+            <ram:ActualDeliverySupplyChainEvent>
+                <ram:OccurrenceDateTime>
+                    <udt:DateTimeString format="102">20201110</udt:DateTimeString>
+                </ram:OccurrenceDateTime>
+            </ram:ActualDeliverySupplyChainEvent>
+        </ram:ApplicableHeaderTradeDelivery>
+        <ram:ApplicableHeaderTradeSettlement>
+            <ram:PaymentReference>RE-20201121/508</ram:PaymentReference>
+            <ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
+            <ram:SpecifiedTradeSettlementPaymentMeans>
+                <ram:TypeCode>42</ram:TypeCode>
+                <ram:Information>Bank transfer</ram:Information>
+                <ram:PayeePartyCreditorFinancialAccount>
+                    <ram:IBANID>DE88200800000970375700</ram:IBANID>
+                    <ram:AccountName>Max Mustermann</ram:AccountName>
+                </ram:PayeePartyCreditorFinancialAccount>
+                <ram:PayeeSpecifiedCreditorFinancialInstitution>
+                    <ram:BICID>COBADEFFXXX</ram:BICID>
+                </ram:PayeeSpecifiedCreditorFinancialInstitution>
+            </ram:SpecifiedTradeSettlementPaymentMeans>
+            <ram:ApplicableTradeTax>
+                <ram:CalculatedAmount>11.20</ram:CalculatedAmount>
+                <ram:TypeCode>VAT</ram:TypeCode>
+                <ram:BasisAmount>160.00</ram:BasisAmount>
+                <ram:CategoryCode>S</ram:CategoryCode>
+                <ram:RateApplicablePercent>7.00</ram:RateApplicablePercent>
+            </ram:ApplicableTradeTax>
+            <ram:ApplicableTradeTax>
+                <ram:CalculatedAmount>63.84</ram:CalculatedAmount>
+                <ram:TypeCode>VAT</ram:TypeCode>
+                <ram:BasisAmount>336.00</ram:BasisAmount>
+                <ram:CategoryCode>S</ram:CategoryCode>
+                <ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
+            </ram:ApplicableTradeTax>
+            <ram:SpecifiedTradePaymentTerms>
+                <ram:Description>Zahlbar ohne Abzug bis 12.12.2020</ram:Description>
+                <ram:DueDateDateTime>
+                    <udt:DateTimeString format="102">20201212</udt:DateTimeString>
+                </ram:DueDateDateTime>
+            </ram:SpecifiedTradePaymentTerms>
+            <ram:SpecifiedTradeSettlementHeaderMonetarySummation>
+                <ram:LineTotalAmount>496.00</ram:LineTotalAmount>
+                <ram:ChargeTotalAmount>0.00</ram:ChargeTotalAmount>
+                <ram:AllowanceTotalAmount>0.00</ram:AllowanceTotalAmount>
+                <ram:TaxBasisTotalAmount>496.00</ram:TaxBasisTotalAmount>
+                <ram:TaxTotalAmount currencyID="EUR">75.04</ram:TaxTotalAmount>
+                <ram:GrandTotalAmount>571.04</ram:GrandTotalAmount>
+                <ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>
+                <ram:DuePayableAmount>571.04</ram:DuePayableAmount>
+            </ram:SpecifiedTradeSettlementHeaderMonetarySummation>
+        </ram:ApplicableHeaderTradeSettlement>
+    </rsm:SupplyChainTradeTransaction>
+</rsm:CrossIndustryInvoice>

+ 48 - 0
Source/QuestPDF.ConformanceTests/Resources/zugferd-xmp-metadata.xml

@@ -0,0 +1,48 @@
+<rdf:Description xmlns:fx="urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#" rdf:about="">
+    <fx:ConformanceLevel>EN 16931</fx:ConformanceLevel>
+    <fx:DocumentType>INVOICE</fx:DocumentType>
+    <fx:DocumentFileName>factur-x.xml</fx:DocumentFileName>
+    <fx:Version>1.0</fx:Version>
+</rdf:Description>
+<rdf:Description xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
+                 xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#"
+                 xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"
+                 rdf:about="">
+<pdfaExtension:schemas>
+    <rdf:Bag>
+        <rdf:li rdf:parseType="Resource">
+            <pdfaSchema:schema>ZUGFeRD PDFA Extension Schema</pdfaSchema:schema>
+            <pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
+            <pdfaSchema:prefix>fx</pdfaSchema:prefix>
+            <pdfaSchema:property>
+                <rdf:Seq>
+                    <rdf:li rdf:parseType="Resource">
+                        <pdfaProperty:name>DocumentFileName</pdfaProperty:name>
+                        <pdfaProperty:valueType>Text</pdfaProperty:valueType>
+                        <pdfaProperty:category>external</pdfaProperty:category>
+                        <pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
+                    </rdf:li>
+                    <rdf:li rdf:parseType="Resource">
+                        <pdfaProperty:name>DocumentType</pdfaProperty:name>
+                        <pdfaProperty:valueType>Text</pdfaProperty:valueType>
+                        <pdfaProperty:category>external</pdfaProperty:category>
+                        <pdfaProperty:description>INVOICE</pdfaProperty:description>
+                    </rdf:li>
+                    <rdf:li rdf:parseType="Resource">
+                        <pdfaProperty:name>Version</pdfaProperty:name>
+                        <pdfaProperty:valueType>Text</pdfaProperty:valueType>
+                        <pdfaProperty:category>external</pdfaProperty:category>
+                        <pdfaProperty:description>The actual version of the ZUGFeRD XML schema</pdfaProperty:description>
+                    </rdf:li>
+                    <rdf:li rdf:parseType="Resource">
+                        <pdfaProperty:name>ConformanceLevel</pdfaProperty:name>
+                        <pdfaProperty:valueType>Text</pdfaProperty:valueType>
+                        <pdfaProperty:category>external</pdfaProperty:category>
+                        <pdfaProperty:description>The selected ZUGFeRD profile completeness</pdfaProperty:description>
+                    </rdf:li>
+                </rdf:Seq>
+            </pdfaSchema:property>
+        </rdf:li>
+    </rdf:Bag>
+</pdfaExtension:schemas>
+</rdf:Description>

+ 87 - 0
Source/QuestPDF.ConformanceTests/StyledBoxTests.cs

@@ -0,0 +1,87 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class StyledBoxTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        var avoidTransparency = TestContext.CurrentContext.Test.Arguments.FirstOrDefault() is PDFA_Conformance.PDFA_1A or PDFA_Conformance.PDFA_1B;
+        
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .PaddingVertical(30)
+                        .SemanticSection()
+                        .Column(column =>
+                        {
+                            column.Spacing(30);
+
+                            column.Item()
+                                .SemanticHeader1()
+                                .Text("Conformance Test: Styled Boxes")
+                                .FontSize(36)
+                                .Bold()
+                                .FontColor(Colors.Blue.Darken2);
+
+                            column.Item()
+                                .Background(Colors.Blue.Lighten4)
+                                .Padding(20)
+                                .Text("Background only")
+                                .FontSize(16);
+
+                            column.Item()
+                                .Border(2, Colors.Blue.Darken2)
+                                .Padding(20)
+                                .Text("Border only")
+                                .FontSize(16);
+
+                            column.Item()
+                                .Background(Colors.White)
+                                .Shadow(new BoxShadowStyle
+                                {
+                                    OffsetX = 5,
+                                    OffsetY = 5,
+                                    Blur = avoidTransparency ? 0 : 10,
+                                    Spread = 5,
+                                    Color = Colors.Grey.Medium
+                                })
+                                .Padding(20)
+                                .Text("Simple shadow")
+                                .FontSize(16);
+
+                            column.Item()
+                                .Border(1, Colors.Purple.Lighten4)
+                                .Background(Colors.Purple.Lighten5)
+                                .CornerRadius(15)
+                                .Padding(20)
+                                .Text("Rounded corners")
+                                .FontSize(16);
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("Sect", sect =>
+            {
+                sect.Child("H1", h1 => h1.Alt("Conformance Test: Styled Boxes"));
+
+                foreach (var i in Enumerable.Range(1, 4))
+                    sect.Child("P");
+            });
+        });
+    }
+}

+ 51 - 0
Source/QuestPDF.ConformanceTests/SvgTests.cs

@@ -0,0 +1,51 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class SvgTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .Column(column =>
+                        {
+                            column.Spacing(25);
+
+                            column.Item()
+                                .SemanticHeader1()
+                                .Text("Conformance Test: SVG")
+                                .FontSize(24)
+                                .Bold()
+                                .FontColor(Colors.Blue.Darken2);
+
+                            column.Item()
+                                .Text("SVG content should be rendered correctly and possible to be annotated as semantic image. Image taken from: undraw.co");
+
+                            column.Item()
+                                .SemanticImage("Sample SVG image description")
+                                .Svg("Resources/image.svg");
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("H1", h1 => h1.Alt("Conformance Test: SVG"));
+            root.Child("P");
+            root.Child("Figure", figure => figure.Alt("Sample SVG image description"));
+        });
+    }
+}

+ 110 - 0
Source/QuestPDF.ConformanceTests/Table/TableWithFooterTests.cs

@@ -0,0 +1,110 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests.Table;
+
+internal class TableWithFooterTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .Shrink()
+                        .Border(1)
+                        .BorderColor(Colors.Grey.Darken1)
+                        .SemanticTable()
+                        .Table(table =>
+                        {
+                            table.ColumnsDefinition(columns =>
+                            {
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                            });
+
+                            foreach (var i in Enumerable.Range(1, 30))
+                            {
+                                table.Cell().Element(CellStyle).Text($"{i}/1");
+                                table.Cell().Element(CellStyle).Text($"{i}/2");
+                                table.Cell().Element(CellStyle).Text($"{i}/3");
+                            }
+                            
+                            table.Footer(footer =>
+                            {
+                                footer.Cell().Element(FooterCellStyle).Text("F11");
+                                footer.Cell().Element(FooterCellStyle).Text("F12");
+                                footer.Cell().Element(FooterCellStyle).Text("F13");
+                                
+                                footer.Cell().Element(FooterCellStyle).Text("F21");
+                                footer.Cell().Element(FooterCellStyle).Text("F22");
+                                footer.Cell().Element(FooterCellStyle).Text("F23");
+                            });
+                            
+                            IContainer CellStyle(IContainer container) =>
+                                container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten2)
+                                    .Padding(8);
+                            
+                            IContainer FooterCellStyle(IContainer container) =>
+                                container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten2)
+                                    .Background(Colors.Grey.Lighten3)
+                                    .Padding(8);
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("Table", table =>
+            {
+                table.Child("TBody", tbody =>
+                {
+                    foreach (var i in Enumerable.Range(1, 30))
+                    {
+                        tbody.Child("TR", row =>
+                        {
+                            row.Child("TD", td => td.Child("P"));
+                            row.Child("TD", td => td.Child("P"));
+                            row.Child("TD", td => td.Child("P"));
+                        });
+                    }
+                });
+                
+                table.Child("TFoot", footer =>
+                {
+                    footer.Child("TR", tfoot =>
+                    {
+                        tfoot.Child("TR", row =>
+                        {
+                            row.Child("TD", td => td.Child("P"));
+                            row.Child("TD", td => td.Child("P"));
+                            row.Child("TD", td => td.Child("P"));
+                        });
+                        
+                        tfoot.Child("TR", row =>
+                        {
+                            row.Child("TD", td => td.Child("P"));
+                            row.Child("TD", td => td.Child("P"));
+                            row.Child("TD", td => td.Child("P"));
+                        });
+                    });
+                });
+            });
+        });
+    }
+}

+ 223 - 0
Source/QuestPDF.ConformanceTests/Table/TableWithHeaderCellsSpanningMultipleColumnsTests.cs

@@ -0,0 +1,223 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests.Table;
+
+internal class TableWithHeaderCellsSpanningMultipleColumnsTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .Shrink()
+                        .Border(1)
+                        .BorderColor(Colors.Grey.Darken1)
+                        .SemanticTable()
+                        .Table(table =>
+                        {
+                            table.ColumnsDefinition(columns =>
+                            {
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                            });
+                            
+                            table.Header(header =>
+                            {
+                                header.Cell().RowSpan(2).Element(HeaderCellStyle).Text("Paper Type");
+                                header.Cell().ColumnSpan(2).Element(HeaderCellStyle).Text("Width");
+                                header.Cell().ColumnSpan(2).Element(HeaderCellStyle).Text("Height");
+                                header.Cell().Element(HeaderCellStyle).Text("Inches");
+                                header.Cell().Element(HeaderCellStyle).Text("Points");
+                                header.Cell().Element(HeaderCellStyle).Text("Inches");
+                                header.Cell().Element(HeaderCellStyle).Text("Points");
+                            });
+
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("A3");
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                                
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("A4");
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                            
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("A5");
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Decimal());
+                            
+                            IContainer HeaderCellStyle(IContainer container) =>
+                                container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten2)
+                                    .Background(Colors.Grey.Lighten3)
+                                    .Padding(8)
+                                    .AlignMiddle()
+                                    .DefaultTextStyle(x => x.Bold());
+
+                            IContainer CellStyle(IContainer container) =>
+                                container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten2)
+                                    .Padding(8);
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("Table", table =>
+            {
+                table.Child("THead", thead =>
+                {
+                    thead.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th
+                            .Id(5)
+                            .Attribute("Table", "RowSpan", 2)
+                            .Child("P"));
+                        
+                        row.Child("TH", th => th
+                            .Id(6)
+                            .Attribute("Table", "ColSpan", 2)
+                            .Child("P"));
+                        
+                        row.Child("TH", th => th
+                            .Id(7)
+                            .Attribute("Table", "ColSpan", 2)
+                            .Child("P"));
+                    });
+                    
+                    thead.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th
+                            .Id(9)
+                            .Attribute("Table", "Headers", new[] { 6 })
+                            .Child("P"));
+                        
+                        row.Child("TH", th => th
+                            .Id(10)
+                            .Attribute("Table", "Headers", new[] { 6 })
+                            .Child("P"));
+                        
+                        row.Child("TH", th => th
+                            .Id(11)
+                            .Attribute("Table", "Headers", new[] { 7 })
+                            .Child("P"));
+                        
+                        row.Child("TH", th => th
+                            .Id(12)
+                            .Attribute("Table", "Headers", new[] { 7 })
+                            .Child("P"));
+                    });
+                });
+
+                table.Child("TBody", tbody =>
+                {
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th
+                            .Id(22)
+                            .Attribute("Table", "Headers", new[] { 5 })
+                            .Child("P"));
+                        
+                        row.Child("TD", th => th
+                            .Id(23)
+                            .Attribute("Table", "Headers", new[] { 6, 9, 22 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(24)
+                            .Attribute("Table", "Headers", new[] { 6, 10, 22 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(25)
+                            .Attribute("Table", "Headers", new[] { 7, 11, 22 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(26)
+                            .Attribute("Table", "Headers", new[] { 7, 12, 22 })
+                            .Child("P"));
+                    });
+                    
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th
+                            .Id(28)
+                            .Attribute("Table", "Headers", new[] { 5 })
+                            .Child("P"));
+                        
+                        row.Child("TD", th => th
+                            .Id(29)
+                            .Attribute("Table", "Headers", new[] { 6, 9, 28 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(30)
+                            .Attribute("Table", "Headers", new[] { 6, 10, 28 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(31)
+                            .Attribute("Table", "Headers", new[] { 7, 11, 28 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(32)
+                            .Attribute("Table", "Headers", new[] { 7, 12, 28 })
+                            .Child("P"));
+                    });
+                    
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th
+                            .Id(34)
+                            .Attribute("Table", "Headers", new[] { 5 })
+                            .Child("P"));
+                        
+                        row.Child("TD", th => th
+                            .Id(35)
+                            .Attribute("Table", "Headers", new[] { 6, 9, 34 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(36)
+                            .Attribute("Table", "Headers", new[] { 6, 10, 34 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(37)
+                            .Attribute("Table", "Headers", new[] { 7, 11, 34 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(38)
+                            .Attribute("Table", "Headers", new[] { 7, 12, 34 })
+                            .Child("P"));
+                    });
+                });
+            });
+        });
+    }
+}

+ 232 - 0
Source/QuestPDF.ConformanceTests/Table/TableWithHeaderCellsSpanningMultipleRowsTests.cs

@@ -0,0 +1,232 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests.Table;
+
+internal class TableWithHeaderCellsSpanningMultipleRowsTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .Shrink()
+                        .Border(1)
+                        .BorderColor(Colors.Grey.Darken1)
+                        .SemanticTable()
+                        .Table(table =>
+                        {
+                            table.ColumnsDefinition(columns =>
+                            {
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                            });
+                            
+                            table.Header(header =>
+                            {
+                                header.Cell().Element(HeaderCellStyle).Text("Year");
+                                header.Cell().Element(HeaderCellStyle).Text("Quarter");
+                                header.Cell().Element(HeaderCellStyle).Text("Outcome");
+                                header.Cell().Element(HeaderCellStyle).Text("Income");
+                            });
+
+                            table.Cell().RowSpan(4).AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("2024");
+                                
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q1");
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+                                
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q2");
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+                                
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q3");
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+                                
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q4");
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+
+                            table.Cell().RowSpan(2).AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("2025");
+
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q1");
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+                                
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q2");
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+                            table.Cell().Element(CellStyle).Text(Placeholders.Price());
+                            
+                            IContainer HeaderCellStyle(IContainer container) =>
+                                container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten2)
+                                    .Background(Colors.Grey.Lighten3)
+                                    .Padding(8)
+                                    .AlignMiddle()
+                                    .DefaultTextStyle(x => x.Bold());
+
+                            IContainer CellStyle(IContainer container) =>
+                                container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten2)
+                                    .Padding(8);
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("Table", table =>
+            {
+                table.Child("THead", thead =>
+                {
+                    thead.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th.Id(5).Child("P"));
+                        row.Child("TH", th => th.Id(6).Child("P"));
+                        row.Child("TH", th => th.Id(7).Child("P"));
+                        row.Child("TH", th => th.Id(8).Child("P"));
+                    });
+                });
+                
+                table.Child("TBody", tbody =>
+                {
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th
+                            .Id(15)
+                            .Attribute("Table", "RowSpan", 4)
+                            .Attribute("Table", "Headers", new[] { 5 })
+                            .Child("P"));
+                        
+                        row.Child("TH", th => th
+                            .Id(16)
+                            .Attribute("Table", "Headers", new[] { 6, 15 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(17)
+                            .Attribute("Table", "Headers", new[] { 7, 15, 16 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(18)
+                            .Attribute("Table", "Headers", new[] { 8, 15, 16 })
+                            .Child("P"));
+                    });
+                    
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th
+                            .Id(20)
+                            .Attribute("Table", "Headers", new[] { 6, 15 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(21)
+                            .Attribute("Table", "Headers", new[] { 7, 15, 20 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(22)
+                            .Attribute("Table", "Headers", new[] { 8, 15, 20 })
+                            .Child("P"));
+                    });
+                    
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th
+                            .Id(24)
+                            .Attribute("Table", "Headers", new[] { 6, 15 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(25)
+                            .Attribute("Table", "Headers", new[] { 7, 15, 24 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(26)
+                            .Attribute("Table", "Headers", new[] { 8, 15, 24 })
+                            .Child("P"));
+                    });
+                    
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th
+                            .Id(28)
+                            .Attribute("Table", "Headers", new[] { 6, 15 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(29)
+                            .Attribute("Table", "Headers", new[] { 7, 15, 28 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(30)
+                            .Attribute("Table", "Headers", new[] { 8, 15, 28 })
+                            .Child("P"));
+                    });
+
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th
+                            .Id(32)
+                            .Attribute("Table", "RowSpan", 2)
+                            .Attribute("Table", "Headers", new[] { 5 })
+                            .Child("P"));
+                        
+                        row.Child("TH", th => th
+                            .Id(33)
+                            .Attribute("Table", "Headers", new[] { 6, 32 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(34)
+                            .Attribute("Table", "Headers", new[] { 7, 32, 33 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(35)
+                            .Attribute("Table", "Headers", new[] { 8, 32, 33 })
+                            .Child("P"));
+                    });
+                    
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th
+                            .Id(37)
+                            .Attribute("Table", "Headers", new[] { 6, 32 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(38)
+                            .Attribute("Table", "Headers", new[] { 7, 32, 37 })
+                            .Child("P"));
+                        
+                        row.Child("TD", td => td
+                            .Id(39)
+                            .Attribute("Table", "Headers", new[] { 8, 32, 37 })
+                            .Child("P"));
+                    });
+                });
+            });
+        });
+    }
+}

+ 112 - 0
Source/QuestPDF.ConformanceTests/Table/TableWithHorizontalHeadersTests.cs

@@ -0,0 +1,112 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests.Table;
+
+internal class TableWithHorizontalHeadersTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .Shrink()
+                        .Border(1)
+                        .BorderColor(Colors.Grey.Darken1)
+                        .SemanticTable()                        
+                        .Table(table =>
+                        {
+                            table.ColumnsDefinition(columns =>
+                            {
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                            });
+
+                            // Row 1: Name
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Name");
+                            table.Cell().Element(CellStyle).Text("John Smith");
+                            table.Cell().Element(CellStyle).Text("Jane Doe");
+
+                            // Row 2: Position
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Position");
+                            table.Cell().Element(CellStyle).Text("Senior Developer");
+                            table.Cell().Element(CellStyle).Text("UX Designer");
+
+                            // Row 3: Department
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Department");
+                            table.Cell().Element(CellStyle).Text("Engineering");
+                            table.Cell().Element(CellStyle).Text("Design");
+
+                            // Row 4: Experience
+                            table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Experience");
+                            table.Cell().Element(CellStyle).Text("5 years");
+                            table.Cell().Element(CellStyle).Text("3 years");
+
+                            IContainer HeaderCellStyle(IContainer container) =>
+                                container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten2)
+                                    .Background(Colors.Grey.Lighten3)
+                                    .Padding(8)
+                                    .AlignMiddle()
+                                    .DefaultTextStyle(x => x.Bold());
+
+                            IContainer CellStyle(IContainer container) =>
+                                container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten2)
+                                    .Padding(8);
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("Table", table =>
+            {
+                table.Child("TBody", tbody =>
+                {
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th.Attribute("Table", "Scope", "Row").Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                    });
+
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th.Attribute("Table", "Scope", "Row").Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                    });
+
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th.Attribute("Table", "Scope", "Row").Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                    });
+
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th.Attribute("Table", "Scope", "Row").Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                    });
+                });
+            });
+        });
+    }
+}

+ 111 - 0
Source/QuestPDF.ConformanceTests/Table/TableWithVerticalHeadersTests.cs

@@ -0,0 +1,111 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests.Table;
+
+internal class TableWithVerticalHeadersTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    { 
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .Shrink()
+                        .Border(1)
+                        .BorderColor(Colors.Grey.Darken1)
+                        .SemanticTable()
+                        .Table(table =>
+                        {
+                            table.ColumnsDefinition(columns =>
+                            {
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                            });
+                            
+                            table.Header(header =>
+                            {
+                                header.Cell().Element(HeaderCellStyle).Text("Name");
+                                header.Cell().Element(HeaderCellStyle).Text("Position");
+                                header.Cell().Element(HeaderCellStyle).Text("Department");
+                                header.Cell().Element(HeaderCellStyle).Text("Experience");
+                            });
+
+                            // Row 1:
+                            table.Cell().Element(CellStyle).Text("John Smith");
+                            table.Cell().Element(CellStyle).Text("Senior Developer");
+                            table.Cell().Element(CellStyle).Text("Engineering");
+                            table.Cell().Element(CellStyle).Text("5 years");
+                            
+                            // Row 2:
+                            table.Cell().Element(CellStyle).Text("Jane Doe");
+                            table.Cell().Element(CellStyle).Text("UX Designer");
+                            table.Cell().Element(CellStyle).Text("Design");
+                            table.Cell().Element(CellStyle).Text("3 years");
+                            IContainer HeaderCellStyle(IContainer container) =>
+                                container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten2)
+                                    .Background(Colors.Grey.Lighten3)
+                                    .Padding(8)
+                                    .AlignMiddle()
+                                    .DefaultTextStyle(x => x.Bold());
+
+                            IContainer CellStyle(IContainer container) =>
+                                container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten2)
+                                    .Padding(8);
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("Table", table =>
+            {
+                table.Child("THead", thead =>
+                {
+                    thead.Child("TR", row =>
+                    {
+                        row.Child("TH", th => th.Attribute("Table", "Scope", "Column").Child("P"));
+                        row.Child("TH", th => th.Attribute("Table", "Scope", "Column").Child("P"));
+                        row.Child("TH", th => th.Attribute("Table", "Scope", "Column").Child("P"));
+                        row.Child("TH", th => th.Attribute("Table", "Scope", "Column").Child("P"));
+                    });
+                });
+                
+                table.Child("TBody", tbody =>
+                {
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                    });
+
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                    });
+                });
+            });
+        });
+    }
+}

+ 102 - 0
Source/QuestPDF.ConformanceTests/Table/TableWithoutHeadersTests.cs

@@ -0,0 +1,102 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests.Table;
+
+internal class TableWithoutHeadersTests : ConformanceTestBase
+{
+    protected override Document GetDocumentUnderTest()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .Border(1)
+                        .BorderColor(Colors.Grey.Darken1)
+                        .SemanticTable()
+                        .Table(table =>
+                        {
+                            table.ColumnsDefinition(columns =>
+                            {
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                                columns.RelativeColumn();
+                            });
+
+                            // Row 1
+                            table.Cell().Element(CellStyle).Text("11");
+                            table.Cell().Element(CellStyle).Text("12");
+                            table.Cell().Element(CellStyle).Text("13");
+
+                            // Row 2
+                            table.Cell().Element(CellStyle).Text("21");
+                            table.Cell().Element(CellStyle).Text("22");
+                            table.Cell().Element(CellStyle).Text("23");
+
+                            // Row 3
+                            table.Cell().Element(CellStyle).Text("31");
+                            table.Cell().Element(CellStyle).Text("32");
+                            table.Cell().Element(CellStyle).Text("33");
+
+                            // Row 4
+                            table.Cell().Element(CellStyle).Text("41");
+                            table.Cell().Element(CellStyle).Text("42");
+                            table.Cell().Element(CellStyle).Text("43");
+                            
+                            IContainer CellStyle(IContainer container) =>
+                                container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten2)
+                                    .Padding(8);
+                        });
+                });
+            });
+    }
+
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("Table", table =>
+            {
+                table.Child("TBody", tbody =>
+                {
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TD", th => th.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                    });
+
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TD", th => th.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                    });
+
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TD", th => th.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                    });
+
+                    tbody.Child("TR", row =>
+                    {
+                        row.Child("TD", th => th.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                        row.Child("TD", td => td.Child("P"));
+                    });
+                });
+            });
+        });
+    }
+}

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

@@ -0,0 +1,164 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class TableOfContentsTests : ConformanceTestBase
+{
+    protected override 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);
+                        });
+                });
+            });
+        
+        static 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}"));
+                                    });
+                            }
+                        });
+                });
+        }
+        
+        static 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()
+                                        .SemanticIgnore()
+                                        .Width(200)
+                                        .Height(150)
+                                        .CornerRadius(10)
+                                        .Background(Placeholders.BackgroundColor());
+                                }
+                            });
+                        
+                        if (i < 10)
+                            column.Item().PageBreak();
+                    }
+                });
+        }
+    }
+    
+    protected override SemanticTreeNode? GetExpectedSemanticTree()
+    {
+        return ExpectedSemanticTree.DocumentRoot(root =>
+        {
+            root.Child("H1", h1 => h1.Alt("Conformance Test:\nTable of Contents"));
+
+            // Table of Contents Section
+            root.Child("Sect", sect =>
+            {
+                sect.Child("P");
+
+                sect.Child("TOC", toc =>
+                {
+                    foreach (var i in Enumerable.Range(1, 10))
+                    {
+                        toc.Child("TOCI", toci =>
+                        {
+                            toci.Child("Link", link =>
+                            {
+                                link.Alt($"Link to section {i}");
+                                link.Child("P"); // Number
+                                link.Child("P"); // Label
+                                link.Child("P"); // Page number
+                            });
+                        });
+                    }
+                });
+            });
+
+            // Content Sections
+            foreach (var i in Enumerable.Range(1, 10))
+            {
+                root.Child("Sect", sect =>
+                {
+                    sect.Child("H2", h2 => h2.Alt($"Section {i}"));
+                    sect.Child("P");
+                });
+            }
+        });
+    }
+}

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

@@ -0,0 +1,78 @@
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+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()
+            .WithMetadata(GetMetadata())
+            .WithSettings(new DocumentSettings
+            {
+                PDFA_Conformance = PDFA_Conformance.PDFA_3A
+            })
+            .GeneratePdfAndShow();
+    }
+    
+    [Test, TestCaseSource(nameof(PDFA_ConformanceLevels))]
+    public void Test_PDFA(PDFA_Conformance conformance)
+    {
+        GetDocumentUnderTest()
+            .WithMetadata(GetMetadata())
+            .WithSettings(new DocumentSettings
+            {
+                PDFA_Conformance = conformance
+            })
+            .TestConformanceWithVeraPdf();
+    }
+    
+    [Test, TestCaseSource(nameof(PDFUA_ConformanceLevels))]
+    public void Test_PDFUA(PDFUA_Conformance conformance)
+    {
+        GetDocumentUnderTest()
+            .WithMetadata(GetMetadata())
+            .WithSettings(new DocumentSettings
+            {
+                PDFUA_Conformance = conformance
+            })
+            .TestConformanceWithVeraPdf();
+    }
+    
+    [Test]
+    public void TestSemanticMeaning()
+    {
+        var expectedSemanticTree = GetExpectedSemanticTree();
+            
+        GetDocumentUnderTest()
+            .WithSettings(new DocumentSettings
+            {
+                PDFUA_Conformance = PDFUA_Conformance.PDFUA_1
+            })
+            .TestSemanticTree(expectedSemanticTree);
+    }
+
+    private DocumentMetadata GetMetadata()
+    {
+        return new DocumentMetadata
+        {
+            Language = "en-US",
+            Title = "Conformance Test",
+            Subject = this.GetType().Name.Replace("Tests", string.Empty).PrettifyName()
+        };
+    }
+
+    protected abstract Document GetDocumentUnderTest();
+    
+    protected abstract SemanticTreeNode? GetExpectedSemanticTree();
+}

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

@@ -0,0 +1,48 @@
+using ImageMagick;
+
+namespace QuestPDF.ConformanceTests.TestEngine;
+
+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);
+    }
+}

+ 80 - 0
Source/QuestPDF.ConformanceTests/TestEngine/MustangConformanceTestRunner.cs

@@ -0,0 +1,80 @@
+using System.Diagnostics;
+using System.Text;
+using System.Xml.Linq;
+using QuestPDF.Fluent;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests.TestEngine;
+
+public static class MustangConformanceTestRunner
+{
+    public class ValidationResult
+    {
+        public bool IsDocumentValid => !FailedRules.Any();
+        public ICollection<string> FailedRules { get; set; } = [];
+        
+        public string GetErrorMessage()
+        {
+            var errorMessage = new StringBuilder();
+
+            foreach (var failedRule in FailedRules)
+            {
+                errorMessage.AppendLine($"🟥\tError");
+                errorMessage.AppendLine($"\t{failedRule}");
+                errorMessage.AppendLine();
+            }
+
+            return errorMessage.ToString();
+        }
+    }
+
+    public static void TestConformance(string filePath)
+    {
+        var result = RunMustang(filePath);
+
+        if (!result.IsDocumentValid)
+        {
+            Console.WriteLine(result.GetErrorMessage());
+            Assert.Fail();
+        }
+    }
+
+    private static ValidationResult RunMustang(string pdfFilePath)
+    {
+        if (!File.Exists(pdfFilePath))
+            throw new FileNotFoundException($"PDF file not found: {pdfFilePath}");
+
+        var mustangExecutablePath = Environment.GetEnvironmentVariable("MUSTANG_EXECUTABLE_PATH");
+        
+        if (string.IsNullOrEmpty(mustangExecutablePath))
+            throw new Exception("The location path of the Mustang executable is not set. Set the MUSTANG_EXECUTABLE_PATH environment variable to the path of the Mustang executable.");
+        
+        var arguments = $"-jar {mustangExecutablePath} --action validate --source {pdfFilePath}";
+
+        var process = new Process
+        {
+            StartInfo = new ProcessStartInfo
+            {
+                FileName = "java",
+                Arguments = arguments,
+                RedirectStandardOutput = true,
+                RedirectStandardError = true,
+                UseShellExecute = false,
+                CreateNoWindow = true
+            }
+        };
+
+        process.Start();
+        var output = process.StandardOutput.ReadToEnd();
+        process.WaitForExit();
+
+        return new ValidationResult()
+        {
+            FailedRules = XDocument
+                .Parse(output)
+                .Descendants("error")
+                .Select(x => x.Value)
+                .ToList()
+        };
+    }
+}

+ 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 is not (SkSemanticNodeSpecialId.BackgroundArtifact or SkSemanticNodeSpecialId.LayoutArtifact))
+            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;
+    }
+}

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

@@ -0,0 +1,156 @@
+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? actualRoot, SemanticTreeNode? expectedRoot)
+    {
+        if (expectedRoot == null && actualRoot == null)
+            return;
+
+        if (expectedRoot == null)
+        {
+            Assert.Fail($"Expected null but got node of type '{actualRoot?.Type}'");
+            return;
+        }
+
+        if (actualRoot == null)
+        {
+            Assert.Fail($"Expected node of type '{expectedRoot.Type}' but got null");
+            return;
+        }
+        
+        var currentPath = new Stack<string>();
+        
+        try
+        {
+            Compare(actualRoot, expectedRoot);
+        }
+        catch
+        {
+            var pathText = string.Join(" -> ", currentPath.Reverse());
+            Console.WriteLine("Problem location");
+            Console.WriteLine(pathText);
+
+            throw;
+        }        
+
+        void Compare(SemanticTreeNode actual, SemanticTreeNode expected)
+        {
+            if (!currentPath.Any())
+                currentPath.Push(actual.Type);
+            
+            if (expected.NodeId != 0)
+                Assert.That(actual.NodeId, Is.EqualTo(expected.NodeId), "NodeId mismatch");
+            
+            Assert.That(actual.Type, Is.EqualTo(expected.Type), "Type mismatch");
+            Assert.That(actual.Alt, Is.EqualTo(expected.Alt), "Alt mismatch");
+            Assert.That(actual.Lang, Is.EqualTo(expected.Lang), "Lang mismatch");
+
+            CompareAttributes();
+            CompareChildren();
+
+            void CompareChildren()
+            {
+                Assert.That(actual.Children.Count, Is.EqualTo(expected.Children.Count), "Children count mismatch");
+                
+                var hasMultipleChildren = actual.Children.Count > 1;
+            
+                foreach (var (actualChild, expectedChild) in actual.Children.Zip(expected.Children))
+                {
+                    var prefix = hasMultipleChildren ? $"{actual.Children.IndexOf(actualChild)}:" : "";
+                    currentPath.Push(prefix + actualChild.Type);
+                
+                    Compare(actualChild, expectedChild);
+                
+                    currentPath.Pop();
+                }
+            }
+            
+            void CompareAttributes()
+            {
+                Assert.That(actual.Attributes.Count, Is.EqualTo(expected.Attributes.Count), "Attribute count mismatch");
+
+                var actualList = actual.Attributes.OrderBy(a => a.Owner).ThenBy(a => a.Name);
+                var expectedList = expected.Attributes.OrderBy(a => a.Owner).ThenBy(a => a.Name);
+
+                foreach (var (actualAttribute, expectedAttribute) in actualList.Zip(expectedList))
+                {
+                    Assert.That(actualAttribute.Owner, Is.EqualTo(expectedAttribute.Owner), "Attribute owner mismatch");
+                    Assert.That(actualAttribute.Name, Is.EqualTo(expectedAttribute.Name), "Attribute name mismatch");
+                    Assert.That(actualAttribute.Value, Is.EqualTo(expectedAttribute.Value), $"Attribute value mismatch for '{expectedAttribute.Owner}:{expectedAttribute.Name}");
+                }
+            }   
+        }
+    }
+}
+
+internal static class ExpectedSemanticTree
+{
+    public static SemanticTreeNode DocumentRoot(Action<SemanticTreeNode> configuration)
+    {
+        var root = new SemanticTreeNode
+        {
+            Type = "Document"
+        };
+        
+        configuration(root);
+        return root;
+    }
+
+    public static void Child(this SemanticTreeNode parent, string type, Action<SemanticTreeNode>? configuration = null)
+    {
+        var child = new SemanticTreeNode
+        {
+            Type = type
+        };
+
+        configuration?.Invoke(child);
+        parent.Children.Add(child);
+    }
+    
+    public static SemanticTreeNode Id(this SemanticTreeNode node, int id)
+    {
+        node.NodeId = id;
+        return node;
+    }
+    
+    public static SemanticTreeNode Attribute(this SemanticTreeNode node, string owner, string name, object value)
+    {
+        var attribute = new SemanticTreeNode.Attribute
+        {
+            Owner = owner,
+            Name = name,
+            Value = value
+        };
+        
+        node.Attributes.Add(attribute);
+        return node;
+    }
+    
+    public static SemanticTreeNode Alt(this SemanticTreeNode node, string alt)
+    {
+        node.Alt = alt;
+        return node;
+    }
+    
+    public static SemanticTreeNode Lang(this SemanticTreeNode node, string lang)
+    {
+        node.Lang = lang;
+        return node;
+    }
+}

+ 132 - 0
Source/QuestPDF.ConformanceTests/TestEngine/VeraPdfConformanceTestRunner.cs

@@ -0,0 +1,132 @@
+using System.Diagnostics;
+using System.Text;
+using System.Text.Json;
+using QuestPDF.Fluent;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests.TestEngine;
+
+public static class VeraPdfConformanceTestRunner
+{
+    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 ErrorMessage { get; set; }
+            public string Context { get; set; }
+        }
+
+        public string GetErrorMessage()
+        {
+            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();
+                errorMessage.AppendLine($"\t{failedRule.ErrorMessage}");
+                errorMessage.AppendLine();
+                errorMessage.AppendLine($"\t{failedRule.Context}");
+                errorMessage.AppendLine();
+            }
+
+            return errorMessage.ToString();
+        }
+    }
+    
+    public static void TestConformanceWithVeraPdf(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);
+    }
+    
+    public static void TestConformance(string filePath)
+    {
+        var result = RunVeraPDF(filePath);
+
+        if (!result.IsDocumentValid)
+        {
+            Console.WriteLine(result.GetErrorMessage());
+            Assert.Fail();
+        }
+    }
+    
+    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())
+            {
+                foreach (var check in failedRule.GetProperty("checks").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(),
+                        ErrorMessage = check.GetProperty("errorMessage").GetString(),
+                        Context = check.GetProperty("context").GetString()
+                    });
+                }
+            }
+        }
+
+        return result;
+    }
+}

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

@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests
+{
+    public class TestsSetup
+    {
+        [ModuleInitializer]
+        public static void Setup()
+        {
+            QuestPDF.Settings.License = LicenseType.Community;
+            QuestPDF.Settings.UseEnvironmentFonts = false;
+        }
+    }
+}

+ 69 - 0
Source/QuestPDF.ConformanceTests/ZugferdTests.cs

@@ -0,0 +1,69 @@
+using QuestPDF.ConformanceTests.TestEngine;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ConformanceTests;
+
+internal class ZugferdTests
+{
+    [Test]
+    public void ZugferdValidation_WithMustang()
+    {
+        var guid = Guid.NewGuid();
+        var invoicePath = Path.Combine(Path.GetTempPath(), $"{guid}.pdf");
+
+        Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Margin(60);
+
+                    page.Content()
+                        .Text("Conformance Test: ZUGFeRD")
+                        .FontSize(24)
+                        .FontColor(Colors.Blue.Darken2)
+                        .Bold();
+                });
+            })
+            .WithMetadata(new DocumentMetadata
+            {
+                Title = "Conformance Test: ZUGFeRD",
+                Author = "SampleCompany",
+                Subject = "ZUGFeRD Test Document",
+                Language = "en-US"
+            })
+            .WithSettings(new DocumentSettings
+            {
+                PDFA_Conformance = PDFA_Conformance.PDFA_3B
+            })
+            .GeneratePdf(invoicePath);
+        
+        VeraPdfConformanceTestRunner.TestConformance(invoicePath);
+        
+        var zugferdInvoicePath = Path.Combine(Path.GetTempPath(), $"zugferd-{guid}.pdf");
+
+        var facturPath = Path.Combine("Resources", "zugferd-factur-x.xml");
+        var metadataPath = Path.Combine("Resources", "zugferd-xmp-metadata.xml");
+
+        DocumentOperation
+            .LoadFile(invoicePath)
+            .AddAttachment(new DocumentOperation.DocumentAttachment
+            {
+                Key = "factur-zugferd",
+                FilePath = facturPath,
+                AttachmentName = "factur-x.xml",
+                MimeType = "text/xml",
+                Description = "Factur-X Invoice",
+                Relationship = DocumentOperation.DocumentAttachmentRelationship.Source,
+                CreationDate = DateTime.UtcNow,
+                ModificationDate = DateTime.UtcNow
+            })
+            .ExtendMetadata(File.ReadAllText(metadataPath))
+            .Save(zugferdInvoicePath);
+        
+        VeraPdfConformanceTestRunner.TestConformance(zugferdInvoicePath);
+        MustangConformanceTestRunner.TestConformance(zugferdInvoicePath); 
+    }
+}

+ 83 - 0
Source/QuestPDF.DocumentationExamples/AccessibilityExamples.cs

@@ -0,0 +1,83 @@
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.DocumentationExamples;
+
+public class AccessibilityExamples
+{
+    [Test]
+    public void MinimalExample()
+    {
+        Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Size(PageSizes.A5);
+                    page.Margin(30);
+
+                    page.Header()
+                        .PaddingBottom(15)
+                        .SemanticHeader1()
+                        .Text("Accessibility Test Document")
+                        .FontColor(Colors.Blue.Darken3)
+                        .FontSize(24)
+                        .Bold();
+                    
+                    page.Content()
+                        .Column(column =>
+                        {
+                            column.Spacing(20);
+                            
+                            column.Item()
+                                .SemanticSection()
+                                .Column(column =>
+                                {
+                                    column.Item()
+                                        .PaddingBottom(10)
+                                        .SemanticHeader2()
+                                        .Text("Section with text content")
+                                        .FontColor(Colors.Blue.Darken1)
+                                        .FontSize(16);
+                                    
+                                    column.Item()
+                                        .Text(Placeholders.Paragraphs())
+                                        .FontSize(12)
+                                        .ParagraphSpacing(8);
+                                });
+                            
+                            column.Item()
+                                .PreventPageBreak()
+                                .SemanticSection()
+                                .Column(column =>
+                                {
+                                    column.Item()
+                                        .PaddingBottom(10)
+                                        .SemanticHeader2()
+                                        .Text("Section with image")
+                                        .FontColor(Colors.Blue.Darken1)
+                                        .FontSize(16);
+                                    
+                                    column.Item()
+                                        .Width(250)
+                                        .SemanticImage("Image showing a laptop")
+                                        .Image("Resources/product.jpg");
+                                });
+                        });
+                });
+            })
+            .WithMetadata(new DocumentMetadata
+            {
+                Language = "en-US",
+                Title = "Accessibility Test",
+                Subject = "This document shows how easy it is to create accessible PDF documents with QuestPDF"
+            })
+            .WithSettings(new DocumentSettings
+            {
+                PDFA_Conformance = PDFA_Conformance.PDFA_3A,
+                PDFUA_Conformance = PDFUA_Conformance.PDFUA_1
+            })
+            .GeneratePdf("accessibility-minimal-example.pdf");
+    }
+}

+ 2 - 2
Source/QuestPDF.DocumentationExamples/QuestPDF.DocumentationExamples.csproj

@@ -17,11 +17,11 @@
         </PackageReference>
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
         <PackageReference Include="NUnit" Version="4.4.0" />
-        <PackageReference Include="NUnit.Analyzers" Version="4.10.0">
+        <PackageReference Include="NUnit.Analyzers" Version="4.11.0">
           <PrivateAssets>all</PrivateAssets>
           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
         </PackageReference>
-        <PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
+        <PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
         <PackageReference Include="ScottPlot" Version="5.0.56" />
         <PackageReference Include="SkiaSharp" Version="3.119.1" />
         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" />

+ 709 - 0
Source/QuestPDF.DocumentationExamples/Resources/semantic-book-content.json

@@ -0,0 +1,709 @@
+[
+  {
+    "term": "Variable",
+    "description": "A storage location paired with an associated symbolic name, which contains some known or unknown quantity of information referred to as a value.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Data Types",
+    "thirdLevelCategory": "Primitives"
+  },
+  {
+    "term": "Function",
+    "description": "A block of reusable code that performs a specific task and can be called by name.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Control Flow",
+    "thirdLevelCategory": "Subroutines"
+  },
+  {
+    "term": "If-Else Statement",
+    "description": "A conditional statement that executes a block of code if a specified condition is true, and another block if it is false.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Control Flow",
+    "thirdLevelCategory": "Conditionals"
+  },
+  {
+    "term": "For Loop",
+    "description": "A control flow statement for specifying iteration, which allows code to be executed repeatedly.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Control Flow",
+    "thirdLevelCategory": "Loops"
+  },
+  {
+    "term": "Integer",
+    "description": "A data type that represents a whole number, without a fractional component.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Data Types",
+    "thirdLevelCategory": "Primitives"
+  },
+  {
+    "term": "String",
+    "description": "A data type representing a sequence of characters.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Data Types",
+    "thirdLevelCategory": "Primitives"
+  },
+  {
+    "term": "Boolean",
+    "description": "A data type with only two possible values: true or false.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Data Types",
+    "thirdLevelCategory": "Primitives"
+  },
+  {
+    "term": "Array",
+    "description": "A data structure consisting of a collection of elements, each identified by at least one array index or key.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Data Structures",
+    "thirdLevelCategory": "Linear"
+  },
+  {
+    "term": "Object",
+    "description": "A data structure that contains data in the form of key-value pairs.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Data Types",
+    "thirdLevelCategory": "Composite"
+  },
+  {
+    "term": "Null",
+    "description": "A special value representing the intentional absence of any object value.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Data Types",
+    "thirdLevelCategory": "Primitives"
+  },
+  {
+    "term": "Class",
+    "description": "A blueprint for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions).",
+    "firstLevelCategory": "Programming Paradigms",
+    "secondLevelCategory": "Object-Oriented",
+    "thirdLevelCategory": "Core Constructs"
+  },
+  {
+    "term": "Inheritance",
+    "description": "A mechanism where a new class derives properties and behavior from an existing class.",
+    "firstLevelCategory": "Programming Paradigms",
+    "secondLevelCategory": "Object-Oriented",
+    "thirdLevelCategory": "Principles"
+  },
+  {
+    "term": "Polymorphism",
+    "description": "The provision of a single interface to entities of different types, allowing objects to be treated as instances of their parent class.",
+    "firstLevelCategory": "Programming Paradigms",
+    "secondLevelCategory": "Object-Oriented",
+    "thirdLevelCategory": "Principles"
+  },
+  {
+    "term": "Encapsulation",
+    "description": "The bundling of data with the methods that operate on that data, or the restricting of direct access to some of an object's components.",
+    "firstLevelCategory": "Programming Paradigms",
+    "secondLevelCategory": "Object-Oriented",
+    "thirdLevelCategory": "Principles"
+  },
+  {
+    "term": "Abstraction",
+    "description": "The concept of hiding the complex reality while exposing only the necessary parts.",
+    "firstLevelCategory": "Programming Paradigms",
+    "secondLevelCategory": "Object-Oriented",
+    "thirdLevelCategory": "Principles"
+  },
+  {
+    "term": "Algorithm",
+    "description": "A finite sequence of well-defined, computer-implementable instructions to solve a class of problems.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Algorithms",
+    "thirdLevelCategory": "Fundamentals"
+  },
+  {
+    "term": "Linked List",
+    "description": "A linear collection of data elements whose order is not given by their physical placement in memory. Each element points to the next.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Data Structures",
+    "thirdLevelCategory": "Linear"
+  },
+  {
+    "term": "Stack",
+    "description": "A linear data structure that follows the Last-In, First-Out (LIFO) principle.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Data Structures",
+    "thirdLevelCategory": "Linear"
+  },
+  {
+    "term": "Queue",
+    "description": "A linear data structure that follows the First-In, First-Out (FIFO) principle.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Data Structures",
+    "thirdLevelCategory": "Linear"
+  },
+  {
+    "term": "Tree",
+    "description": "A hierarchical data structure with a root value and subtrees of children with a parent node, represented as a set of linked nodes.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Data Structures",
+    "thirdLevelCategory": "Non-Linear"
+  },
+  {
+    "term": "Graph",
+    "description": "A data structure consisting of a set of vertices (or nodes) and a set of edges that connect these vertices.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Data Structures",
+    "thirdLevelCategory": "Non-Linear"
+  },
+  {
+    "term": "Hash Table",
+    "description": "A data structure that implements an associative array abstract data type, a structure that can map keys to values.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Data Structures",
+    "thirdLevelCategory": "Key-Value"
+  },
+  {
+    "term": "Binary Search",
+    "description": "A search algorithm that finds the position of a target value within a sorted array.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Algorithms",
+    "thirdLevelCategory": "Searching"
+  },
+  {
+    "term": "Bubble Sort",
+    "description": "A simple sorting algorithm that repeatedly steps through the list, compares adjacent elements and swaps them if they are in the wrong order.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Algorithms",
+    "thirdLevelCategory": "Sorting"
+  },
+  {
+    "term": "Merge Sort",
+    "description": "An efficient, comparison-based, divide and conquer sorting algorithm.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Algorithms",
+    "thirdLevelCategory": "Sorting"
+  },
+  {
+    "term": "Big O Notation",
+    "description": "A mathematical notation that describes the limiting behavior of a function when the argument tends towards a particular value or infinity, used to classify algorithms according to their running time or space requirements.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Complexity",
+    "thirdLevelCategory": "Asymptotic Analysis"
+  },
+  {
+    "term": "Recursion",
+    "description": "A method of solving a problem where the solution depends on solutions to smaller instances of the same problem.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Control Flow",
+    "thirdLevelCategory": "Subroutines"
+  },
+  {
+    "term": "API (Application Programming Interface)",
+    "description": "A set of rules and protocols for building and interacting with software applications.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Architecture",
+    "thirdLevelCategory": "Integration"
+  },
+  {
+    "term": "REST (Representational State Transfer)",
+    "description": "An architectural style for designing networked applications, often used for creating web services.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Architecture",
+    "thirdLevelCategory": "Integration"
+  },
+  {
+    "term": "HTTP (Hypertext Transfer Protocol)",
+    "description": "The foundation of data communication for the World Wide Web.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Protocols",
+    "thirdLevelCategory": "Communication"
+  },
+  {
+    "term": "JSON (JavaScript Object Notation)",
+    "description": "A lightweight data-interchange format that is easy for humans to read and write and easy for machines to parse and generate.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Data Formats",
+    "thirdLevelCategory": "Serialization"
+  },
+  {
+    "term": "HTML (Hypertext Markup Language)",
+    "description": "The standard markup language for documents designed to be displayed in a web browser.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Frontend",
+    "thirdLevelCategory": "Markup"
+  },
+  {
+    "term": "CSS (Cascading Style Sheets)",
+    "description": "A style sheet language used for describing the presentation of a document written in a markup language like HTML.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Frontend",
+    "thirdLevelCategory": "Styling"
+  },
+  {
+    "term": "JavaScript",
+    "description": "A high-level, interpreted programming language that conforms to the ECMAScript specification, primarily used for web development.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Frontend",
+    "thirdLevelCategory": "Scripting"
+  },
+  {
+    "term": "DOM (Document Object Model)",
+    "description": "A programming interface for HTML and XML documents. It represents the page so that programs can change the document structure, style, and content.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Frontend",
+    "thirdLevelCategory": "APIs"
+  },
+  {
+    "term": "Frontend",
+    "description": "The part of a website or application that the user interacts with directly; also known as client-side.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "General Concepts",
+    "thirdLevelCategory": "Client-Side"
+  },
+  {
+    "term": "Backend",
+    "description": "The server-side of a website or application, responsible for storing and organizing data and ensuring everything on the client-side works.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "General Concepts",
+    "thirdLevelCategory": "Server-Side"
+  },
+  {
+    "term": "Database",
+    "description": "An organized collection of data, generally stored and accessed electronically from a computer system.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Data Storage",
+    "thirdLevelCategory": "Systems"
+  },
+  {
+    "term": "SQL (Structured Query Language)",
+    "description": "A domain-specific language used in programming and designed for managing data held in a relational database management system.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Data Storage",
+    "thirdLevelCategory": "Query Languages"
+  },
+  {
+    "term": "NoSQL",
+    "description": "A database that provides a mechanism for storage and retrieval of data that is modeled in means other than the tabular relations used in relational databases.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Data Storage",
+    "thirdLevelCategory": "Systems"
+  },
+  {
+    "term": "Git",
+    "description": "A distributed version-control system for tracking changes in source code during software development.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Version Control",
+    "thirdLevelCategory": "Systems"
+  },
+  {
+    "term": "Commit",
+    "description": "An operation in version control which saves the current state of changes to the local repository.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Version Control",
+    "thirdLevelCategory": "Operations"
+  },
+  {
+    "term": "Branch",
+    "description": "A parallel version of a repository in version control, allowing for independent development without affecting the main line.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Version Control",
+    "thirdLevelCategory": "Concepts"
+  },
+  {
+    "term": "Merge",
+    "description": "An operation in version control that integrates changes from different branches into a single branch.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Version Control",
+    "thirdLevelCategory": "Operations"
+  },
+  {
+    "term": "Repository",
+    "description": "A central location in which data is stored and managed, commonly used for source code.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Version Control",
+    "thirdLevelCategory": "Concepts"
+  },
+  {
+    "term": "Compiler",
+    "description": "A special program that processes statements written in a particular programming language and turns them into machine language or 'code' that a computer's processor uses.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Language Processors",
+    "thirdLevelCategory": "Translation"
+  },
+  {
+    "term": "Interpreter",
+    "description": "A computer program that directly executes instructions written in a programming or scripting language, without requiring them to have been previously compiled into a machine language program.",
+    "firstLevelCategory": "Tools &Technologies",
+    "secondLevelCategory": "Language Processors",
+    "thirdLevelCategory": "Execution"
+  },
+  {
+    "term": "IDE (Integrated Development Environment)",
+    "description": "A software application that provides comprehensive facilities to computer programmers for software development.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Development Tools",
+    "thirdLevelCategory": "Editors"
+  },
+  {
+    "term": "Debugger",
+    "description": "A computer program used to test and find bugs (errors) in other programs.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Development Tools",
+    "thirdLevelCategory": "Testing"
+  },
+  {
+    "term": "Library",
+    "description": "A collection of non-volatile resources used by computer programs, often for software development.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Code Reusability",
+    "thirdLevelCategory": "Collections"
+  },
+  {
+    "term": "Framework",
+    "description": "A pre-written, structured body of code that provides a standard way to build and deploy applications.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Code Reusability",
+    "thirdLevelCategory": "Scaffolding"
+  },
+  {
+    "term": "Agile",
+    "description": "A set of practices for software development under which requirements and solutions evolve through the collaborative effort of self-organizing cross-functional teams.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "Methodologies",
+    "thirdLevelCategory": "Iterative"
+  },
+  {
+    "term": "Scrum",
+    "description": "An agile framework for managing knowledge work, with an emphasis on software development.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "Methodologies",
+    "thirdLevelCategory": "Frameworks"
+  },
+  {
+    "term": "Waterfall Model",
+    "description": "A sequential design process in which progress is seen as flowing steadily downwards (like a waterfall) through the phases of conception, initiation, analysis, design, construction, testing, deployment and maintenance.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "Methodologies",
+    "thirdLevelCategory": "Sequential"
+  },
+  {
+    "term": "Unit Testing",
+    "description": "A level of software testing where individual units or components of a software are tested.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "Testing",
+    "thirdLevelCategory": "Levels"
+  },
+  {
+    "term": "Integration Testing",
+    "description": "A level of software testing where individual units are combined and tested as a group.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "Testing",
+    "thirdLevelCategory": "Levels"
+  },
+  {
+    "term": "CI/CD (Continuous Integration/Continuous Deployment)",
+    "description": "The combined practices of continuous integration and either continuous delivery or continuous deployment, aimed at frequent and reliable software releases.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "DevOps",
+    "thirdLevelCategory": "Automation"
+  },
+  {
+    "term": "DevOps",
+    "description": "A set of practices that combines software development (Dev) and IT operations (Ops) to shorten the systems development life cycle and provide continuous delivery with high software quality.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "DevOps",
+    "thirdLevelCategory": "Culture"
+  },
+  {
+    "term": "Syntax",
+    "description": "The set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a language.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Language Fundamentals",
+    "thirdLevelCategory": "Grammar"
+  },
+  {
+    "term": "Semantics",
+    "description": "The meaning of a programming language's statements.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Language Fundamentals",
+    "thirdLevelCategory": "Meaning"
+  },
+  {
+    "term": "Functional Programming",
+    "description": "A programming paradigm where programs are constructed by applying and composing functions.",
+    "firstLevelCategory": "Programming Paradigms",
+    "secondLevelCategory": "Functional",
+    "thirdLevelCategory": "Core Concepts"
+  },
+  {
+    "term": "Pure Function",
+    "description": "A function whose return value is only determined by its input values, without observable side effects.",
+    "firstLevelCategory": "Programming Paradigms",
+    "secondLevelCategory": "Functional",
+    "thirdLevelCategory": "Principles"
+  },
+  {
+    "term": "Immutability",
+    "description": "A principle where an object's state cannot be modified after it is created.",
+    "firstLevelCategory": "Programming Paradigms",
+    "secondLevelCategory": "Functional",
+    "thirdLevelCategory": "Principles"
+  },
+  {
+    "term": "Higher-Order Function",
+    "description": "A function that either takes one or more functions as arguments or returns a function as its result.",
+    "firstLevelCategory": "Programming Paradigms",
+    "secondLevelCategory": "Functional",
+    "thirdLevelCategory": "Core Concepts"
+  },
+  {
+    "term": "Asynchronous",
+    "description": "Operations that allow a program to start a potentially long-running task and still be able to be responsive to other events while that task runs.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Execution Model",
+    "thirdLevelCategory": "Concurrency"
+  },
+  {
+    "term": "Promise",
+    "description": "An object representing the eventual completion or failure of an asynchronous operation.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Execution Model",
+    "thirdLevelCategory": "Concurrency"
+  },
+  {
+    "term": "Thread",
+    "description": "The smallest sequence of programmed instructions that can be managed independently by a scheduler.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Execution Model",
+    "thirdLevelCategory": "Parallelism"
+  },
+  {
+    "term": "Garbage Collection",
+    "description": "A form of automatic memory management that attempts to reclaim memory occupied by objects that are no longer in use by the program.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Memory Management",
+    "thirdLevelCategory": "Automatic"
+  },
+  {
+    "term": "Pointer",
+    "description": "A variable whose value is the memory address of another variable.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Memory Management",
+    "thirdLevelCategory": "Manual"
+  },
+  {
+    "term": "SDK (Software Development Kit)",
+    "description": "A collection of software development tools in one installable package.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Development Tools",
+    "thirdLevelCategory": "Tooling"
+  },
+  {
+    "term": "Node.js",
+    "description": "A JavaScript runtime built on Chrome's V8 JavaScript engine, used for building server-side applications.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Backend",
+    "thirdLevelCategory": "Runtimes"
+  },
+  {
+    "term": "React",
+    "description": "A JavaScript library for building user interfaces, particularly for single-page applications.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Frontend",
+    "thirdLevelCategory": "Libraries"
+  },
+  {
+    "term": "Angular",
+    "description": "A platform and framework for building single-page client applications using HTML and TypeScript.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Frontend",
+    "thirdLevelCategory": "Frameworks"
+  },
+  {
+    "term": "Vue.js",
+    "description": "A progressive framework for building user interfaces.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Frontend",
+    "thirdLevelCategory": "Frameworks"
+  },
+  {
+    "term": "Docker",
+    "description": "A set of platform-as-a-service products that use OS-level virtualization to deliver software in packages called containers.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "Deployment",
+    "thirdLevelCategory": "Containerization"
+  },
+  {
+    "term": "Kubernetes",
+    "description": "An open-source container-orchestration system for automating computer application deployment, scaling, and management.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "Deployment",
+    "thirdLevelCategory": "Orchestration"
+  },
+  {
+    "term": "Microservices",
+    "description": "An architectural style that structures an application as a collection of loosely coupled services.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Architecture",
+    "thirdLevelCategory": "Design Patterns"
+  },
+  {
+    "term": "Monolithic Architecture",
+    "description": "An architectural style where an application is built as a single, indivisible unit.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Architecture",
+    "thirdLevelCategory": "Design Patterns"
+  },
+  {
+    "term": "Cookie",
+    "description": "A small piece of data sent from a website and stored on the user's computer by the user's web browser while the user is browsing.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Protocols",
+    "thirdLevelCategory": "State Management"
+  },
+  {
+    "term": "Session",
+    "description": "A way to store information (in variables) to be used across multiple pages on a server.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Protocols",
+    "thirdLevelCategory": "State Management"
+  },
+  {
+    "term": "Cache",
+    "description": "A hardware or software component that stores data so that future requests for that data can be served faster.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Performance",
+    "thirdLevelCategory": "Optimization"
+  },
+  {
+    "term": "Time Complexity",
+    "description": "The computational complexity that describes the amount of computer time it takes to run an algorithm.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Complexity",
+    "thirdLevelCategory": "Performance"
+  },
+  {
+    "term": "Space Complexity",
+    "description": "The amount of memory space required to solve an instance of the computational problem as a function of the input size.",
+    "firstLevelCategory": "Data Structures & Algorithms",
+    "secondLevelCategory": "Complexity",
+    "thirdLevelCategory": "Memory"
+  },
+  {
+    "term": "Procedural Programming",
+    "description": "A programming paradigm based upon the concept of procedure calls, where statements are structured into procedures (or subroutines).",
+    "firstLevelCategory": "Programming Paradigms",
+    "secondLevelCategory": "Procedural",
+    "thirdLevelCategory": "Core Concepts"
+  },
+  {
+    "term": "Operator",
+    "description": "A symbol that tells the compiler or interpreter to perform specific mathematical, relational, or logical operations.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Language Fundamentals",
+    "thirdLevelCategory": "Expressions"
+  },
+  {
+    "term": "Expression",
+    "description": "A combination of one or more constants, variables, operators, and functions that the programming language interprets and computes to produce another value.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Language Fundamentals",
+    "thirdLevelCategory": "Expressions"
+  },
+  {
+    "term": "Deployment",
+    "description": "The process of making a software system available for use.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "Deployment",
+    "thirdLevelCategory": "Process"
+  },
+  {
+    "term": "Middleware",
+    "description": "Software that lies between an operating system and the applications running on it, enabling communication and data management.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Backend",
+    "thirdLevelCategory": "Architecture"
+  },
+  {
+    "term": "Endpoint",
+    "description": "One end of a communication channel. When an API interacts with another system, the touchpoints of this communication are considered endpoints.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Architecture",
+    "thirdLevelCategory": "Integration"
+  },
+  {
+    "term": "Type System",
+    "description": "A set of rules that assigns a property called type to the various constructs of a computer program.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Data Types",
+    "thirdLevelCategory": "Typing"
+  },
+  {
+    "term": "Static Typing",
+    "description": "Type checking is performed during compile-time.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Data Types",
+    "thirdLevelCategory": "Typing"
+  },
+  {
+    "term": "Dynamic Typing",
+    "description": "Type checking is performed at run-time.",
+    "firstLevelCategory": "Core Concepts",
+    "secondLevelCategory": "Data Types",
+    "thirdLevelCategory": "Typing"
+  },
+  {
+    "term": "Lambda Function",
+    "description": "An anonymous function that can be defined without being bound to an identifier.",
+    "firstLevelCategory": "Programming Paradigms",
+    "secondLevelCategory": "Functional",
+    "thirdLevelCategory": "Core Concepts"
+  },
+  {
+    "term": "Environment Variable",
+    "description": "A variable whose value is set outside the program, typically through functionality built into the operating system or microservice.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Configuration",
+    "thirdLevelCategory": "Runtime"
+  },
+  {
+    "term": "CLI (Command-Line Interface)",
+    "description": "A text-based user interface used to view and manage computer files.",
+    "firstLevelCategory": "Tools & Technologies",
+    "secondLevelCategory": "Interfaces",
+    "thirdLevelCategory": "Text-Based"
+  },
+  {
+    "term": "HTTPS",
+    "description": "An extension of the Hypertext Transfer Protocol for secure communication over a computer network.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Protocols",
+    "thirdLevelCategory": "Security"
+  },
+  {
+    "term": "Authentication",
+    "description": "The process of verifying the identity of a user or process.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Security",
+    "thirdLevelCategory": "Access Control"
+  },
+  {
+    "term": "Authorization",
+    "description": "The process of specifying access rights/privileges to resources.",
+    "firstLevelCategory": "Web Development",
+    "secondLevelCategory": "Security",
+    "thirdLevelCategory": "Access Control"
+  },
+  {
+    "term": "Regression Testing",
+    "description": "A type of software testing to confirm that a recent program or code change has not adversely affected existing features.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "Testing",
+    "thirdLevelCategory": "Types"
+  },
+  {
+    "term": "Code Review",
+    "description": "A software quality assurance activity in which one or several humans check a program mainly by viewing and reading parts of its source code.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "Quality Assurance",
+    "thirdLevelCategory": "Practices"
+  },
+  {
+    "term": "Refactoring",
+    "description": "The process of restructuring existing computer code—changing the factoring—without changing its external behavior.",
+    "firstLevelCategory": "Software Development Lifecycle",
+    "secondLevelCategory": "Maintenance",
+    "thirdLevelCategory": "Code Improvement"
+  }
+]

+ 265 - 0
Source/QuestPDF.DocumentationExamples/SemanticExamples.cs

@@ -0,0 +1,265 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.DocumentationExamples;
+
+public class SemanticExamples
+{
+    [Test]
+    public void HeaderAndFooter()
+    {
+        Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.MinSize(new PageSize(0, 0));
+                    page.MaxSize(new PageSize(600, 250));
+                    page.DefaultTextStyle(x => x.FontSize(16));
+                    page.Margin(25);
+
+                    page.Content()
+                        .Border(1)
+                        .BorderColor(Colors.Grey.Lighten1)
+                        .SemanticTable()
+                        .Table(table =>
+                        {
+                            var pageSizes = new List<(string name, double width, double height)>()
+                            {
+                                ("Letter (ANSI A)", 8.5f, 11),
+                                ("Legal", 8.5f, 14),
+                                ("Ledger (ANSI B)", 11, 17),
+                                ("Tabloid (ANSI B)", 17, 11),
+                                ("ANSI C", 22, 17),
+                                ("ANSI D", 34, 22),
+                                ("ANSI E", 44, 34)
+                            };
+
+                            const int inchesToPoints = 72;
+
+                            IContainer DefaultCellStyle(IContainer container, string backgroundColor)
+                            {
+                                return container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten1)
+                                    .Background(backgroundColor)
+                                    .PaddingVertical(5)
+                                    .PaddingHorizontal(10)
+                                    .AlignCenter()
+                                    .AlignMiddle();
+                            }
+
+                            table.ColumnsDefinition(columns =>
+                            {
+                                columns.RelativeColumn();
+
+                                columns.ConstantColumn(80);
+                                columns.ConstantColumn(80);
+
+                                columns.ConstantColumn(80);
+                                columns.ConstantColumn(80);
+                            });
+
+                            table.Header(header =>
+                            {
+                                // please be sure to call the 'header' handler!
+
+                                header.Cell().RowSpan(2).Element(CellStyle).ExtendHorizontal().AlignLeft()
+                                    .Text("Document type").Bold();
+
+                                header.Cell().ColumnSpan(2).Element(CellStyle).Text("Inches").Bold();
+                                header.Cell().ColumnSpan(2).Element(CellStyle).Text("Points").Bold();
+
+                                header.Cell().Element(CellStyle).Text("Width");
+                                header.Cell().Element(CellStyle).Text("Height");
+
+                                header.Cell().Element(CellStyle).Text("Width");
+                                header.Cell().Element(CellStyle).Text("Height");
+
+                                // you can extend existing styles by creating additional methods
+                                IContainer CellStyle(IContainer container) =>
+                                    DefaultCellStyle(container, Colors.Grey.Lighten3);
+                            });
+
+                            foreach (var page in pageSizes)
+                            {
+                                table.Cell().Element(CellStyle).ExtendHorizontal().AlignLeft().Text(page.name);
+
+                                // inches
+                                table.Cell().Element(CellStyle).Text(page.width);
+                                table.Cell().Element(CellStyle).Text(page.height);
+
+                                // points
+                                table.Cell().Element(CellStyle).Text(page.width * inchesToPoints);
+                                table.Cell().Element(CellStyle).Text(page.height * inchesToPoints);
+
+                                IContainer CellStyle(IContainer container) =>
+                                    DefaultCellStyle(container, Colors.White).ShowOnce();
+                            }
+                        });
+                });
+            })
+            .GeneratePdf();
+    }
+    
+    
+    public class BookTermModel
+    {
+        public string Term { get; set; }
+        public string Description { get; set; }
+        public string FirstLevelCategory { get; set; }
+        public string SecondLevelCategory { get; set; }
+        public string ThirdLevelCategory { get; set; }
+    }
+    
+    [Test]
+    public async Task GenerateBook()
+    {
+        QuestPDF.Settings.EnableCaching = false;
+        QuestPDF.Settings.EnableDebugging = false;
+        
+        var serializerSettings = new JsonSerializerOptions
+        {
+            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+            WriteIndented = true
+        };
+        
+        var bookData = await File.ReadAllTextAsync("Resources/semantic-book-content.json");
+        var terms = JsonSerializer.Deserialize<ICollection<BookTermModel>>(bookData, serializerSettings);
+        var categories = terms
+            .GroupBy(x => x.FirstLevelCategory)
+            .Select(x => new
+            {
+                Category = x.Key,
+                Terms = x
+                    .GroupBy(y => y.SecondLevelCategory)
+                    .Select(y => new
+                    {
+                        Category = y.Key,
+                        Terms = y
+                            .GroupBy(z => z.ThirdLevelCategory)
+                            .Select(z => new
+                            {
+                                Category = z.Key,
+                                Terms = z.ToList()
+                            })
+                            .ToList()
+                    })
+                    .ToList()
+            })
+            .ToList();
+        
+        Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Size(PageSizes.A4);
+                    page.DefaultTextStyle(x => x.FontSize(20));
+                    page.Margin(50);
+                    page.PageColor(Colors.White);
+
+                    page.Header()
+                        .Text("Programming Terms")
+                        .Bold()
+                        .FontSize(36);
+                    
+                    page.Content()
+                        .PaddingVertical(24)
+                        .Column(column =>
+                        {
+                            foreach (var category1 in categories)
+                            {
+                                column.Item()
+                                    .SemanticSection()
+                                    .EnsureSpace(100)
+                                    .Column(column =>
+                                    {
+                                        column.Spacing(24);
+                                        
+                                        column.Item()
+                                            .PaddingBottom(8)
+                                            .SemanticHeader1()
+                                            .Text(category1.Category)
+                                            .FontSize(24)
+                                            .FontColor(Colors.Blue.Darken4)
+                                            .Bold();
+
+                                        foreach (var category2 in category1.Terms)
+                                        {
+                                            column.Item()
+                                                .SemanticSection()
+                                                .EnsureSpace(100)
+                                                .Column(column =>
+                                                {
+                                                    column.Spacing(8);
+                                                    
+                                                    column.Item()
+                                                        .PaddingBottom(8)
+                                                        .SemanticHeader2()
+                                                        .Text(category2.Category)
+                                                        .FontSize(20)
+                                                        .FontColor(Colors.Blue.Darken2)
+                                                        .Bold();
+
+                                                    foreach (var category3 in category2.Terms)
+                                                    {
+                                                        column.Item()
+                                                            .SemanticSection()
+                                                            .EnsureSpace(100)
+                                                            .Column(column =>
+                                                            {
+                                                                column.Spacing(8);
+                                                                
+                                                                column.Item()
+                                                                    .PaddingBottom(8)
+                                                                    .SemanticHeader3()
+                                                                    .Text(category3.Category)
+                                                                    .FontSize(16)
+                                                                    .FontColor(Colors.Blue.Medium)
+                                                                    .Bold();
+
+                                                                foreach (var term in category3.Terms)
+                                                                {
+                                                                    column.Item()
+                                                                        .SemanticParagraph()
+                                                                        .Text(text =>
+                                                                        {
+                                                                            text.Span(term.Term).Bold();
+                                                                            text.Span(" - ");
+                                                                            text.Span(term.Description);
+                                                                        });
+                                                                }
+                                                            });
+                                                    }
+                                                });
+                                        }
+                                    });
+                                
+                                column.Item().PageBreak();
+                            }
+                        });
+
+                    page.Footer()
+                        .AlignCenter()
+                        .Text(text =>
+                        {
+                            text.Span("Page ");
+                            text.CurrentPageNumber();
+                            text.Span(" of ");
+                            text.TotalPages();
+                        });
+                });
+            })
+            .WithMetadata(new DocumentMetadata()
+            {
+                Title = "Programming Terms",
+                Language = "en-US"
+            })
+            .GeneratePdf();
+    }
+}

+ 2 - 2
Source/QuestPDF.LayoutTests/QuestPDF.LayoutTests.csproj

@@ -12,11 +12,11 @@
     <ItemGroup>
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
         <PackageReference Include="NUnit" Version="4.4.0" />
-        <PackageReference Include="NUnit.Analyzers" Version="4.10.0">
+        <PackageReference Include="NUnit.Analyzers" Version="4.11.0">
           <PrivateAssets>all</PrivateAssets>
           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
         </PackageReference>
-        <PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
+        <PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
         <PackageReference Include="SkiaSharp" Version="3.119.1" />
         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" />
     </ItemGroup>

+ 1 - 6
Source/QuestPDF.LayoutTests/TestEngine/ContinuousBlock.cs

@@ -47,12 +47,7 @@ internal class ContinuousBlock : Element, IStateful
         
     private float HeightOffset { get; set; }
 
-    public void ResetState(bool hardReset = false)
-    {
-        if (hardReset)
-            HeightOffset = 0;
-    }
-        
+    public void ResetState(bool hardReset = false) => HeightOffset = 0;
     public object GetState() => HeightOffset;
     public void SetState(object state) => HeightOffset = (float) state;
     

+ 1 - 6
Source/QuestPDF.LayoutTests/TestEngine/SolidBlock.cs

@@ -36,12 +36,7 @@ internal class SolidBlock : Element, IStateful
         
     private bool IsRendered { get; set; }
 
-    public void ResetState(bool hardReset = false)
-    {
-        if (hardReset)
-            IsRendered = false;
-    }
-        
+    public void ResetState(bool hardReset = false) => IsRendered = false;
     public object GetState() => IsRendered;
     public void SetState(object state) => IsRendered = (bool) state;
     

+ 1 - 1
Source/QuestPDF.ReportSample/QuestPDF.ReportSample.csproj

@@ -10,7 +10,7 @@
     <ItemGroup>
         <PackageReference Include="BenchmarkDotNet" Version="0.15.4" />
         <PackageReference Include="nunit" Version="4.4.0" />
-        <PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
+        <PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
         <PackageReference Include="SkiaSharp" Version="3.119.1" />
         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" />

+ 1 - 1
Source/QuestPDF.UnitTests/ImageTests.cs

@@ -147,7 +147,7 @@ namespace QuestPDF.UnitTests
             var highDpiSize = GetDocumentSize(container => container.Image(photo).WithRasterDpi(144));
 
             var dpiSizeRatio = (highDpiSize / (float)lowDpiSize);
-            Assert.That(dpiSizeRatio, Is.GreaterThan(40));
+            Assert.That(dpiSizeRatio, Is.GreaterThan(35));
         }
         
         private static int GetDocumentSize(Action<IContainer> container)

+ 2 - 2
Source/QuestPDF.UnitTests/QuestPDF.UnitTests.csproj

@@ -9,11 +9,11 @@
 
     <ItemGroup>
         <PackageReference Include="nunit" Version="4.4.0" />
-        <PackageReference Include="NUnit.Analyzers" Version="4.10.0">
+        <PackageReference Include="NUnit.Analyzers" Version="4.11.0">
           <PrivateAssets>all</PrivateAssets>
           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
         </PackageReference>
-        <PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
+        <PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
         <PackageReference Include="SkiaSharp" Version="3.119.1" />
         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" />

+ 5 - 2
Source/QuestPDF.UnitTests/TestEngine/MockCanvas.cs

@@ -46,8 +46,11 @@ namespace QuestPDF.UnitTests.TestEngine
         public void ClipRectangle(SkRect clipArea) => throw new NotImplementedException();
         public void ClipRoundedRectangle(SkRoundedRect clipArea) => throw new NotImplementedException();
         
-        public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
-        public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
+        public void DrawHyperlink(Size size, string url, string? description) => throw new NotImplementedException();
+        public void DrawSectionLink(Size size, string sectionName, string? description) => throw new NotImplementedException();
         public void DrawSection(string sectionName) => throw new NotImplementedException();
+        
+        public void MarkCurrentContentAsArtifact(bool isArtifact) => throw new NotImplementedException();
+        public void SetSemanticNodeId(int nodeId) => throw new NotImplementedException();
     }
 }

+ 5 - 2
Source/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs

@@ -43,8 +43,11 @@ namespace QuestPDF.UnitTests.TestEngine
         public void ClipRectangle(SkRect clipArea) => throw new NotImplementedException();
         public void ClipRoundedRectangle(SkRoundedRect clipArea) => throw new NotImplementedException();
         
-        public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
-        public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
+        public void DrawHyperlink(Size size, string url, string? description) => throw new NotImplementedException();
+        public void DrawSectionLink(Size size, string sectionName, string? description) => throw new NotImplementedException();
         public void DrawSection(string sectionName) => throw new NotImplementedException();
+        
+        public void MarkCurrentContentAsArtifact(bool isArtifact) => throw new NotImplementedException();
+        public void SetSemanticNodeId(int nodeId) => throw new NotImplementedException();
     }
 }

+ 2 - 2
Source/QuestPDF.VisualTests/QuestPDF.VisualTests.csproj

@@ -15,11 +15,11 @@
         <PackageReference Include="coverlet.collector" Version="6.0.0"/>
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
         <PackageReference Include="NUnit" Version="4.4.0" />
-        <PackageReference Include="NUnit.Analyzers" Version="4.10.0">
+        <PackageReference Include="NUnit.Analyzers" Version="4.11.0">
           <PrivateAssets>all</PrivateAssets>
           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
         </PackageReference>
-        <PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
+        <PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
         <PackageReference Include="SkiaSharp" Version="3.119.1" />
         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" />
     </ItemGroup>

+ 2 - 2
Source/QuestPDF.ZUGFeRD/QuestPDF.ZUGFeRD.csproj

@@ -16,11 +16,11 @@
         </PackageReference>
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
         <PackageReference Include="NUnit" Version="4.4.0" />
-        <PackageReference Include="NUnit.Analyzers" Version="4.10.0">
+        <PackageReference Include="NUnit.Analyzers" Version="4.11.0">
           <PrivateAssets>all</PrivateAssets>
           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
         </PackageReference>
-        <PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
+        <PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
     </ItemGroup>
 
     <ItemGroup>

+ 6 - 0
Source/QuestPDF.sln

@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.DocumentationExamp
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.VisualTests", "QuestPDF.VisualTests\QuestPDF.VisualTests.csproj", "{D7EB8ACD-4F99-439C-898F-EBFF0AFE367E}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.ConformanceTests", "QuestPDF.ConformanceTests\QuestPDF.ConformanceTests.csproj", "{FB8C2DE3-5866-49F8-844C-9510F1A1BC72}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		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}.Release|Any CPU.ActiveCfg = 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
 EndGlobal

+ 5 - 0
Source/QuestPDF/Drawing/DocumentCanvases/CompanionDocumentCanvas.cs

@@ -86,6 +86,11 @@ namespace QuestPDF.Drawing.DocumentCanvases
         #endregion
         
         #region IDocumentCanvas
+
+        public void SetSemanticTree(SemanticTreeNode? semanticTree)
+        {
+            
+        }
         
         public void BeginDocument()
         {

+ 5 - 0
Source/QuestPDF/Drawing/DocumentCanvases/FreeDocumentCanvas.cs

@@ -7,6 +7,11 @@ internal sealed class FreeDocumentCanvas : IDocumentCanvas
 {
     private FreeDrawingCanvas DrawingCanvas { get; } = new();
         
+    public void SetSemanticTree(SemanticTreeNode? semanticTree)
+    {
+            
+    }
+    
     public void BeginDocument()
     {
             

+ 5 - 0
Source/QuestPDF/Drawing/DocumentCanvases/ImageDocumentCanvas.cs

@@ -44,6 +44,11 @@ namespace QuestPDF.Drawing.DocumentCanvases
         
         #region IDocumentCanvas
         
+        public void SetSemanticTree(SemanticTreeNode? semanticTree)
+        {
+            
+        }
+        
         public void BeginDocument()
         {
             

+ 83 - 17
Source/QuestPDF/Drawing/DocumentCanvases/PdfDocumentCanvas.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Diagnostics;
+using System.Linq;
 using QuestPDF.Drawing.DrawingCanvases;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Infrastructure;
@@ -9,26 +10,33 @@ namespace QuestPDF.Drawing.DocumentCanvases
 {
     internal sealed class PdfDocumentCanvas : IDocumentCanvas, IDisposable
     {
-        private SkDocument Document { get; }
+        private SkWriteStream WriteStream { get; }
+        private DocumentMetadata DocumentMetadata { get; }
+        private DocumentSettings DocumentSettings { get; }
+        private SkPdfTag? SemanticTag { get; set; }
+        
+        private SkDocument? Document { get; set; }
         private SkCanvas? CurrentPageCanvas { get; set; }
         private ProxyDrawingCanvas DrawingCanvas { get; } = new();
         
         public PdfDocumentCanvas(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings)
         {
-            Document = CreatePdf(stream, documentMetadata, documentSettings);
+            WriteStream = stream;
+            DocumentMetadata = documentMetadata;
+            DocumentSettings = documentSettings;
         }
 
-        private static SkDocument CreatePdf(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings)
+        private SkDocument CreatePdf()
         {
             // do not extract to another method, as it will cause the SkText objects
             // to be disposed before the SkPdfDocument is created
-            using var title = new SkText(documentMetadata.Title);
-            using var author = new SkText(documentMetadata.Author);
-            using var subject = new SkText(documentMetadata.Subject);
-            using var keywords = new SkText(documentMetadata.Keywords);
-            using var creator = new SkText(documentMetadata.Creator);
-            using var producer = new SkText(documentMetadata.Producer);
-            using var language = new SkText(documentMetadata.Language);
+            using var title = new SkText(DocumentMetadata.Title);
+            using var author = new SkText(DocumentMetadata.Author);
+            using var subject = new SkText(DocumentMetadata.Subject);
+            using var keywords = new SkText(DocumentMetadata.Keywords);
+            using var creator = new SkText(DocumentMetadata.Creator);
+            using var producer = new SkText(DocumentMetadata.Producer);
+            using var language = new SkText(DocumentMetadata.Language);
             
             var internalMetadata = new SkPdfDocumentMetadata
             {
@@ -40,23 +48,54 @@ namespace QuestPDF.Drawing.DocumentCanvases
                 Producer = producer,
                 Language = language,
                 
-                CreationDate = new SkDateTime(documentMetadata.CreationDate),
-                ModificationDate = new SkDateTime(documentMetadata.ModifiedDate),
+                CreationDate = new SkDateTime(DocumentMetadata.CreationDate),
+                ModificationDate = new SkDateTime(DocumentMetadata.ModifiedDate),
+                
+                PDFA_Conformance = GetPDFAConformanceLevel(DocumentSettings.PDFA_Conformance),
+                PDFUA_Conformance = GetPDFUAConformanceLevel(DocumentSettings.PDFUA_Conformance),
                 
-                RasterDPI = documentSettings.ImageRasterDpi,
-                SupportPDFA = documentSettings.PdfA,
-                CompressDocument = documentSettings.CompressDocument
+                RasterDPI = DocumentSettings.ImageRasterDpi,
+                CompressDocument = DocumentSettings.CompressDocument,
+                
+                SemanticNodeRoot = SemanticTag?.Instance ?? IntPtr.Zero
             };
             
             try
             {
-                return SkPdfDocument.Create(stream, internalMetadata);
+                return SkPdfDocument.Create(WriteStream, internalMetadata);
             }
             catch (TypeInitializationException exception)
             {
                 throw new InitializationException("PDF", exception);
             }
         }
+
+        static Skia.PDFA_Conformance GetPDFAConformanceLevel(Infrastructure.PDFA_Conformance conformanceLevel)
+        {
+            return conformanceLevel switch
+            {
+                Infrastructure.PDFA_Conformance.None => Skia.PDFA_Conformance.None,
+                Infrastructure.PDFA_Conformance.PDFA_1A => Skia.PDFA_Conformance.PDFA_1A,
+                Infrastructure.PDFA_Conformance.PDFA_1B => Skia.PDFA_Conformance.PDFA_1B,
+                Infrastructure.PDFA_Conformance.PDFA_2A => Skia.PDFA_Conformance.PDFA_2A,
+                Infrastructure.PDFA_Conformance.PDFA_2B => Skia.PDFA_Conformance.PDFA_2B,
+                Infrastructure.PDFA_Conformance.PDFA_2U => Skia.PDFA_Conformance.PDFA_2U,
+                Infrastructure.PDFA_Conformance.PDFA_3A => Skia.PDFA_Conformance.PDFA_3A,
+                Infrastructure.PDFA_Conformance.PDFA_3B => Skia.PDFA_Conformance.PDFA_3B,
+                Infrastructure.PDFA_Conformance.PDFA_3U => Skia.PDFA_Conformance.PDFA_3U,
+                _ => throw new ArgumentOutOfRangeException(nameof(conformanceLevel), conformanceLevel, "Unsupported PDF/A conformance level")
+            };
+        }
+        
+        static Skia.PDFUA_Conformance GetPDFUAConformanceLevel(Infrastructure.PDFUA_Conformance conformanceLevel)
+        {
+            return conformanceLevel switch
+            {
+                Infrastructure.PDFUA_Conformance.None => Skia.PDFUA_Conformance.None,
+                Infrastructure.PDFUA_Conformance.PDFUA_1 => Skia.PDFUA_Conformance.PDFUA_1,
+                _ => throw new ArgumentOutOfRangeException(nameof(conformanceLevel), conformanceLevel, "Unsupported PDF/UA conformance level")
+            };
+        }
         
         #region IDisposable
         
@@ -71,6 +110,9 @@ namespace QuestPDF.Drawing.DocumentCanvases
             Document?.Dispose();
             CurrentPageCanvas?.Dispose();
             DrawingCanvas?.Dispose();
+            SemanticTag?.Dispose();
+            
+            // don't dispose WriteStream - its lifetime is managed externally
             
             GC.SuppressFinalize(this);
         }
@@ -79,9 +121,33 @@ namespace QuestPDF.Drawing.DocumentCanvases
         
         #region IDocumentCanvas
         
-        public void BeginDocument()
+        public void SetSemanticTree(SemanticTreeNode? semanticTree)
         {
+            if (semanticTree == null)
+            {
+                SemanticTag?.Dispose();
+                SemanticTag = null;
+                return;
+            }
             
+            SemanticTag = Convert(semanticTree);
+            
+            static SkPdfTag Convert(SemanticTreeNode node)
+            {
+                var result = SkPdfTag.Create(node.NodeId, node.Type, node.Alt, node.Lang);
+                var children = node.Children.Select(Convert).ToArray();
+                result.SetChildren(children);
+                
+                foreach (var nodeAttribute in node.Attributes)
+                    result.AddAttribute(nodeAttribute.Owner, nodeAttribute.Name, nodeAttribute.Value);
+                
+                return result;
+            }
+        }
+        
+        public void BeginDocument()
+        {
+            Document ??= CreatePdf();
         }
 
         public void EndDocument()

+ 5 - 0
Source/QuestPDF/Drawing/DocumentCanvases/SvgDocumentCanvas.cs

@@ -40,6 +40,11 @@ namespace QuestPDF.Drawing.DocumentCanvases
         
         #region IDocumentCanvas
         
+        public void SetSemanticTree(SemanticTreeNode? semanticTree)
+        {
+            
+        }
+        
         public void BeginDocument()
         {
             

+ 5 - 0
Source/QuestPDF/Drawing/DocumentCanvases/XpsDocumentCanvas.cs

@@ -51,6 +51,11 @@ namespace QuestPDF.Drawing.DocumentCanvases
         
         #region IDocumentCanvas
         
+        public void SetSemanticTree(SemanticTreeNode? semanticTree)
+        {
+            
+        }
+        
         public void BeginDocument()
         {
             

+ 2 - 2
Source/QuestPDF/Drawing/DocumentContainer.cs

@@ -9,7 +9,7 @@ namespace QuestPDF.Drawing
 {
     internal sealed class DocumentContainer : IDocumentContainer
     {
-        internal List<IComponent> Pages { get; set; } = new List<IComponent>();
+        internal List<IComponent> Pages { get; set; } = [];
         
         internal Container Compose()
         {
@@ -37,7 +37,7 @@ namespace QuestPDF.Drawing
                             .SelectMany(x => new List<Action>()
                             {
                                 () => column.Item().PageBreak(),
-                                () => column.Item().Component(x)
+                                () => column.Item().SemanticTag("Part").Component(x)
                             })
                             .Skip(1)
                             .ToList()

+ 138 - 46
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Threading;
 using QuestPDF.Companion;
 using QuestPDF.Drawing.DocumentCanvases;
 using QuestPDF.Drawing.Exceptions;
@@ -12,6 +11,8 @@ using QuestPDF.Elements.Text.Items;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Skia;
+using PDFA_Conformance = QuestPDF.Infrastructure.PDFA_Conformance;
+using PDFUA_Conformance = QuestPDF.Infrastructure.PDFUA_Conformance;
 
 namespace QuestPDF.Drawing
 {
@@ -28,6 +29,7 @@ namespace QuestPDF.Drawing
             
             var metadata = document.GetMetadata();
             var settings = document.GetSettings();
+
             using var canvas = new PdfDocumentCanvas(stream, metadata, settings);
             RenderDocument(canvas, document, settings);
         }
@@ -100,24 +102,17 @@ 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)
         {
-            canvas.BeginDocument();
-        
             if (document is MergedDocument mergedDocument)
+            {
                 RenderMergedDocument(canvas, mergedDocument, settings);
-        
-            else
-                RenderSingleDocument(canvas, document, settings);
-        
-            canvas.EndDocument();
-        }
-
-        private static void RenderSingleDocument(IDocumentCanvas canvas, IDocument document, DocumentSettings settings)
-        {
+                return;
+            }
+            
+            var semanticTreeManager = CreateSemanticTreeManager(settings);
             var useOriginalImages = canvas is ImageDocumentCanvas;
-
-            var content = ConfigureContent(document, settings, useOriginalImages);
+            var content = ConfigureContent(document, settings, semanticTreeManager, useOriginalImages);
             
             if (canvas is CompanionDocumentCanvas)
                 content.VisitChildren(x => x.CreateProxy(y => new LayoutProxy(y)));
@@ -127,7 +122,12 @@ namespace QuestPDF.Drawing
                 var pageContext = new PageContext();
                 RenderPass(pageContext, new FreeDocumentCanvas(), content);
                 pageContext.ProceedToNextRenderingPhase();
+
+                canvas.ConfigureWithSemanticTree(semanticTreeManager);
+                
+                canvas.BeginDocument();
                 RenderPass(pageContext, canvas, content);
+                canvas.EndDocument();
             
                 if (canvas is CompanionDocumentCanvas companionCanvas)
                     companionCanvas.Hierarchy = content.ExtractHierarchy();
@@ -142,59 +142,77 @@ namespace QuestPDF.Drawing
         {
             var useOriginalImages = canvas is ImageDocumentCanvas;
             
+            var sharedPageContent = new PageContext();
+            var useSharedPageContext = document.PageNumberStrategy == MergedDocumentPageNumberStrategy.Continuous;
+
+            var semanticTreeManager = CreateSemanticTreeManager(settings);
+            
             var documentParts = Enumerable
                 .Range(0, document.Documents.Count)
                 .Select(index => new
                 {
                     DocumentId = index,
-                    Content = ConfigureContent(document.Documents[index], settings, useOriginalImages)
+                    Content = ConfigureContent(document.Documents[index], settings, semanticTreeManager, useOriginalImages),
+                    PageContext = useSharedPageContext ? sharedPageContent : new PageContext()
                 })
                 .ToList();
             
             try
             {
-                if (document.PageNumberStrategy == MergedDocumentPageNumberStrategy.Continuous)
+                foreach (var documentPart in documentParts)
+                    documentPart.PageContext.SetDocumentId(documentPart.DocumentId);
+                
+                foreach (var documentPart in documentParts)
                 {
-                    var documentPageContext = new PageContext();
+                    RenderPass(documentPart.PageContext, new FreeDocumentCanvas(), documentPart.Content);
+                    documentPart.PageContext.ProceedToNextRenderingPhase();
+                }
 
-                    foreach (var documentPart in documentParts)
-                    {
-                        documentPageContext.SetDocumentId(documentPart.DocumentId);
-                        RenderPass(documentPageContext, new FreeDocumentCanvas(), documentPart.Content);
-                    }
+                canvas.ConfigureWithSemanticTree(semanticTreeManager);
                 
-                    documentPageContext.ProceedToNextRenderingPhase();
+                canvas.BeginDocument();
 
-                    foreach (var documentPart in documentParts)
-                    {
-                        documentPageContext.SetDocumentId(documentPart.DocumentId);
-                        RenderPass(documentPageContext, canvas, documentPart.Content);
-                        documentPart.Content.ReleaseDisposableChildren();
-                    }
-                }
-                else
+                foreach (var documentPart in documentParts)
                 {
-                    foreach (var documentPart in documentParts)
-                    {
-                        var pageContext = new PageContext();
-                        pageContext.SetDocumentId(documentPart.DocumentId);
-                    
-                        RenderPass(pageContext, new FreeDocumentCanvas(), documentPart.Content);
-                        pageContext.ProceedToNextRenderingPhase();
-                        RenderPass(pageContext, canvas, documentPart.Content);
-                    
-                        documentPart.Content.ReleaseDisposableChildren();
-                    }
+                    RenderPass(documentPart.PageContext, canvas, documentPart.Content);
+                    documentPart.Content.ReleaseDisposableChildren();
                 }
+                
+                canvas.EndDocument();
             }
-            catch
+            finally
             {
                 documentParts.ForEach(x => x.Content.ReleaseDisposableChildren());
-                throw;
             }
         }
 
-        private static Container ConfigureContent(IDocument document, DocumentSettings settings, bool useOriginalImages)
+        private static SemanticTreeManager? CreateSemanticTreeManager(DocumentSettings settings)
+        {
+            return IsDocumentSemanticAware() ? new SemanticTreeManager() : null;
+
+            bool IsDocumentSemanticAware()
+            {
+                if (settings.PDFUA_Conformance is not PDFUA_Conformance.None)
+                    return true;
+                
+                if (settings.PDFA_Conformance is PDFA_Conformance.PDFA_1A or PDFA_Conformance.PDFA_2A or PDFA_Conformance.PDFA_3A)
+                    return true;
+
+                return false;
+            }
+        }
+
+        private static void ConfigureWithSemanticTree(this IDocumentCanvas canvas, SemanticTreeManager? semanticTreeManager)
+        {
+            if (semanticTreeManager == null) 
+                return;
+            
+            var semanticTree = semanticTreeManager.GetSemanticTree();
+            semanticTreeManager.Reset();
+            canvas.SetSemanticTree(semanticTree);
+        }
+
+        private static Container ConfigureContent(IDocument document, DocumentSettings settings, SemanticTreeManager? semanticTreeManager, bool useOriginalImages)
         {
             var container = new DocumentContainer();
             document.Compose(container);
@@ -208,6 +226,12 @@ namespace QuestPDF.Drawing
             if (Settings.EnableCaching)
                 content.ApplyCaching();
             
+            if (semanticTreeManager != null)
+            {
+                content.ApplySemanticParagraphs();
+                content.InjectSemanticTreeManager(semanticTreeManager);
+            }
+            
             return content;
         }
 
@@ -335,6 +359,24 @@ namespace QuestPDF.Drawing
             }
         }
 
+        internal static void InjectSemanticTreeManager(this Element content, SemanticTreeManager semanticTreeManager)
+        {
+            content.VisitChildren(x =>
+            {
+                if (x is ISemanticAware semanticAware)
+                {
+                    semanticAware.SemanticTreeManager = semanticTreeManager;
+                }
+                else if (x is TextBlock textBlock)
+                {
+                    foreach (var textBlockElement in textBlock.Items.OfType<TextBlockElement>())
+                    {
+                        textBlockElement.Element.InjectSemanticTreeManager(semanticTreeManager);
+                    }
+                }
+            });
+        }
+        
         internal static void InjectDependencies(this Element content, IPageContext pageContext, IDrawingCanvas canvas)
         {
             content.VisitChildren(x =>
@@ -512,5 +554,55 @@ namespace QuestPDF.Drawing
                     ApplyInheritedAndGlobalTexStyle(child, documentDefaultTextStyle);
             }
         }
+
+        internal static void ApplySemanticParagraphs(this Element root)
+        {
+            var isFooterContext = false;
+            
+            Traverse(root);
+            
+            void Traverse(Element element)
+            {
+                if (element is SemanticTag { TagType: "H" or "H1" or "H2" or "H3" or "H4" or "H5" or "H6" or "P" or "Lbl" })
+                {
+                    return;
+                }
+                else if (element is ArtifactTag)
+                {
+                    // ignore all Text elements that are marked as artifacts
+                }
+                else if (element is DebugPointer { Type: DebugPointerType.DocumentStructure, Label: nameof(DocumentStructureTypes.Footer) } debugPointer)
+                {
+                    isFooterContext = true;
+                    Traverse(debugPointer.Child);
+                    isFooterContext = false;
+                }
+                else if (element is ContainerElement container)
+                {
+                    if (container.Child is TextBlock textBlock)
+                    {
+                        var textBlockContainsPageNumber = textBlock.Items.Any(x => x is TextBlockPageNumber);
+                        
+                        if (isFooterContext && textBlockContainsPageNumber)
+                            return;
+                        
+                        container.CreateProxy(x => new SemanticTag
+                        {
+                            Child = x,
+                            TagType = "P"
+                        });
+                    }
+                    else
+                    {
+                        Traverse(container.Child);
+                    }
+                }
+                else
+                {
+                    foreach (var child in element.GetChildren())
+                        Traverse(child);
+                }
+            }
+        }
     }
 }

+ 7 - 2
Source/QuestPDF/Drawing/DrawingCanvases/FreeDrawingCanvas.cs

@@ -133,12 +133,12 @@ namespace QuestPDF.Drawing.DrawingCanvases
             
         }
         
-        public void DrawHyperlink(string url, Size size)
+        public void DrawHyperlink(Size size, string url, string? description)
         {
            
         }
 
-        public void DrawSectionLink(string sectionName, Size size)
+        public void DrawSectionLink(Size size, string sectionName, string? description)
         {
             
         }
@@ -147,5 +147,10 @@ namespace QuestPDF.Drawing.DrawingCanvases
         {
             
         }
+        
+        public void SetSemanticNodeId(int nodeId)
+        {
+            
+        }
     }
 }

+ 9 - 4
Source/QuestPDF/Drawing/DrawingCanvases/ProxyDrawingCanvas.cs

@@ -147,14 +147,14 @@ internal sealed class ProxyDrawingCanvas : IDrawingCanvas, IDisposable
         Target.ClipRoundedRectangle(clipArea);
     }
 
-    public void DrawHyperlink(string url, Size size)
+    public void DrawHyperlink(Size size, string url, string? description)
     {
-        Target.DrawHyperlink(url, size);
+        Target.DrawHyperlink(size, url, description);
     }
 
-    public void DrawSectionLink(string sectionName, Size size)
+    public void DrawSectionLink(Size size, string sectionName, string? description)
     {
-        Target.DrawSectionLink(sectionName, size);
+        Target.DrawSectionLink(size, sectionName, description);
     }
 
     public void DrawSection(string sectionName)
@@ -162,5 +162,10 @@ internal sealed class ProxyDrawingCanvas : IDrawingCanvas, IDisposable
         Target.DrawSection(sectionName);
     }
     
+    public void SetSemanticNodeId(int nodeId)
+    {
+        Target.SetSemanticNodeId(nodeId);
+    }
+    
     #endregion
 }

+ 22 - 4
Source/QuestPDF/Drawing/DrawingCanvases/SkiaDrawingCanvas.cs

@@ -216,20 +216,38 @@ namespace QuestPDF.Drawing.DrawingCanvases
             CurrentCanvas.ClipRoundedRectangle(clipArea);
         }
         
-        public void DrawHyperlink(string url, Size size)
+        public void DrawHyperlink(Size size, string url, string? description)
         {
-            CurrentCanvas.AnnotateUrl(size.Width, size.Height, url);
+            CurrentCanvas.AnnotateUrl(size.Width, size.Height, url, description);
         }
         
-        public void DrawSectionLink(string sectionName, Size size)
+        public void DrawSectionLink(Size size, string sectionName, string? description)
         {
-            CurrentCanvas.AnnotateDestinationLink(size.Width, size.Height, sectionName);
+            CurrentCanvas.AnnotateDestinationLink(size.Width, size.Height, sectionName, description);
         }
 
         public void DrawSection(string sectionName)
         {
             CurrentCanvas.AnnotateDestination(sectionName);
         }
+
+        private int ArtifactNestingDepth { get; set; } = 0;
+
+        public void MarkCurrentContentAsArtifact(bool isArtifact)
+        {
+            ArtifactNestingDepth += isArtifact ? 1 : -1;
+        }
+
+        public void SetSemanticNodeId(int nodeId)
+        {
+            var isInsideArtifact = ArtifactNestingDepth > 0;
+            var isArtifactNode = nodeId < 0;
+            
+            if (isInsideArtifact && !isArtifactNode)
+                return;
+            
+            CurrentCanvas.SetSemanticNodeId(nodeId);
+        }
         
         #endregion
     }

+ 1 - 1
Source/QuestPDF/Drawing/Proxy/LayoutProxy.cs

@@ -44,7 +44,7 @@ internal sealed class LayoutProxy : ElementProxy
             // Image or DynamicImage or SvgImage or DynamicSvgImage should be excluded
             // They rely on the AspectRation component to provide true intrinsic size
             
-            return Child is TextBlock or AspectRatio or Unconstrained;
+            return Child is TextBlock or AspectRatio or Unconstrained or SemanticTag or ArtifactTag;
         }
     }
 

+ 107 - 0
Source/QuestPDF/Drawing/SemanticTreeManager.cs

@@ -0,0 +1,107 @@
+using System.Collections.Generic;
+
+namespace QuestPDF.Drawing;
+
+internal class SemanticTreeNode
+{
+    public int NodeId { get; set; }
+    public string Type { get; set; } = "";
+    public string? Alt { get; set; }
+    public string? Lang { get; set; }
+    public IList<SemanticTreeNode> Children { get; } = [];
+    public ICollection<Attribute> Attributes { get; } = [];
+
+    public class Attribute
+    {
+        public string Owner { get; set; }
+        public string Name { get; set; }
+        public object Value { get; set; }
+    }
+}
+
+class SemanticTreeManager
+{
+    private int CurrentNodeId { get; set; }
+    private SemanticTreeNode? Root { get; set; }
+    private Stack<SemanticTreeNode> Stack { get; set; } = [];
+
+    public SemanticTreeManager()
+    {
+        PopulateWithTopLevelNode();
+    }
+
+    private void PopulateWithTopLevelNode()
+    {
+        AddNode(new SemanticTreeNode
+        {
+            NodeId = GetNextNodeId(),
+            Type = "Document"
+        });
+    }
+    
+    public int GetNextNodeId()
+    {
+        CurrentNodeId++;
+        return CurrentNodeId;
+    }
+    
+    public void AddNode(SemanticTreeNode node)
+    {
+        if (Root == null)
+        {
+            Root = node;
+            Stack.Push(node);
+            return;
+        }
+        
+        Stack.Peek()?.Children.Add(node);
+    }
+    
+    public void PushOnStack(SemanticTreeNode node)
+    {
+        Stack.Push(node);
+    }
+    
+    public void PopStack()
+    {
+        Stack.Pop();
+    }
+    
+    public SemanticTreeNode PeekStack()
+    {
+        return Stack.Peek();
+    }
+    
+    public void Reset()
+    {
+        CurrentNodeId = 0;
+        Root = null;
+        Stack.Clear();
+    }
+
+    public SemanticTreeNode? GetSemanticTree()
+    {
+        return Root;
+    }
+    
+    #region Artifacts
+    
+    private int ArtifactNestingLevel { get; set; } = 0;
+    
+    public void BeginArtifactContent()
+    {
+        ArtifactNestingLevel++;
+    }
+    
+    public void EndArtifactContent()
+    {
+        ArtifactNestingLevel--;
+    }
+    
+    public bool IsCurrentContentArtifact()
+    {
+        return ArtifactNestingLevel > 0;
+    }
+    
+    #endregion
+}

+ 26 - 0
Source/QuestPDF/Elements/ArtifactTag.cs

@@ -0,0 +1,26 @@
+using QuestPDF.Drawing;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements;
+
+internal class ArtifactTag : ContainerElement, ISemanticAware
+{
+    public SemanticTreeManager? SemanticTreeManager { get; set; }
+    
+    public int Id { get; set; }
+    
+    internal override void Draw(Size availableSpace)
+    {
+        if (SemanticTreeManager == null)
+        {
+            base.Draw(availableSpace);
+            return;       
+        }
+        
+        Canvas.SetSemanticNodeId(Id);
+        
+        SemanticTreeManager.BeginArtifactContent();
+        Child?.Draw(availableSpace);
+        SemanticTreeManager.EndArtifactContent();
+    }
+}

+ 12 - 3
Source/QuestPDF/Elements/Dynamic.cs

@@ -7,8 +7,10 @@ using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Elements
 {
-    internal sealed class DynamicHost : Element, IStateful, IContentDirectionAware
+    internal sealed class DynamicHost : Element, IStateful, IContentDirectionAware, ISemanticAware
     {
+        public SemanticTreeManager? SemanticTreeManager { get; set; }
+        
         private DynamicComponentProxy Child { get; }
         private object InitialComponentState { get; set; }
 
@@ -66,6 +68,7 @@ namespace QuestPDF.Elements
             {
                 PageContext = PageContext,
                 Canvas = Canvas,
+                SemanticTreeManager = SemanticTreeManager,
                 
                 TextStyle = TextStyle,
                 ContentDirection = ContentDirection,
@@ -115,7 +118,8 @@ namespace QuestPDF.Elements
     {
         internal IPageContext PageContext { get; set; }
         internal IDrawingCanvas Canvas { get; set; }
-
+        internal SemanticTreeManager? SemanticTreeManager { get; set; }
+        
         internal TextStyle TextStyle { get; set; }
         internal ContentDirection ContentDirection { get; set; }
 
@@ -169,9 +173,14 @@ namespace QuestPDF.Elements
             container.ApplyInheritedAndGlobalTexStyle(TextStyle);
             container.ApplyContentDirection(ContentDirection);
             container.ApplyDefaultImageConfiguration(ImageTargetDpi, ImageCompressionQuality, UseOriginalImage);
-            
             container.InjectDependencies(PageContext, Canvas);
             container.VisitChildren(x => (x as IStateful)?.ResetState());
+            
+            if (SemanticTreeManager != null)
+            {
+                container.InjectSemanticTreeManager(SemanticTreeManager);
+                container.ApplySemanticParagraphs();
+            }
 
             container.Size = container.Measure(Size.Max);
             

+ 2 - 1
Source/QuestPDF/Elements/Hyperlink.cs

@@ -8,6 +8,7 @@ namespace QuestPDF.Elements
     {
         public ContentDirection ContentDirection { get; set; }
         public string Url { get; set; } = "https://www.questpdf.com";
+        public string? Description { get; set; }
         
         internal override void Draw(Size availableSpace)
         {
@@ -21,7 +22,7 @@ namespace QuestPDF.Elements
                 : new Position(availableSpace.Width - targetSize.Width, 0);
 
             Canvas.Translate(horizontalOffset);
-            Canvas.DrawHyperlink(Url, availableSpace);
+            Canvas.DrawHyperlink(availableSpace, Url, Description);
             Canvas.Translate(horizontalOffset.Reverse());
             
             base.Draw(availableSpace);

+ 9 - 2
Source/QuestPDF/Elements/Lazy.cs

@@ -6,8 +6,10 @@ using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Elements;
 
-internal sealed class Lazy : ContainerElement, IContentDirectionAware, IStateful
+internal sealed class Lazy : ContainerElement, ISemanticAware, IContentDirectionAware, IStateful
 {
+    public SemanticTreeManager? SemanticTreeManager { get; set; }
+    
     public Action<IContainer> ContentSource { get; set; }
     public bool IsCacheable { get; set; }
 
@@ -59,9 +61,14 @@ internal sealed class Lazy : ContainerElement, IContentDirectionAware, IStateful
         container.ApplyInheritedAndGlobalTexStyle(TextStyle);
         container.ApplyContentDirection(ContentDirection);
         container.ApplyDefaultImageConfiguration(ImageTargetDpi.Value, ImageCompressionQuality.Value, UseOriginalImage);
-            
         container.InjectDependencies(PageContext, Canvas);
         container.VisitChildren(x => (x as IStateful)?.ResetState());
+
+        if (SemanticTreeManager != null)
+        {
+            container.InjectSemanticTreeManager(SemanticTreeManager);
+            container.ApplySemanticParagraphs();
+        }
     }
     
     #region IStateful

+ 1 - 0
Source/QuestPDF/Elements/Line.cs

@@ -75,6 +75,7 @@ namespace QuestPDF.Elements
                 : new Position(0, Thickness / 2);
             
             Canvas.Translate(offset);
+            Canvas.SetSemanticNodeId(SkSemanticNodeSpecialId.LayoutArtifact);
             Canvas.DrawLine(start, end, paint);
             Canvas.Translate(offset.Reverse());
             

+ 6 - 1
Source/QuestPDF/Elements/Page.cs

@@ -2,6 +2,7 @@ using System;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 namespace QuestPDF.Elements
 {
@@ -35,12 +36,15 @@ namespace QuestPDF.Elements
                 .DefaultTextStyle(DefaultTextStyle.DisableFontFeature(FontFeatures.StandardLigatures))
                 .Layers(layers =>
                 {
-                    layers.Layer().ZIndex(int.MinValue).Background(BackgroundColor);
+                    layers.Layer()
+                        .ZIndex(int.MinValue)
+                        .Background(BackgroundColor);
                     
                     layers
                         .Layer()
                         .Repeat()
                         .DebugPointer(DebugPointerType.DocumentStructure, DocumentStructureTypes.Background.ToString())
+                        .Artifact(SkSemanticNodeSpecialId.BackgroundArtifact)
                         .Element(Background);
                     
                     layers
@@ -80,6 +84,7 @@ namespace QuestPDF.Elements
                     layers
                         .Layer()
                         .Repeat()
+                        .Artifact(SkSemanticNodeSpecialId.PaginationWatermarkArtifact)
                         .DebugPointer(DebugPointerType.DocumentStructure, DocumentStructureTypes.Foreground.ToString())
                         .Element(Foreground);
                 });

+ 60 - 3
Source/QuestPDF/Elements/RepeatContent.cs

@@ -2,21 +2,63 @@ using QuestPDF.Drawing;
 using QuestPDF.Elements.Text;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 namespace QuestPDF.Elements;
 
-internal sealed class RepeatContent : ContainerElement
+internal sealed class RepeatContent : ContainerElement, IStateful, ISemanticAware
 {
+    public SemanticTreeManager? SemanticTreeManager { get; set; }
+    
+    public enum RepeatContextType
+    {
+        PageHeader,
+        PageFooter,
+        Other
+    }
+    
+    public RepeatContextType RepeatContext { get; set; } = RepeatContextType.Other;
+    
     internal override void Draw(Size availableSpace)
     {
         OptimizeContentCacheBehavior();
         
-        var childMeasurement = Child?.Measure(availableSpace);
+        var childMeasurement = Child.Measure(availableSpace);
+
+        if (SemanticTreeManager == null)
+        {
+            base.Draw(availableSpace);
+            ResetChildrenIfNecessary();
+            return;      
+        }
+        
+        if (IsFullyRendered)
+        {
+            var paginationNodeId = RepeatContext switch
+            {
+                RepeatContextType.PageHeader => SkSemanticNodeSpecialId.PaginationHeaderArtifact,
+                RepeatContextType.PageFooter => SkSemanticNodeSpecialId.PaginationFooterArtifact,
+                _ => SkSemanticNodeSpecialId.PaginationArtifact
+            };
+        
+            Canvas.SetSemanticNodeId(paginationNodeId);
+            SemanticTreeManager.BeginArtifactContent();
+        }
+        
         base.Draw(availableSpace);
+        
+        if (IsFullyRendered)
+            SemanticTreeManager.EndArtifactContent();
+
+        ResetChildrenIfNecessary();
 
-        if (childMeasurement?.Type == SpacePlanType.FullRender)
+        void ResetChildrenIfNecessary()
         {
+            if (childMeasurement.Type != SpacePlanType.FullRender) 
+                return;
+            
             Child.VisitChildren(x => (x as IStateful)?.ResetState(false));
+            IsFullyRendered = true;
         }
     }
     
@@ -56,4 +98,19 @@ internal sealed class RepeatContent : ContainerElement
     }
     
     #endregion
+    
+    #region IStateful
+        
+    private bool IsFullyRendered { get; set; }
+
+    public void ResetState(bool hardReset = false)
+    {
+        if (hardReset)
+            IsFullyRendered = false;
+    }
+    
+    public object GetState() => IsFullyRendered;
+    public void SetState(object state) => IsFullyRendered = (bool) state;
+    
+    #endregion
 }

+ 2 - 1
Source/QuestPDF/Elements/SectionLink.cs

@@ -7,6 +7,7 @@ namespace QuestPDF.Elements
     internal sealed class SectionLink : ContainerElement
     {
         public string SectionName { get; set; }
+        public string? Description { get; set; }
         
         internal override void Draw(Size availableSpace)
         {
@@ -16,7 +17,7 @@ namespace QuestPDF.Elements
                 return;
 
             var targetName = PageContext.GetDocumentLocationName(SectionName);
-            Canvas.DrawSectionLink(targetName, targetSize);
+            Canvas.DrawSectionLink(targetSize, targetName, Description);
             base.Draw(availableSpace);
         }
 

+ 112 - 0
Source/QuestPDF/Elements/SemanticTag.cs

@@ -0,0 +1,112 @@
+using System;
+using System.Text;
+using QuestPDF.Drawing;
+using QuestPDF.Elements.Text;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements;
+
+internal class SemanticTag : ContainerElement, ISemanticAware
+{
+    public SemanticTreeManager? SemanticTreeManager { get; set; }
+    public SemanticTreeNode? SemanticTreeNode { get; private set; }
+
+    public string TagType { get; set; }
+    public string? Alt { get; set; }
+    public string? Lang { get; set; }
+
+    internal override void Draw(Size availableSpace)
+    {
+        if (SemanticTreeManager == null || SemanticTreeManager.IsCurrentContentArtifact())
+        {
+            Child?.Draw(availableSpace);
+            return;       
+        }
+        
+        RegisterCurrentSemanticNode();
+        
+        SemanticTreeManager.PushOnStack(SemanticTreeNode);
+        Canvas.SetSemanticNodeId(SemanticTreeNode.NodeId);
+        Child?.Draw(availableSpace);
+        SemanticTreeManager.PopStack();
+    }
+
+    internal void RegisterCurrentSemanticNode()
+    {
+        if (SemanticTreeNode != null)
+            return;
+        
+        if (TagType is "H" or "H1" or "H2" or "H3" or "H4" or "H5" or "H6")
+            UpdateHeaderText();
+        
+        if (TagType is "Link")
+            UpdateInnerLink();
+        
+        var id = SemanticTreeManager.GetNextNodeId();
+            
+        SemanticTreeNode = new SemanticTreeNode
+        {
+            NodeId = id,
+            Type = TagType,
+            Alt = Alt,
+            Lang = Lang
+        };
+            
+        SemanticTreeManager.AddNode(SemanticTreeNode);
+    }
+
+    private void UpdateHeaderText()
+    {
+        if (!string.IsNullOrWhiteSpace(Alt))
+            return;
+        
+        var builder = new StringBuilder();
+        Traverse(builder, Child);
+        Alt = builder.ToString();
+        
+        static void Traverse(StringBuilder builder, Element element)
+        {
+            if (element is TextBlock textBlock)
+            {
+                if (builder.Length > 0)
+                    builder.Append(' ');
+                
+                builder.Append(textBlock.Text);
+            }
+            else if (element is ContainerElement container)
+            {
+                Traverse(builder, container.Child);
+            }
+            else
+            {
+                foreach (var child in element.GetChildren())
+                    Traverse(builder, child);
+            }
+        }
+    }
+    
+    private void UpdateInnerLink()
+    {
+        if (string.IsNullOrWhiteSpace(Alt))
+            return;
+        
+        var currentChild = Child;
+        
+        while (currentChild != null)
+        {
+            if (currentChild is Hyperlink hyperlink)
+            {
+                hyperlink.Description = Alt;
+                return;
+            }
+            
+            if (currentChild is SectionLink sectionLink)
+            {
+                sectionLink.Description = Alt;
+                return;
+            }
+            
+            currentChild = (currentChild as ContainerElement)?.Child;
+        }
+    }
+}

+ 12 - 0
Source/QuestPDF/Elements/StyledBox.cs

@@ -89,13 +89,18 @@ namespace QuestPDF.Elements
             {
                 // optimization: draw a simple rectangle with border
                 if (backgroundPaint != null)
+                {
+                    Canvas.SetSemanticNodeId(SkSemanticNodeSpecialId.BackgroundArtifact);
                     Canvas.DrawRectangle(Position.Zero, availableSpace, backgroundPaint);
+                }
                 
                 base.Draw(availableSpace);
                 
                 if (borderPaint != null)
                 {
                     borderPaint.SetStroke(BorderLeft);
+                    
+                    Canvas.SetSemanticNodeId(SkSemanticNodeSpecialId.LayoutArtifact);
                     Canvas.DrawRectangle(Position.Zero, availableSpace, borderPaint);
                 }
                 
@@ -118,6 +123,7 @@ namespace QuestPDF.Elements
                     Color = Shadow.Color
                 };
                 
+                Canvas.SetSemanticNodeId(SkSemanticNodeSpecialId.BackgroundArtifact);
                 Canvas.DrawShadow(shadowRect, canvasShadow);
             }
 
@@ -128,7 +134,10 @@ namespace QuestPDF.Elements
             }
 
             if (backgroundPaint != null)
+            {
+                Canvas.SetSemanticNodeId(SkSemanticNodeSpecialId.BackgroundArtifact);
                 Canvas.DrawRectangle(Position.Zero, availableSpace, backgroundPaint);
+            }
             
             base.Draw(availableSpace);
             
@@ -136,7 +145,10 @@ namespace QuestPDF.Elements
                 Canvas.Restore();
 
             if (borderPaint != null)
+            {
+                Canvas.SetSemanticNodeId(SkSemanticNodeSpecialId.LayoutArtifact);
                 Canvas.DrawComplexBorder(borderInnerRect, borderOuterRect, borderPaint);
+            }
         }
 
         private (Position start, Position end) GetLinearGradientPositions(Size availableSpace, float angle)

+ 203 - 1
Source/QuestPDF/Elements/Table/Table.cs

@@ -1,12 +1,13 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using QuestPDF.Drawing;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Elements.Table
 {
-    internal sealed class Table : Element, IStateful, IContentDirectionAware
+    internal sealed class Table : Element, IStateful, IContentDirectionAware, ISemanticAware
     {
         // configuration
         public List<TableColumnDefinition> Columns { get; set; } = new();
@@ -110,6 +111,7 @@ namespace QuestPDF.Elements.Table
         internal override void Draw(Size availableSpace)
         {
             Initialize();
+            RegisterSemanticTree();
             
             if (IsRendered)
                 return;
@@ -353,5 +355,205 @@ namespace QuestPDF.Elements.Table
         }
     
         #endregion
+        
+        #region Semantic
+
+        internal enum TablePartType
+        {
+            Header,
+            Body,
+            Footer
+        }
+        
+        internal bool EnableAutomatedSemanticTagging { get; set; }
+        private bool IsSemanticTaggingApplied { get; set; }
+        public SemanticTreeManager? SemanticTreeManager { get; set; } = new();
+
+        internal bool TableRequiresAdvancedHeaderTagging { get; set; }
+        internal TablePartType PartType { get; set; }
+        public List<TableCell> HeaderCells { get; set; } = []; 
+
+        private void RegisterSemanticTree()
+        {
+            if (SemanticTreeManager == null)
+                return;
+            
+            if (SemanticTreeManager.IsCurrentContentArtifact())
+                return;
+            
+            if (!EnableAutomatedSemanticTagging)
+                return;
+            
+            if (IsSemanticTaggingApplied)
+                return;
+            
+            IsSemanticTaggingApplied = true;
+            
+            foreach (var tableRow in Cells.GroupBy(x => x.Row))
+            {
+                var rowSemanticTreeNode = new SemanticTreeNode()
+                {
+                    NodeId = SemanticTreeManager.GetNextNodeId(), 
+                    Type = "TR"
+                };
+                
+                SemanticTreeManager.AddNode(rowSemanticTreeNode);
+                SemanticTreeManager.PushOnStack(rowSemanticTreeNode);
+                
+                foreach (var tableCell in tableRow.OrderBy(x => x.Column))
+                {
+                    tableCell.CreateProxy(x => new SemanticTag
+                    {
+                        SemanticTreeManager = SemanticTreeManager,
+                        Canvas = Canvas,
+                        
+                        TagType = "TD",
+                        Child = x
+                    });
+
+                    if (tableCell.Child is not SemanticTag semanticTag)
+                        continue;
+                    
+                    if (PartType is TablePartType.Header || tableCell.IsSemanticHorizontalHeader)
+                        semanticTag.TagType = "TH";
+                    
+                    semanticTag.RegisterCurrentSemanticNode();
+                    tableCell.SemanticNodeId = semanticTag.SemanticTreeNode!.NodeId;
+                    
+                    AssignCellAttributesForColumnAndRowSpans(tableCell, semanticTag);
+                }
+                
+                SemanticTreeManager.PopStack();
+            }
+
+            AssignCellAttributesForHeaderCellRoles();
+            
+            static void AssignCellAttributesForColumnAndRowSpans(TableCell tableCell, SemanticTag semanticTag)
+            {
+                if (tableCell.ColumnSpan > 1)
+                {
+                    semanticTag.SemanticTreeNode.Attributes.Add(new SemanticTreeNode.Attribute
+                    {
+                        Owner = "Table",
+                        Name = "ColSpan",
+                        Value = tableCell.ColumnSpan
+                    });
+                }
+
+                if (tableCell.RowSpan > 1)
+                {
+                    semanticTag.SemanticTreeNode.Attributes.Add(new SemanticTreeNode.Attribute
+                    {
+                        Owner = "Table",
+                        Name = "RowSpan",
+                        Value = tableCell.RowSpan
+                    });
+                }
+            }
+
+            void AssignCellAttributesForHeaderCellRoles()
+            {
+                if (PartType is TablePartType.Footer)
+                    return;
+
+                if (TableRequiresAdvancedHeaderTagging)
+                {
+                    AssignCellAttributesForHeaderCellRolesOfComplexTables();
+                }
+                else
+                {
+                    AssignCellAttributesForHeaderCellRolesOfSimpleTables();
+                }
+            }
+            
+            void AssignCellAttributesForHeaderCellRolesOfSimpleTables()
+            {
+                foreach (var tableCell in Cells)
+                {
+                    if (tableCell.Child is not SemanticTag semanticTag)
+                        continue;
+
+                    if (semanticTag.TagType != "TH") 
+                        continue;
+                    
+                    var scopeValue = (PartType is TablePartType.Header, tableCell.IsSemanticHorizontalHeader) switch
+                    {
+                        (true, true) => "Both",
+                        (true, false) => "Column",
+                        (false, true) => "Row",
+                        (false, false) => null
+                    };
+
+                    if (scopeValue == null)
+                        continue;
+                    
+                    semanticTag.SemanticTreeNode.Attributes.Add(new SemanticTreeNode.Attribute
+                    {
+                        Owner = "Table", 
+                        Name = "Scope", 
+                        Value = scopeValue
+                    });
+                }
+            }
+            
+            void AssignCellAttributesForHeaderCellRolesOfComplexTables()
+            {
+                var semanticHorizontalHeaders = Cells
+                    .Where(x => x.IsSemanticHorizontalHeader)
+                    .ToList();
+                
+                foreach (var tableCell in Cells)
+                {
+                    if (tableCell.Child is not SemanticTag semanticTag)
+                        continue;
+                    
+                    var relatedHeaders = GetRelatedHeadersFor(tableCell).ToArray();
+                    
+                    if (!relatedHeaders.Any())
+                        continue;
+                    
+                    semanticTag.SemanticTreeNode!.Attributes.Add(new SemanticTreeNode.Attribute
+                    {
+                        Owner = "Table",
+                        Name = "Headers",
+                        Value = relatedHeaders
+                    });
+                }
+
+                IEnumerable<int> GetRelatedHeadersFor(TableCell cell)
+                {
+                    var isHeader = PartType == TablePartType.Header;
+                    
+                    var headerCells = (isHeader ? Cells : HeaderCells).AsEnumerable();
+                    
+                    if (isHeader)
+                        headerCells = headerCells.Where(x => x.Row < cell.Row);
+                    
+                    var relatedVerticalHeaders = headerCells
+                        .Where(x => x.Column < cell.Column + cell.ColumnSpan && cell.Column < x.Column + x.ColumnSpan)
+                        .Select(x => x.SemanticNodeId);
+                    
+                    if (isHeader)
+                        return relatedVerticalHeaders; 
+                    
+                    var relatedHorizontalHeaders = semanticHorizontalHeaders
+                        .Where(x => x.Column < cell.Column)
+                        .Where(x => x.Row < cell.Row + cell.RowSpan && cell.Row < x.Row + x.RowSpan)
+                        .Select(x => x.SemanticNodeId);
+                        
+                    return relatedVerticalHeaders.Concat(relatedHorizontalHeaders);
+                }
+            }
+        }
+        
+        public static bool DoesTableBodyRequireExtendedHeaderTagging(ICollection<TableCell> headerCells, ICollection<TableCell> bodyCells)
+        {
+            return ContainsSpanningCells(headerCells) || ContainsSpanningCells(bodyCells);
+                
+            static bool ContainsSpanningCells(IEnumerable<TableCell> cells) =>
+                cells.Any(x => x.RowSpan > 1 || x.ColumnSpan > 1);
+        }
+        
+        #endregion
     }
 }

+ 3 - 0
Source/QuestPDF/Elements/Table/TableCell.cs

@@ -12,6 +12,9 @@ namespace QuestPDF.Elements.Table
         
         public int ZIndex { get; set; }
         
+        public bool IsSemanticHorizontalHeader { get; set; }
+        public int SemanticNodeId { get; set; }
+        
         public bool IsRendered { get; set; }
     }
 }

+ 2 - 2
Source/QuestPDF/Elements/Text/TextBlock.cs

@@ -240,7 +240,7 @@ namespace QuestPDF.Elements.Text
                             continue;
                         
                         Canvas.Translate(offset);
-                        Canvas.DrawHyperlink(hyperlink.Url, new Size(position.Width, position.Height));
+                        Canvas.DrawHyperlink(new Size(position.Width, position.Height), hyperlink.Url, hyperlink.Text);
                         Canvas.Translate(offset.Reverse());
                     }
                 }
@@ -261,7 +261,7 @@ namespace QuestPDF.Elements.Text
                             continue;
                         
                         Canvas.Translate(offset);
-                        Canvas.DrawSectionLink(targetName, new Size(position.Width, position.Height));
+                        Canvas.DrawSectionLink(new Size(position.Width, position.Height), targetName, sectionLink.Text);
                         Canvas.Translate(offset.Reverse());
                     }
                 }

+ 8 - 2
Source/QuestPDF/Fluent/DecorationExtensions.cs

@@ -28,7 +28,10 @@ namespace QuestPDF.Fluent
 
             var container = new Container();
             Decoration.Before = container;
-            return container.DebugPointer(DebugPointerType.ElementStructure, "Before").Repeat();
+            
+            return container
+                .DebugPointer(DebugPointerType.ElementStructure, "Before")
+                .RepeatAsHeader();
         }
         
         /// <summary>
@@ -82,7 +85,10 @@ namespace QuestPDF.Fluent
             
             var container = new Container();
             Decoration.After = container;
-            return container.DebugPointer(DebugPointerType.ElementStructure, "After").Repeat();
+
+            return container
+                .DebugPointer(DebugPointerType.ElementStructure, "After")
+                .RepeatAsFooter();
         }
         
         /// <summary>

+ 16 - 0
Source/QuestPDF/Fluent/ElementExtensions.cs

@@ -448,6 +448,22 @@ namespace QuestPDF.Fluent
             return element.Element(new RepeatContent());
         }
         
+        internal static IContainer RepeatAsHeader(this IContainer element)
+        {
+            return element.Element(new RepeatContent
+            {
+                RepeatContext = RepeatContent.RepeatContextType.PageHeader
+            });
+        }
+        
+        internal static IContainer RepeatAsFooter(this IContainer element)
+        {
+            return element.Element(new RepeatContent
+            {
+                RepeatContext = RepeatContent.RepeatContextType.PageFooter
+            });
+        }
+        
         /// <summary>
         /// <para>
         /// Delays the creation of document content and reduces its lifetime, significantly lowering memory usage in large documents containing thousands of pages. 

+ 6 - 2
Source/QuestPDF/Fluent/MultiColumnExtensions.cs

@@ -2,6 +2,7 @@ using System;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Elements;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 namespace QuestPDF.Fluent;
 
@@ -82,9 +83,12 @@ public sealed class MultiColumnDescriptor
         if (MultiColumn.Spacer is not Empty)
             throw new DocumentComposeException("The 'MultiColumn.Spacer' layer has already been defined. Please call this method only once.");
         
-        var container = new RepeatContent();
+        var container = new Container();
         MultiColumn.Spacer = container;
-        return container;
+        
+        return container
+            .Artifact(SkSemanticNodeSpecialId.LayoutArtifact)
+            .Repeat();
     }
 }
 

+ 324 - 0
Source/QuestPDF/Fluent/SemanticExtensions.cs

@@ -0,0 +1,324 @@
+using System;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+
+namespace QuestPDF.Fluent;
+
+public static class SemanticExtensions
+{
+    internal static IContainer Artifact(this IContainer container, int nodeId)
+    {
+        return container.Element(new Elements.ArtifactTag
+        {
+            Id = nodeId
+        });
+    }
+    
+    /// <summary>
+    /// Excludes the container content from the semantic tree.
+    /// Use for decorative elements, layout artifacts, or other non-structural content that shouldn't be part of the document's logical structure.
+    /// </summary>
+    public static IContainer SemanticIgnore(this IContainer container)
+    {
+        return container.Artifact(SkSemanticNodeSpecialId.OtherArtifact);
+    }
+    
+    internal static IContainer SemanticTag(this IContainer container, string type, string? alternativeText = null, string? language = null)
+    {
+        return container.Element(new Elements.SemanticTag
+        {
+            TagType = type, 
+            Alt = alternativeText,
+            Lang = language
+        });
+    }
+
+    /// <summary>
+    /// Marks a self-contained body of text that forms a single narrative or exposition,
+    /// such as a blog post, news story, or forum post.
+    /// As a best practice, articles should not be nested within each other.
+    /// </summary>
+    public static IContainer SemanticArticle(this IContainer container)
+    {
+        return container.SemanticTag("Art");
+    }
+    
+    /// <summary>
+    /// Applies a 'Section' tag, grouping a set of related content.
+    /// A section typically includes a heading (e.g., SemanticHeader2) and its corresponding content.
+    /// Sections can be nested to create a hierarchical document structure.
+    /// </summary>
+    public static IContainer SemanticSection(this IContainer container)
+    {
+        return container.SemanticTag("Sect");
+    }
+    
+    /// <summary>
+    /// Marks a generic block-level container for grouping elements.
+    /// It's often used when a more specific semantic tag (like 'Article' or 'Section') doesn't apply,
+    /// serving as a general-purpose 'div', similar to its HTML counterpart.
+    /// </summary>
+    public static IContainer SemanticDivision(this IContainer container)
+    {
+        return container.SemanticTag("Div");
+    }
+    
+    /// <summary>
+    /// Designates a block of text that is a quotation, typically consisting of one or more paragraphs.
+    /// This is for block-level quotes, as opposed to <see cref="SemanticQuote"/> which is for inline text.
+    /// </summary>
+    public static IContainer SemanticBlockQuotation(this IContainer container)
+    {
+        return container.SemanticTag("BlockQuote");
+    }
+    
+    /// <summary>
+    /// Identifies a brief portion of text that serves as a caption or description
+    /// for a table, figure, or image. It should be placed near the element it describes.
+    /// </summary>
+    public static IContainer SemanticCaption(this IContainer container)
+    {
+        return container.SemanticTag("Caption");
+    }
+    
+    /// <summary>
+    /// Marks a section of the document as an index.
+    /// This container typically holds a sequence of entries and references.
+    /// </summary>
+    public static IContainer SemanticIndex(this IContainer container)
+    {
+        return container.SemanticTag("Index");
+    }
+    
+    /// <summary>
+    /// Applies a language attribute to a container, specifying the natural language (e.g., 'en-US', 'es-ES') of its content.
+    /// This is crucial for accessibility, enabling screen readers to use the correct pronunciation.
+    /// </summary>
+    /// <param name="language">The ISO 639 language code (e.g., 'en-US' or 'fr-FR') for the content.</param>
+    public static IContainer SemanticLanguage(this IContainer container, string language)
+    {
+        return container.SemanticTag("NonStruct", language: language);
+    }
+    
+    #region Table of Contents
+    
+    /// <summary>
+    /// Marks a container as a Table of Contents (TOC).
+    /// <para>
+    /// A TOC should be composed of <see cref="SemanticTableOfContentsItem"/> elements.
+    /// TOCs can be nested to represent a hierarchical document structure.
+    /// </para>
+    /// <para>This tag can also be used for lists of figures, lists of tables, or bibliographies.</para>
+    /// </summary>
+    public static IContainer SemanticTableOfContents(this IContainer container)
+    {
+        return container.SemanticTag("TOC");
+    }
+    
+    /// <summary>
+    /// Marks an individual item within a <see cref="SemanticTableOfContents"/>.
+    /// This typically represents a single entry in the list.
+    /// </summary>
+    public static IContainer SemanticTableOfContentsItem(this IContainer container)
+    {
+        return container.SemanticTag("TOCI");
+    }
+    
+    #endregion
+    
+    #region Headers
+    
+    private static IContainer SemanticHeader(this IContainer container, int level)
+    {
+        if (level < 1 || level > 6)
+            throw new ArgumentOutOfRangeException(nameof(level), "Header level must be between 1 and 6.");
+
+        return container.SemanticTag($"H{level}");
+    }
+    
+    /// <summary>
+    /// Marks the content as a level 1 heading (H1), the highest level in the document hierarchy.
+    /// Headings are crucial for navigation and outlining the document's structure.
+    /// </summary>
+    public static IContainer SemanticHeader1(this IContainer container)
+    {
+        return container.SemanticHeader(1);
+    }
+    
+    /// <summary>
+    /// Marks the content as a level 2 heading (H2).
+    /// </summary>
+    public static IContainer SemanticHeader2(this IContainer container)
+    {
+        return container.SemanticHeader(2);
+    }
+    
+    /// <summary>
+    /// Marks the content as a level 3 heading (H3).
+    /// </summary>
+    public static IContainer SemanticHeader3(this IContainer container)
+    {
+        return container.SemanticHeader(3);
+    }
+    
+    /// <summary>
+    /// Marks the content as a level 4 heading (H4).
+    /// </summary>
+    public static IContainer SemanticHeader4(this IContainer container)
+    {
+        return container.SemanticHeader(4);
+    }
+    
+    /// <summary>
+    /// Marks the content as a level 5 heading (H5).
+    /// </summary>
+    public static IContainer SemanticHeader5(this IContainer container)
+    {
+        return container.SemanticHeader(5);
+    }
+    
+    /// <summary>
+    /// Marks the content as a level 6 heading (H6), the lowest level in the document hierarchy.
+    /// </summary>
+    public static IContainer SemanticHeader6(this IContainer container)
+    {
+        return container.SemanticHeader(6);
+    }
+    
+    #endregion
+    
+    /// <summary>
+    /// Marks a container as a paragraph.
+    /// This is one of the most common block-level tags for organizing text content.
+    /// </summary>
+    public static IContainer SemanticParagraph(this IContainer container)
+    {
+        return container.SemanticTag("P");
+    }
+    
+    #region Lists
+    
+    /// <summary>
+    /// Marks a container as a list.
+    /// Its direct children should be one or more <see cref="SemanticListItem"/> elements.
+    /// A <see cref="SemanticCaption"/> can also be included as an optional first child.
+    /// </summary>
+    public static IContainer SemanticList(this IContainer container)
+    {
+        return container.SemanticTag("L");
+    }
+
+    /// <summary>
+    /// Marks an individual item within a <see cref="SemanticList"/>.
+    /// Its children should typically be a <see cref="SemanticListLabel"/> (e.g., the bullet or number) and/or a <see cref="SemanticListItemBody"/> (the content).
+    /// </summary>
+    public static IContainer SemanticListItem(this IContainer container)
+    {
+        return container.SemanticTag("LI");
+    }
+    
+    /// <summary>
+    /// Marks the label of a list item.
+    /// This container holds the bullet, number (e.g., '1.'), or term (in a definition list) that identifies the list item.
+    /// </summary>
+    public static IContainer SemanticListLabel(this IContainer container)
+    {
+        return container.SemanticTag("Lbl");
+    }
+    
+    /// <summary>
+    /// Marks the body or descriptive content of a <see cref="SemanticListItem"/>.
+    /// This contains the main text or content associated with the list item's label.
+    /// </summary>
+    public static IContainer SemanticListItemBody(this IContainer container)
+    {
+        return container.SemanticTag("LBody");
+    }
+    
+    #endregion
+    
+    #region Table
+    
+    /// <summary>
+    /// Marks a container as a table.
+    /// The library automatically automatically tags headers, rows, cells, etc.
+    /// </summary>
+    public static IContainer SemanticTable(this IContainer container)
+    {
+        return container.SemanticTag("Table");
+    }
+    
+    #endregion
+    
+    #region Inline Elements
+    
+    /// <summary>
+    /// Marks a generic inline portion of text (Span).
+    /// This is useful for grouping inline elements or applying styling, similar to an HTML &lt;span&gt;.
+    /// </summary>
+    /// <param name="alternativeText">Optional alternative text, often used to provide an expansion for an abbreviation or other supplementary information.</param>
+    public static IContainer SemanticSpan(this IContainer container, string? alternativeText = null)
+    {
+        return container.SemanticTag("Span", alternativeText);
+    }
+    
+    /// <summary>
+    /// Marks an inline portion of text as a quote.
+    /// This differs from <see cref="SemanticBlockQuotation"/>, which is intended for block-level content (one or more paragraphs).
+    /// </summary>
+    public static IContainer SemanticQuote(this IContainer container)
+    {
+        return container.SemanticTag("Quote");
+    }
+
+    /// <summary>
+    /// Marks a fragment of text as computer code.
+    /// </summary>
+    public static IContainer SemanticCode(this IContainer container)
+    {
+        return container.SemanticTag("Code");
+    }
+
+    /// <summary>
+    /// Marks the content as a hyperlink (Link).
+    /// </summary>
+    /// <param name="alternativeText">Alternative text describing the link's purpose or destination. This is essential for screen readers.</param>
+    public static IContainer SemanticLink(this IContainer container, string alternativeText)
+    {
+        return container.SemanticTag("Link", alternativeText: alternativeText);
+    }
+    
+    #endregion
+    
+    #region Illustration Elements
+    
+    /// <summary>
+    /// Marks a container as a figure, which is an item of graphical content like a chart, diagram, or photograph.
+    /// </summary>
+    /// <param name="alternativeText">A textual description of the figure, read by screen readers. This is essential for accessibility.</param>
+    public static IContainer SemanticFigure(this IContainer container, string alternativeText)
+    {
+        return container.SemanticTag("Figure", alternativeText: alternativeText);
+    }
+    
+    /// <summary>
+    /// An alias for <see cref="SemanticFigure"/>. Marks the content as an image.
+    /// </summary>
+    /// <param name="alternativeText">A textual description of the image, read by screen readers. This is essential for accessibility.</param>
+    public static IContainer SemanticImage(this IContainer container, string alternativeText)
+    {
+        return container.SemanticFigure(alternativeText);
+    }
+    
+    /// <summary>
+    /// Marks the content as a mathematical formula.
+    /// From a structural and accessibility standpoint, it is treated similarly to a figure.
+    /// </summary>
+    public static IContainer SemanticFormula(this IContainer container, string alternativeText)
+    {
+        return container.SemanticTag("Formula", alternativeText: alternativeText);
+    }
+    
+    #endregion
+}

+ 54 - 14
Source/QuestPDF/Fluent/TableExtensions.cs

@@ -75,6 +75,8 @@ namespace QuestPDF.Fluent
     
     public sealed class TableDescriptor
     {
+        internal bool EnableAutomatedSemanticTagging { get; set; } = false;
+        
         private Table HeaderTable { get; } = new();
         private Table ContentTable { get; } = new();
         private Table FooterTable { get; } = new();
@@ -159,28 +161,53 @@ namespace QuestPDF.Fluent
         {
             var container = new Container();
 
-            ConfigureTable(HeaderTable);
-            ConfigureTable(ContentTable);
-            ConfigureTable(FooterTable);
+            var hasHeader = HeaderTable.Cells.Any();
+            var hasFooter = FooterTable.Cells.Any();
+            
+            ConfigureTable(HeaderTable, Table.TablePartType.Header);
+            ConfigureTable(ContentTable, Table.TablePartType.Body);
+            ConfigureTable(FooterTable, Table.TablePartType.Footer);
+            
+            var tableRequiresAdvancedHeaderTagging = Table.DoesTableBodyRequireExtendedHeaderTagging(HeaderTable.Cells, ContentTable.Cells);
+            HeaderTable.TableRequiresAdvancedHeaderTagging = tableRequiresAdvancedHeaderTagging;
+            ContentTable.TableRequiresAdvancedHeaderTagging = tableRequiresAdvancedHeaderTagging;
+            ContentTable.HeaderCells = HeaderTable.Cells;
             
             container
                 .Decoration(decoration =>
                 {
-                    decoration.Before().Element(HeaderTable);
-                    decoration.Content().ShowIf(ContentTable.Cells.Any()).Element(ContentTable);
-                    decoration.After().Element(FooterTable);
+                    decoration
+                        .Before()
+                        .ShowIf(hasHeader)
+                        .NonTrackingElement(x => EnableAutomatedSemanticTagging ? x.SemanticTag("THead") : x)
+                        .Element(HeaderTable);
+                    
+                    decoration
+                        .Content()
+                        .NonTrackingElement(x => EnableAutomatedSemanticTagging ? x.SemanticTag("TBody") : x)
+                        .ShowIf(ContentTable.Cells.Any())
+                        .Element(ContentTable);
+                    
+                    decoration
+                        .After()
+                        .ShowIf(hasFooter)
+                        .NonTrackingElement(x => EnableAutomatedSemanticTagging ? x.SemanticTag("TFoot") : x)
+                        .Element(FooterTable);
                 });
 
             return container;
-        }
-
-        private static void ConfigureTable(Table table)
-        {
-            if (!table.Columns.Any())
-                throw new DocumentComposeException($"Table should have at least one column. Please call the '{nameof(ColumnsDefinition)}' method to define columns.");
             
-            table.PlanCellPositions();
-            table.ValidateCellPositions();
+            void ConfigureTable(Table table, Table.TablePartType tablePartType)
+            {
+                if (!table.Columns.Any())
+                    throw new DocumentComposeException($"Table should have at least one column. Please call the '{nameof(ColumnsDefinition)}' method to define columns.");
+            
+                table.PlanCellPositions();
+                table.ValidateCellPositions();
+                
+                table.EnableAutomatedSemanticTagging = EnableAutomatedSemanticTagging;
+                table.PartType = tablePartType;
+            }
         }
     }
     
@@ -196,6 +223,7 @@ namespace QuestPDF.Fluent
         public static void Table(this IContainer element, Action<TableDescriptor> handler)
         {
             var descriptor = new TableDescriptor();
+            descriptor.EnableAutomatedSemanticTagging = element is SemanticTag { TagType: "Table" };
             handler(descriptor);
             element.Element(descriptor.CreateElement());
         }
@@ -258,5 +286,17 @@ namespace QuestPDF.Fluent
 
             return tableCellContainer;
         }
+
+        /// <summary>
+        /// Marks the specified table cell as a semantic horizontal header.
+        /// This allows assistive technologies to recognize the cell as a header, improving accessibility and semantic structure.
+        /// </summary>
+        public static ITableCellContainer AsSemanticHorizontalHeader(this ITableCellContainer tableCellContainer)
+        {
+            if (tableCellContainer is TableCell tableCell)
+                tableCell.IsSemanticHorizontalHeader = true;
+
+            return tableCellContainer;
+        }
     }
 }

+ 39 - 6
Source/QuestPDF/Infrastructure/DocumentSettings.cs

@@ -1,16 +1,30 @@
-namespace QuestPDF.Infrastructure
+using System;
+
+namespace QuestPDF.Infrastructure
 {
     public sealed class DocumentSettings
     {
         public const int DefaultRasterDpi = 72;
-        
+
+        [Obsolete("Please use the ConformanceLevel property instead.")]
+        public bool PdfA
+        {
+            get => PDFA_Conformance != PDFA_Conformance.None;
+            set => PDFA_Conformance = value ? PDFA_Conformance.PDFA_3B : PDFA_Conformance.None;
+        }
+
         /// <summary>
-        /// Gets or sets a value indicating whether or not make the document PDF/A-3b conformant.
-        /// If true, include XMP metadata, a document UUID, and sRGB output intent information.
-        /// This adds length to the document and makes it non-reproducable, but are necessary features for PDF/A-3b conformance.
+        /// Gets or sets the PDF/A conformance level for the document.
+        /// This property determines the adherence of the generated PDF to specific archival standards.
         /// </summary>
-        public bool PdfA { get; set; } = false;
+        public PDFA_Conformance PDFA_Conformance { get; set; } = PDFA_Conformance.None;
 
+        /// <summary>
+        /// Gets or sets the conformance level for PDF/UA (Universal Accessibility) compliance.
+        /// Warning: this setting makes the document non-reproducable.
+        /// </summary>
+        public PDFUA_Conformance PDFUA_Conformance { get; set; } = PDFUA_Conformance.None;
+        
         /// <summary>
         /// Gets or sets a value indicating whether the generated document should be additionally compressed. May greatly reduce file size with a small increase in generation time.
         /// </summary>
@@ -40,4 +54,23 @@
         
         public static DocumentSettings Default => new DocumentSettings();
     }
+    
+    public enum PDFA_Conformance
+    {
+        None = 0,
+        PDFA_1A = 1,
+        PDFA_1B = 2,
+        PDFA_2A = 3,
+        PDFA_2B = 4,
+        PDFA_2U = 5,
+        PDFA_3A = 6,
+        PDFA_3B = 7,
+        PDFA_3U = 8
+    }
+    
+    public enum PDFUA_Conformance
+    {
+        None = 0,
+        PDFUA_1 = 1
+    }
 }

+ 6 - 1
Source/QuestPDF/Infrastructure/IDocumentCanvas.cs

@@ -1,12 +1,17 @@
-namespace QuestPDF.Infrastructure
+using QuestPDF.Drawing;
+
+namespace QuestPDF.Infrastructure
 {
     internal interface IDocumentCanvas
     {
+        void SetSemanticTree(SemanticTreeNode? semanticTree);
+        
         void BeginDocument();
         void EndDocument();
         
         void BeginPage(Size size);
         void EndPage();
+        
         IDrawingCanvas GetDrawingCanvas();
     }
 }

+ 4 - 2
Source/QuestPDF/Infrastructure/IDrawingCanvas.cs

@@ -37,8 +37,10 @@ namespace QuestPDF.Infrastructure
         void ClipRectangle(SkRect clipArea);
         void ClipRoundedRectangle(SkRoundedRect clipArea);
         
-        void DrawHyperlink(string url, Size size);
-        void DrawSectionLink(string sectionName, Size size);
+        void DrawHyperlink(Size size, string url, string? description);
+        void DrawSectionLink(Size size, string sectionName, string? description);
         void DrawSection(string sectionName);
+        
+        void SetSemanticNodeId(int nodeId);
     }
 }

+ 8 - 0
Source/QuestPDF/Infrastructure/ISemanticAware.cs

@@ -0,0 +1,8 @@
+using QuestPDF.Drawing;
+
+namespace QuestPDF.Infrastructure;
+
+internal interface ISemanticAware
+{
+    public SemanticTreeManager? SemanticTreeManager { get; set; }
+}

+ 2 - 1
Source/QuestPDF/QuestPDF.csproj

@@ -3,7 +3,7 @@
         <Authors>MarcinZiabek</Authors>
         <Company>CodeFlint</Company>
         <PackageId>QuestPDF</PackageId>
-        <Version>2025.7.4</Version>
+        <Version>2025.12.0-alpha4</Version>
         <PackageDescription>Generate and edit PDF documents in your .NET applications using the open-source QuestPDF library and its C# Fluent API. Build invoices, reports and data exports with ease.</PackageDescription>
         <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
         <LangVersion>12</LangVersion>
@@ -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'">

+ 36 - 1
Source/QuestPDF/Resources/ReleaseNotes.txt

@@ -1 +1,36 @@
-- Fixed the DocumentOperation.ExtendMetadata operation to preserve /Metadata tag attributes (/Subtype /XML) and improve compatibility with the Mustang validation tool for ZUGFeRD files.
+2025.12.0-alpha0:
+- Added initial support for semantic tagging and bookmarks.
+
+
+2025.12.0-alpha1:
+- Ported all changes from the 2025.7.2 release.
+- Added support for the following conformance standards: PDF/A-2b, PDF/A-2u, PDF/A-2a, PDF/A-3b, PDF/A-3u, PDF/A-3a, and PDF/UA-1.
+- Improved the semantic structure of links (both external hyperlinks and internal links to named destinations) for better compatibility with PDF/UA-1.
+- Corrected semantic tagging of decorative elements such as shadows, backgrounds, borders, and lines by marking them as artifacts.
+- Enhanced the semantic structure and accessibility of simple tables (without cells spanning multiple rows or columns) by automatically tagging header cells.
+- Introduced support for horizontal headers to further improve the semantic structure and accessibility of simple tables.
+
+
+2025.12.0-alpha2:
+- Added semantic auto-tagging for paragraphs.
+- Enhanced auto-tagging feature for tables by supporting more complex cases where cells are spanning multiple rows or columns.
+- Adjusted semantic meaning of MultiColumn spacer to be an artifact.
+- Adjusted semantic meaning of contents that repeats on pages by marking the first occurrence as content, and repetitions as artifacts. This change applies to the Repeat, Decoration and Page Header/Footer elements.
+- Fixed semantic handling of background and watermark page layers.
+- Fixed adding content to the semantic tag tree if it is part of the artifact.
+- Improved visual representation of SemanticTags and ArtifactTags in the Companion App.
+
+
+2025.12.0-alpha3:
+- Ported all changes from the 2025.7.3 release.
+- Updated the Skia native dependency to version m142.
+- Added experimental support for the following conformance standards: PDF/A-1a and PDF/A-1b.
+- Improved automated tagging, which is now enabled only when a relevant conformance standard is active.
+- Created a conformance test suite based on the VeraPDF project.
+
+
+2025.12.0-alpha4:
+- Simplified the Fluent API.
+- Made minor semantic adjustments.
+- Added a conformance test suite based on the Mustang project (for ZUGFeRD).
+- Performed code refactoring and cleanup.

BIN
Source/QuestPDF/Runtimes/linux-arm64/native/libQuestPdfSkia.so


BIN
Source/QuestPDF/Runtimes/linux-musl-x64/native/libQuestPdfSkia.so


BIN
Source/QuestPDF/Runtimes/linux-x64/native/libQuestPdfSkia.so


BIN
Source/QuestPDF/Runtimes/osx-arm64/native/libQuestPdfSkia.dylib


BIN
Source/QuestPDF/Runtimes/osx-x64/native/libQuestPdfSkia.dylib


BIN
Source/QuestPDF/Runtimes/win-x64/native/QuestPdfSkia.dll


BIN
Source/QuestPDF/Runtimes/win-x86/native/QuestPdfSkia.dll


+ 24 - 6
Source/QuestPDF/Skia/SkCanvas.cs

@@ -116,9 +116,9 @@ internal sealed class SkCanvas : IDisposable
         API.canvas_clip_rounded_rectangle(Instance, rect);
     }
     
-    public void AnnotateUrl(float width, float height, string url)
+    public void AnnotateUrl(float width, float height, string url, string? description)
     {
-        API.canvas_annotate_url(Instance, width, height, url);
+        API.canvas_annotate_url(Instance, width, height, url, description);
     }
     
     public void AnnotateDestination(string destinationName)
@@ -126,9 +126,9 @@ internal sealed class SkCanvas : IDisposable
         API.canvas_annotate_destination(Instance, destinationName);
     }
     
-    public void AnnotateDestinationLink(float width, float height, string destinationName)
+    public void AnnotateDestinationLink(float width, float height, string destinationName, string? description)
     {
-        API.canvas_annotate_destination_link(Instance, width, height, destinationName);
+        API.canvas_annotate_destination_link(Instance, width, height, destinationName, description);
     }
     
     public SkCanvasMatrix GetCurrentMatrix()
@@ -141,6 +141,11 @@ internal sealed class SkCanvas : IDisposable
         API.canvas_set_matrix9(Instance, matrix);
     }
     
+    public void SetSemanticNodeId(int nodeId)
+    {
+        API.canvas_set_semantic_node_id(Instance, nodeId);
+    }
+    
     ~SkCanvas()
     {
         this.WarnThatFinalizerIsReached();
@@ -222,18 +227,31 @@ internal sealed class SkCanvas : IDisposable
         public static extern void canvas_clip_rounded_rectangle(IntPtr canvas, SkRoundedRect rect);
         
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
-        public static extern void canvas_annotate_url(IntPtr canvas, float width, float height, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string url);
+        public static extern void canvas_annotate_url(
+            IntPtr canvas, 
+            float width, 
+            float height, 
+            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string url,
+            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string? description);
 
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         public static extern void canvas_annotate_destination(IntPtr canvas, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName);
 
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
-        public static extern void canvas_annotate_destination_link(IntPtr canvas, float width, float height, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName);
+        public static extern void canvas_annotate_destination_link(
+            IntPtr canvas, 
+            float width,
+            float height, 
+            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName,
+            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string? description);
         
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         public static extern SkCanvasMatrix canvas_get_matrix9(IntPtr canvas);
         
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         public static extern void canvas_set_matrix9(IntPtr canvas, SkCanvasMatrix matrix);
+
+        [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void canvas_set_semantic_node_id(IntPtr canvas, int nodeId);
     }
 }

+ 1 - 1
Source/QuestPDF/Skia/SkNativeDependencyCompatibilityChecker.cs

@@ -7,7 +7,7 @@ namespace QuestPDF.Skia;
 
 internal static class SkNativeDependencyCompatibilityChecker
 {
-    private const int ExpectedNativeLibraryVersion = 6;
+    private const int ExpectedNativeLibraryVersion = 8;
     
     private static NativeDependencyCompatibilityChecker Instance { get; } = new()
     {

+ 26 - 2
Source/QuestPDF/Skia/SkPdfDocument.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Runtime.InteropServices;
+using System.Xml;
 
 namespace QuestPDF.Skia;
 
@@ -17,9 +18,32 @@ internal struct SkPdfDocumentMetadata
     public SkDateTime CreationDate;
     public SkDateTime ModificationDate;
 
-    [MarshalAs(UnmanagedType.I1)] public bool SupportPDFA;
+    public PDFA_Conformance PDFA_Conformance;
+    public PDFUA_Conformance PDFUA_Conformance;
+    
     [MarshalAs(UnmanagedType.I1)] public bool CompressDocument;
-    public float RasterDPI;    
+    public float RasterDPI;
+
+    public IntPtr SemanticNodeRoot;
+}
+
+internal enum PDFA_Conformance
+{
+    None = 0,
+    PDFA_1A = 1,
+    PDFA_1B = 2,
+    PDFA_2A = 3,
+    PDFA_2B = 4,
+    PDFA_2U = 5,
+    PDFA_3A = 6,
+    PDFA_3B = 7,
+    PDFA_3U = 8
+}
+
+internal enum PDFUA_Conformance
+{
+    None = 0,
+    PDFUA_1 = 1
 }
 
 internal static class SkPdfDocument

+ 139 - 0
Source/QuestPDF/Skia/SkPdfTag.cs

@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace QuestPDF.Skia;
+
+internal sealed class SkPdfTag : IDisposable
+{
+    public IntPtr Instance { get; private set; }
+    public int NodeId { get; set; }
+    public string Type { get; set; } = "";
+    public string? Alt { get; set; }
+    public string? Lang { get; set; }
+    private ICollection<SkPdfTag>? Children { get; set; }
+    
+    private SkPdfTag(IntPtr instance)
+    {
+        Instance = instance;
+        SkiaAPI.EnsureNotNull(Instance);
+    }
+    
+    public static SkPdfTag Create(int nodeId, string? type, string? alt, string? lang)
+    {
+        var instance = API.pdf_structure_element_create(nodeId, type, alt, lang);
+        return new SkPdfTag(instance) { NodeId = nodeId, Type = type ?? "", Alt = alt, Lang = lang };
+    }
+    
+    public void SetChildren(ICollection<SkPdfTag> children)
+    {
+        Children = children;
+        
+        var childrenArray = children.ToArray();
+        var childrenPointers = childrenArray.Select(c => c.Instance).ToArray();
+        var unmanagedArray = Marshal.AllocHGlobal(IntPtr.Size * childrenPointers.Length);
+        Marshal.Copy(childrenPointers, 0, unmanagedArray, childrenPointers.Length);
+        
+        API.pdf_structure_element_set_children(Instance, unmanagedArray, childrenPointers.Length);
+        Marshal.FreeHGlobal(unmanagedArray);
+    }
+
+    public void AddAttribute(string owner, string name, object value)
+    {
+        // for some reason, other marshaling approaches do not work 
+        var ownerBytes = Encoding.ASCII.GetBytes(owner + "\0");
+        var nameBytes = Encoding.ASCII.GetBytes(name + "\0");
+        
+        if (value is string textValue)
+        {
+            var valueBytes = Encoding.ASCII.GetBytes(textValue + "\0");
+            API.pdf_structure_element_add_attribute_text(Instance, ownerBytes, nameBytes, valueBytes);
+        }
+        else if (value is int intValue)
+        {
+            API.pdf_structure_element_add_attribute_integer(Instance, ownerBytes, nameBytes, intValue);
+        }
+        else if (value is float floatValue)
+        {
+            API.pdf_structure_element_add_attribute_float(Instance, ownerBytes, nameBytes, floatValue);
+        }
+        else if (value is float[] floatArray)
+        {
+            API.pdf_structure_element_add_attribute_float_array(Instance, ownerBytes, nameBytes, floatArray, floatArray.Length);
+        }
+        else if (value is int[] nodeIds)
+        {
+            API.pdf_structure_element_add_attribute_node_ids(Instance, ownerBytes, nameBytes, nodeIds, nodeIds.Length);
+        }
+        else
+        {
+            throw new ArgumentException($"Unsupported attribute value type: {value.GetType()}");
+        }
+    }
+
+    ~SkPdfTag()
+    {
+        this.WarnThatFinalizerIsReached();
+        Dispose();
+    }
+    
+    public void Dispose()
+    {
+        if (Instance == IntPtr.Zero)
+            return;
+
+        // to dispose the entire tree, it is enough to invoke the pdf_structure_element_delete method on the root element
+        // root's children should be only marked as disposed
+        DisposeChildren(this);
+        
+        API.pdf_structure_element_delete(Instance);
+        Instance = IntPtr.Zero;
+        GC.SuppressFinalize(this);
+        
+        static void DisposeChildren(SkPdfTag parent)
+        {
+            if (parent.Children == null)
+                return;
+
+            foreach (var child in parent.Children)
+            {
+                child.Instance = IntPtr.Zero;
+                GC.SuppressFinalize(child);
+                DisposeChildren(child);
+            }
+        }
+    }
+    
+    private static class API
+    {
+        [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern IntPtr pdf_structure_element_create(
+            int nodeId,
+            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string type,
+            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string alt,
+            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string lang);
+        
+        [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void pdf_structure_element_set_children(IntPtr element, IntPtr children, int count);
+        
+        [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void pdf_structure_element_add_attribute_text(IntPtr element, byte[] owner, byte[] name, byte[] value);
+        
+        [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void pdf_structure_element_add_attribute_integer(IntPtr element, byte[] owner, byte[] name, int value);
+        
+        [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void pdf_structure_element_add_attribute_float(IntPtr element, byte[] owner, byte[] name, float value);
+        
+        [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void pdf_structure_element_add_attribute_float_array(IntPtr element, byte[] owner, byte[] name, float[] array, int arrayLength);
+
+        [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void pdf_structure_element_add_attribute_node_ids(IntPtr element, byte[] owner, byte[] name, int[] array, int arrayLength);
+        
+        [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void pdf_structure_element_delete(IntPtr element);
+    }
+}

+ 14 - 0
Source/QuestPDF/Skia/SkSemanticNodeSpecialId.cs

@@ -0,0 +1,14 @@
+namespace QuestPDF.Skia;
+
+internal class SkSemanticNodeSpecialId
+{
+    public const int Nothing = 0;
+    public const int OtherArtifact = -1;
+    public const int PaginationArtifact = -2;
+    public const int PaginationHeaderArtifact = -3;
+    public const int PaginationFooterArtifact = -4;
+    public const int PaginationWatermarkArtifact = -5;
+    public const int LayoutArtifact = -6;
+    public const int PageArtifact = -7;
+    public const int BackgroundArtifact = -8;
+}

Some files were not shown because too many files changed in this diff