Browse Source

Feature: document operations with the qpdf library (#1067)

* Moved implementation

* Document Operation: moved repeated documentation to resources

* Added release notes and `qpdf` license.

* Document operation: added support for attachment relationship

* Document operation: added ExtendMetadata section

* Added ZUGFeRD example

* Updated to skia m132

* Updated qpdf

* ZUGFeRD produces now correct output

* Update README.md

* Layout Debugging: fixed rare exceptions and improved compatibility

* Updated documentation regarding PDF/A-3b conformance

* Integrated nuget strong-name signing

* Published NugetStrongNameSigningKeyForQuestPDF.snk

* Update QuestPDF.csproj

* Changed location of NugetStrongNameSigningKeyForQuestPDF.snk

* Changed build configuration

* Update main.yml

* 2024.12.0-rc0

* Update main.yml

* Remove ARM64 platform target configuration from QuestPDF.ZUGFeRD.csproj

* Generalized loading assemblies

* libqpdf: adjusted names

* QpdfNativeDependencyCompatibilityChecker: lazy compatibility evaluation

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Improved NativeDependencyCompatibilityChecker

* Qpdf: build against openssl 3

* Fixed API compatibility issue

* Qpdf: extended windows native assemblies with required dependencies

* 2024.12.0-rc1

* Updated skia dependency (fixed underline issue)

* 2024.12.0-rc2

* 2024.12.0-rc3
Marcin Ziąbek 1 year ago
parent
commit
0f79ffbe88
40 changed files with 2134 additions and 133 deletions
  1. 14 0
      .github/workflows/main.yml
  2. 43 0
      Source/QuestPDF.ZUGFeRD/GenerationTest.cs
  3. 36 0
      Source/QuestPDF.ZUGFeRD/QuestPDF.ZUGFeRD.csproj
  4. 87 0
      Source/QuestPDF.ZUGFeRD/resource-factur-x.xml
  5. 71 0
      Source/QuestPDF.ZUGFeRD/resource-zugferd-metadata.xml
  6. 6 0
      Source/QuestPDF.sln
  7. 1 2
      Source/QuestPDF/Drawing/DocumentGenerator.cs
  8. 1 2
      Source/QuestPDF/Drawing/FontManager.cs
  9. 483 0
      Source/QuestPDF/Fluent/DocumentOperation.cs
  10. 3 2
      Source/QuestPDF/Fluent/ElementExtensions.cs
  11. 2 2
      Source/QuestPDF/Fluent/MinimalApi.cs
  12. 1 1
      Source/QuestPDF/Helpers/Helpers.cs
  13. 84 8
      Source/QuestPDF/Helpers/NativeDependencyCompatibilityChecker.cs
  14. 3 3
      Source/QuestPDF/Helpers/NativeDependencyProvider.cs
  15. 1 1
      Source/QuestPDF/Helpers/Placeholders.cs
  16. 1 1
      Source/QuestPDF/Infrastructure/Image.cs
  17. 89 0
      Source/QuestPDF/Qpdf/JobConfiguration.cs
  18. 561 0
      Source/QuestPDF/Qpdf/MimeHelper.cs
  19. 102 0
      Source/QuestPDF/Qpdf/QpdfAPI.cs
  20. 45 0
      Source/QuestPDF/Qpdf/QpdfNativeDependencyCompatibilityChecker.cs
  21. 118 0
      Source/QuestPDF/Qpdf/SimpleJsonSerializer.cs
  22. 1 1
      Source/QuestPDF/QuestPDF.csproj
  23. 148 0
      Source/QuestPDF/Resources/Documentation.xml
  24. 202 0
      Source/QuestPDF/Resources/ExternalDependencyLicenses/qpdf.txt
  25. 13 29
      Source/QuestPDF/Resources/ReleaseNotes.txt
  26. BIN
      Source/QuestPDF/Runtimes/linux-arm64/native/libqpdf.so
  27. BIN
      Source/QuestPDF/Runtimes/linux-musl-x64/native/libqpdf.so
  28. BIN
      Source/QuestPDF/Runtimes/linux-x64/native/libqpdf.so
  29. BIN
      Source/QuestPDF/Runtimes/osx-arm64/native/libqpdf.dylib
  30. BIN
      Source/QuestPDF/Runtimes/osx-x64/native/libqpdf.dylib
  31. BIN
      Source/QuestPDF/Runtimes/win-x64/native/libgcc_s_seh-1.dll
  32. BIN
      Source/QuestPDF/Runtimes/win-x64/native/libstdc++-6.dll
  33. BIN
      Source/QuestPDF/Runtimes/win-x64/native/libwinpthread-1.dll
  34. BIN
      Source/QuestPDF/Runtimes/win-x64/native/qpdf.dll
  35. BIN
      Source/QuestPDF/Runtimes/win-x86/native/libgcc_s_dw2-1.dll
  36. BIN
      Source/QuestPDF/Runtimes/win-x86/native/libstdc++-6.dll
  37. BIN
      Source/QuestPDF/Runtimes/win-x86/native/libwinpthread-1.dll
  38. BIN
      Source/QuestPDF/Runtimes/win-x86/native/qpdf.dll
  39. 2 2
      Source/QuestPDF/Settings.cs
  40. 16 79
      Source/QuestPDF/Skia/SkNativeDependencyCompatibilityChecker.cs

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

@@ -63,6 +63,17 @@ jobs:
           apk add libstdc++ libgcc
 
 
+      - name: Install Dependencies required for QPDF (Linux)
+        if: matrix.runtime.name == 'linux-x64' || matrix.runtime.name == 'linux-arm64'
+        shell: bash
+        run: apt install libssl-dev gnutls-dev libjpeg-dev --yes
+          
+
+      - name: Install Dependencies required for QPDF (Linux MUSL)
+        if: matrix.runtime.name == 'linux-musl-x64'
+        run: apk add openssl gnutls libjpeg-turbo
+
+
       - name: Setup dotnet
         uses: actions/setup-dotnet@v3
         with:
@@ -81,6 +92,9 @@ jobs:
           dotnet test QuestPDF.LayoutTests --configuration Release --runtime ${{ matrix.runtime.name }}
           dotnet test QuestPDF.Examples --configuration Release --runtime ${{ matrix.runtime.name }}
           dotnet test QuestPDF.ReportSample --configuration Release --runtime ${{ matrix.runtime.name }} --framework net8.0
+          dotnet test QuestPDF.ZUGFeRD --configuration Release --runtime ${{ matrix.runtime.name }} --framework net8.0
+
+          dotnet build QuestPDF/QuestPDF.csproj --configuration Release --property WarningLevel=0 --property BUILD_PACKAGE=true
 
           dotnet build QuestPDF/QuestPDF.csproj --configuration Release --property WarningLevel=0 --property BUILD_PACKAGE=true
 

+ 43 - 0
Source/QuestPDF.ZUGFeRD/GenerationTest.cs

@@ -0,0 +1,43 @@
+using QuestPDF.Fluent;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.ZUGFeRD;
+
+public class Tests
+{
+    [Test]
+    public void ZUGFeRD_Test()
+    {
+        // TODO: Please make sure that you are eligible to use the Community license.
+        // To learn more about the QuestPDF licensing, please visit:
+        // https://www.questpdf.com/pricing.html
+        QuestPDF.Settings.License = LicenseType.Community;
+        
+        Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Content().Text("Your invoice content");
+                });
+            })
+            .WithSettings(new DocumentSettings { PdfA = true }) // PDF/A-3b
+            .GeneratePdf("invoice.pdf");
+        
+        DocumentOperation
+            .LoadFile("invoice.pdf")
+            .AddAttachment(new DocumentOperation.DocumentAttachment
+            {
+                Key = "factur-zugferd",
+                FilePath = "resource-factur-x.xml",
+                AttachmentName = "factur-x.xml",
+                MimeType = "text/xml",
+                Description = "Factur-X Invoice",
+                Relationship = DocumentOperation.DocumentAttachmentRelationship.Source,
+                CreationDate = DateTime.UtcNow,
+                ModificationDate = DateTime.UtcNow
+            })
+            .ExtendMetadata(File.ReadAllText("resource-zugferd-metadata.xml"))
+            .Save("zugferd-invoice.pdf");
+    }
+}

+ 36 - 0
Source/QuestPDF.ZUGFeRD/QuestPDF.ZUGFeRD.csproj

@@ -0,0 +1,36 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+        <IsPackable>false</IsPackable>
+        <IsTestProject>true</IsTestProject>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="coverlet.collector" Version="6.0.0"/>
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
+        <PackageReference Include="NUnit" Version="3.14.0"/>
+        <PackageReference Include="NUnit.Analyzers" Version="3.9.0"/>
+        <PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <Using Include="NUnit.Framework"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <ProjectReference Include="..\QuestPDF\QuestPDF.csproj"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <None Update="resource-factur-x.xml">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="resource-zugferd-metadata.xml">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+    </ItemGroup>
+
+</Project>

+ 87 - 0
Source/QuestPDF.ZUGFeRD/resource-factur-x.xml

@@ -0,0 +1,87 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<rsm:CrossIndustryInvoice xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <rsm:ExchangedDocumentContext>
+        <ram:GuidelineSpecifiedDocumentContextParameter>
+            <ram:ID>urn:factur-x.eu:1p0:basicwl</ram:ID>
+        </ram:GuidelineSpecifiedDocumentContextParameter>
+    </rsm:ExchangedDocumentContext>
+    <rsm:ExchangedDocument>
+        <ram:ID>FA-2017-0008</ram:ID>
+        <ram:TypeCode>380</ram:TypeCode>
+        <ram:IssueDateTime>
+            <udt:DateTimeString format="102">20171103</udt:DateTimeString>
+        </ram:IssueDateTime>
+        <ram:IncludedNote>
+            <ram:Content>Free shipping (amount &gt; 300 €)</ram:Content>
+        </ram:IncludedNote>
+    </rsm:ExchangedDocument>
+    <rsm:SupplyChainTradeTransaction>
+        <ram:ApplicableHeaderTradeAgreement>
+            <ram:SellerTradeParty>
+                <ram:Name>Au bon moulin</ram:Name>
+                <ram:SpecifiedLegalOrganization>
+                    <ram:ID schemeID="0002">99999999800010</ram:ID>
+                </ram:SpecifiedLegalOrganization>
+                <ram:PostalTradeAddress>
+                    <ram:PostcodeCode>84340</ram:PostcodeCode>
+                    <ram:LineOne>1242 chemin de l'olive</ram:LineOne>
+                    <ram:CityName>Malaucène</ram:CityName>
+                    <ram:CountryID>FR</ram:CountryID>
+                </ram:PostalTradeAddress>
+                <ram:SpecifiedTaxRegistration>
+                    <ram:ID schemeID="VA">FR11999999998</ram:ID>
+                </ram:SpecifiedTaxRegistration>
+            </ram:SellerTradeParty>
+            <ram:BuyerTradeParty>
+                <ram:Name>Me gusta olive</ram:Name>
+                <ram:PostalTradeAddress>
+                    <ram:PostcodeCode>41700</ram:PostcodeCode>
+                    <ram:LineOne>87 camino de la calor</ram:LineOne>
+                    <ram:CityName>Dos Hermanas</ram:CityName>
+                    <ram:CountryID>ES</ram:CountryID>
+                </ram:PostalTradeAddress>
+                <ram:SpecifiedTaxRegistration>
+                    <ram:ID schemeID="VA">ESA12345674</ram:ID>
+                </ram:SpecifiedTaxRegistration>
+            </ram:BuyerTradeParty>
+            <ram:BuyerOrderReferencedDocument>
+                <ram:IssuerAssignedID>COMPRA0832</ram:IssuerAssignedID>
+            </ram:BuyerOrderReferencedDocument>
+            <ram:ContractReferencedDocument>
+                <ram:IssuerAssignedID>FROLIVE2017</ram:IssuerAssignedID>
+            </ram:ContractReferencedDocument>
+        </ram:ApplicableHeaderTradeAgreement>
+        <ram:ApplicableHeaderTradeDelivery/>
+        <ram:ApplicableHeaderTradeSettlement>
+            <ram:PaymentReference>FA-2017-0008</ram:PaymentReference>
+            <ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
+            <ram:SpecifiedTradeSettlementPaymentMeans>
+                <ram:TypeCode>30</ram:TypeCode>
+                <ram:PayeePartyCreditorFinancialAccount>
+                    <ram:IBANID>FR2012421242124212421242124</ram:IBANID>
+                </ram:PayeePartyCreditorFinancialAccount>
+            </ram:SpecifiedTradeSettlementPaymentMeans>
+            <ram:ApplicableTradeTax>
+                <ram:CalculatedAmount currencyID="EUR">0.00</ram:CalculatedAmount>
+                <ram:TypeCode>VAT</ram:TypeCode>
+                <ram:ExemptionReason>French VAT exemption according to articles 262 ter I (for products) and/or 283-2 (for services) of "CGI"</ram:ExemptionReason>
+                <ram:BasisAmount currencyID="EUR">2076.76</ram:BasisAmount>
+                <ram:CategoryCode>K</ram:CategoryCode>
+                <ram:RateApplicablePercent>0.00</ram:RateApplicablePercent>
+            </ram:ApplicableTradeTax>
+            <ram:SpecifiedTradePaymentTerms>
+                <ram:DueDateDateTime>
+                    <udt:DateTimeString format="102">20171203</udt:DateTimeString>
+                </ram:DueDateDateTime>
+            </ram:SpecifiedTradePaymentTerms>
+            <ram:SpecifiedTradeSettlementHeaderMonetarySummation>
+                <ram:LineTotalAmount currencyID="EUR">2076.76</ram:LineTotalAmount>
+                <ram:TaxBasisTotalAmount currencyID="EUR">2076.76</ram:TaxBasisTotalAmount>
+                <ram:TaxTotalAmount currencyID="EUR">0.00</ram:TaxTotalAmount>
+                <ram:GrandTotalAmount currencyID="EUR">2076.76</ram:GrandTotalAmount>
+                <ram:TotalPrepaidAmount currencyID="EUR">623.00</ram:TotalPrepaidAmount>
+                <ram:DuePayableAmount currencyID="EUR">1453.76</ram:DuePayableAmount>
+            </ram:SpecifiedTradeSettlementHeaderMonetarySummation>
+        </ram:ApplicableHeaderTradeSettlement>
+    </rsm:SupplyChainTradeTransaction>
+</rsm:CrossIndustryInvoice>

+ 71 - 0
Source/QuestPDF.ZUGFeRD/resource-zugferd-metadata.xml

@@ -0,0 +1,71 @@
+<rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/" rdf:about="">
+    <dc:title>
+        <rdf:Alt>
+            <rdf:li xml:lang="x-default">Au bon moulin: Facture FA-2017-0008 dated 2017-11-03</rdf:li>
+        </rdf:Alt>
+    </dc:title>
+    <dc:creator>
+        <rdf:Seq>
+            <rdf:li>Au bon moulin</rdf:li>
+        </rdf:Seq>
+    </dc:creator>
+    <dc:description>
+        <rdf:Alt>
+            <rdf:li xml:lang="x-default">Factur-X Facture FA-2017-0008 dated 2017-11-03 issued by Au
+                bon moulin
+            </rdf:li>
+        </rdf:Alt>
+    </dc:description>
+</rdf:Description>
+
+<rdf:Description xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
+                 xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"
+                 xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#" rdf:about="">
+<pdfaExtension:schemas>
+    <rdf:Bag>
+        <rdf:li rdf:parseType="Resource">
+            <pdfaSchema:schema>Factur-X 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 Factur-X 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 conformance level of the embedded Factur-X
+                            data
+                        </pdfaProperty:description>
+                    </rdf:li>
+                </rdf:Seq>
+            </pdfaSchema:property>
+        </rdf:li>
+    </rdf:Bag>
+</pdfaExtension:schemas>
+</rdf:Description>
+
+<rdf:Description xmlns:fx="urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#" rdf:about="">
+<fx:DocumentType>INVOICE</fx:DocumentType>
+<fx:DocumentFileName>factur-x.xml</fx:DocumentFileName>
+<fx:Version>1.0</fx:Version>
+<fx:ConformanceLevel>BASIC WL</fx:ConformanceLevel>
+</rdf:Description>

+ 6 - 0
Source/QuestPDF.sln

@@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.LayoutTests", "Que
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Companion.TestRunner", "QuestPDF.Companion.TestRunner\QuestPDF.Companion.TestRunner.csproj", "{A7A5A88F-1E8C-4FB9-A054-AC5D46C8F18E}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.ZUGFeRD", "QuestPDF.ZUGFeRD\QuestPDF.ZUGFeRD.csproj", "{B301CCFC-9E8B-4C4E-B34A-C95CF6D03367}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -47,5 +49,9 @@ Global
 		{A7A5A88F-1E8C-4FB9-A054-AC5D46C8F18E}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{A7A5A88F-1E8C-4FB9-A054-AC5D46C8F18E}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{A7A5A88F-1E8C-4FB9-A054-AC5D46C8F18E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B301CCFC-9E8B-4C4E-B34A-C95CF6D03367}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B301CCFC-9E8B-4C4E-B34A-C95CF6D03367}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B301CCFC-9E8B-4C4E-B34A-C95CF6D03367}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B301CCFC-9E8B-4C4E-B34A-C95CF6D03367}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 EndGlobal

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

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Linq;
 using QuestPDF.Companion;
 using QuestPDF.Drawing.Exceptions;
@@ -18,7 +17,7 @@ namespace QuestPDF.Drawing
     {
         static DocumentGenerator()
         {
-            NativeDependencyCompatibilityChecker.Test();
+            SkNativeDependencyCompatibilityChecker.Test();
         }
         
         internal static void GeneratePdf(SkWriteStream stream, IDocument document)

+ 1 - 2
Source/QuestPDF/Drawing/FontManager.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Reflection;
-using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Skia;
 using QuestPDF.Skia.Text;
@@ -22,7 +21,7 @@ namespace QuestPDF.Drawing
 
         static FontManager()
         {
-            NativeDependencyCompatibilityChecker.Test();
+            SkNativeDependencyCompatibilityChecker.Test();
             RegisterLibraryDefaultFonts();
         }
         

+ 483 - 0
Source/QuestPDF/Fluent/DocumentOperation.cs

@@ -0,0 +1,483 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using QuestPDF.Qpdf;
+
+namespace QuestPDF.Fluent;
+
+/// <summary>
+/// Provides functionality for performing various operations on PDF documents, including loading, merging, overlaying, underlaying, selecting specific pages, adding attachments, and applying encryption settings.
+/// </summary>
+public class DocumentOperation
+{
+    /// <summary>
+    /// Represents configuration options for applying an overlay or underlay to a PDF document using qpdf.
+    /// </summary>
+    public class LayerConfiguration
+    {
+        /// <summary>
+        /// The file path of the overlay or underlay PDF file to be used.
+        /// </summary>
+        public string FilePath { get; set; }
+
+        /// <summary>
+        /// Specifies the range of pages in the output document where the overlay or underlay will be applied.
+        /// If not specified, the overlay or underlay is applied to all output pages.
+        /// </summary>
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.pageSelector"]/*' />
+        public string? TargetPages { get; set; }
+
+        /// <summary>
+        /// Specifies the range of pages in the overlay or underlay file to be used initially.
+        /// If not specified, all pages in the overlay or underlay file will be used in sequence.
+        /// </summary>
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.pageSelector"]/*' />
+        public string? SourcePages { get; set; }
+
+        /// <summary>
+        /// Specifies an optional range of pages in the overlay or underlay file that will repeat after the initial source pages are exhausted.
+        /// Useful for repeating certain pages of the overlay or underlay file across multiple pages of the output.
+        /// </summary>
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.pageSelector"]/*' />
+        public string? RepeatSourcePages { get; set; }
+    }
+
+    public enum DocumentAttachmentRelationship
+    {
+        /// <summary>
+        /// Indicates data files relevant to the document (e.g., supporting datasets or data tables).
+        /// </summary>
+        Data,
+        
+        /// <summary>
+        /// Represents a source file directly used to create the document.
+        /// </summary>
+        Source,
+        
+        /// <summary>
+        /// An alternative representation of the document content (e.g., XML, HTML).
+        /// </summary>
+        Alternative,
+        
+        /// <summary>
+        /// A file supplementing the content, like additional resources.
+        /// </summary>
+        Supplement,
+        
+        /// <summary>
+        /// No specific relationship is defined.
+        /// </summary>
+        Unspecified
+    }
+    
+    public class DocumentAttachment
+    {
+        /// <summary>
+        /// Sets the key for the attachment, specific to the PDF format.
+        /// Defaults to the file name without its path.
+        /// </summary>
+        public string? Key { get; set; }
+    
+        /// <summary>
+        /// The file path of the attachment. Ensure that the specified file exists.
+        /// </summary>
+        public string FilePath { get; set; }
+    
+        /// <summary>
+        /// Specifies the display name for the attachment.
+        /// This name is typically shown to the user and used by most graphical PDF viewers when saving the file.
+        /// Defaults to the file name without its path.
+        /// </summary>
+        public string? AttachmentName { get; set; }
+    
+        /// <summary>
+        /// Specifies the creation date of the attachment. 
+        /// Defaults to the file's creation time.
+        /// </summary>
+        public DateTime? CreationDate { get; set; }
+    
+        /// <summary>
+        /// Specifies the modification date of the attachment.
+        /// Defaults to the file's last modified time.
+        /// </summary>
+        public DateTime? ModificationDate { get; set; }
+    
+        /// <summary>
+        /// Specifies the MIME type of the attachment, such as "text/plain", "application/pdf", "image/png", etc.
+        /// </summary>
+        public string? MimeType { get; set; }
+    
+        /// <summary>
+        /// Sets a description for the attachment, which may be displayed by some PDF viewers.
+        /// </summary>
+        public string? Description { get; set; }
+    
+        /// <summary>
+        /// Indicates whether to replace an existing attachment with the same key.
+        /// If false, an exception is thrown if an attachment with the same key already exists.
+        /// </summary>
+        public bool Replace { get; set; } = true;
+        
+        /// <summary>
+        /// Specifies the relationship of the embedded file to the document for PDF/A-3b compliance.
+        /// </summary>
+        public DocumentAttachmentRelationship? Relationship { get; set; } = null;
+    }
+
+    public class EncryptionBase
+    {
+        /// <summary>
+        /// The user password for the PDF, allowing restricted access based on encryption settings. 
+        /// May be left null to enable opening the PDF without a password, though this may restrict certain operations.
+        /// </summary>
+        public string? UserPassword { get; set; }
+        
+        /// <summary>
+        /// The owner password for the PDF, granting full access to all document features.
+        /// An empty owner password is considered insecure, as is using the same value for both user and owner passwords.
+        /// </summary>
+        public string OwnerPassword { get; set; }
+    }
+    
+    public class Encryption40Bit : EncryptionBase
+    {
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.annotation"]/*' />
+        public bool AllowAnnotation { get; set; } = true;
+        
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.contentExtraction"]/*' />
+        public bool AllowContentExtraction { get; set; } = true;
+        
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.modification"]/*' />
+        public bool AllowModification { get; set; } = true;
+
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.printing"]/*' />
+        public bool AllowPrinting { get; set; } = true;
+    }
+
+    public class Encryption128Bit : EncryptionBase
+    {
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.annotation"]/*' />
+        public bool AllowAnnotation { get; set; } = true;
+    
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.assembly"]/*' />
+        public bool AllowAssembly { get; set; } = true;
+    
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.contentExtraction"]/*' />
+        public bool AllowContentExtraction { get; set; } = true;
+    
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.fillingForms"]/*' />
+        public bool AllowFillingForms { get; set; } = true;
+
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.printing"]/*' />
+        public bool AllowPrinting { get; set; } = true;
+    
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.encryptMetadata"]/*' />
+        public bool EncryptMetadata { get; set; } = true;
+    }
+
+    public class Encryption256Bit : EncryptionBase
+    {
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.annotation"]/*' />
+        public bool AllowAnnotation { get; set; } = true;
+    
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.assembly"]/*' />
+        public bool AllowAssembly { get; set; } = true;
+    
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.contentExtraction"]/*' />
+        public bool AllowContentExtraction { get; set; } = true;
+    
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.fillingForms"]/*' />
+        public bool AllowFillingForms { get; set; } = true;
+
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.allow.printing"]/*' />
+        public bool AllowPrinting { get; set; } = true;
+    
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.encryption.encryptMetadata"]/*' />
+        public bool EncryptMetadata { get; set; } = true;
+    }
+    
+    internal JobConfiguration Configuration { get; private set; }
+    
+    private DocumentOperation()
+    {
+            
+    }
+
+    /// <summary>
+    /// Loads the specified PDF file for processing, enabling operations such as merging, overlaying or underlaying content, selecting pages, adding attachments, and encrypting.
+    /// </summary>
+    /// <param name="filepath">The full path to the PDF file to be loaded.</param>
+    /// <param name="password">The password for the PDF file, if it is password-protected. Optional.</param>
+    public static DocumentOperation LoadFile(string filepath, string? password = null)
+    {
+        if (!File.Exists(filepath))
+            throw new Exception("The file could not be found");
+        
+        return new DocumentOperation
+        {
+            Configuration = new JobConfiguration
+            {
+                InputFile = filepath,
+                Password = password
+            }
+        };
+    }
+    
+    /// <summary>
+    /// Selects specific pages from the current document based on the provided page selector, marking them for further operations.
+    /// </summary>
+    /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.pageSelector"]/*' />
+    public DocumentOperation TakePages(string pageSelector)
+    {
+        Configuration.Pages ??= new List<JobConfiguration.PageConfiguration>();
+        
+        Configuration.Pages.Add(new JobConfiguration.PageConfiguration
+        {
+            File = ".",
+            Range = pageSelector
+        });
+        
+        return this;
+    }
+    
+    /// <summary>
+    /// Merges pages from the specified PDF file into the current document, according to the provided page selection.
+    /// </summary>
+    /// <param name="filePath">The path to the PDF file to be merged.</param>
+    /// <param name="pageSelector">An optional <see cref="DocumentPageSelector"/> to specify the range of pages to merge. If not provided, all pages will be merged.</param>
+    /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="documentOperation.pageSelector"]/*' />
+    public DocumentOperation MergeFile(string filePath, string? pageSelector = null)
+    {
+        if (!File.Exists(filePath))
+            throw new Exception("The file could not be found");
+        
+        if (Configuration.Pages == null)
+            TakePages("1-z");
+        
+        Configuration.Pages.Add(new JobConfiguration.PageConfiguration
+        {
+            File = filePath,
+            Range = pageSelector ?? "1-z"
+        });
+        
+        return this;
+    }
+
+    /// <summary>
+    /// Applies an underlay to the document using the specified configuration.
+    /// The underlay pages are drawn beneath the target pages in the output file, potentially obscured by the original content.
+    /// </summary>    
+    public DocumentOperation UnderlayFile(LayerConfiguration configuration)
+    {
+        if (!File.Exists(configuration.FilePath))
+            throw new Exception("The file could not be found");
+        
+        Configuration.Underlay ??= new List<JobConfiguration.LayerConfiguration>();
+        
+        Configuration.Underlay.Add(new JobConfiguration.LayerConfiguration
+        {
+            File = configuration.FilePath,
+            To = configuration.TargetPages,
+            From = configuration.SourcePages,
+            Repeat = configuration.RepeatSourcePages
+        });
+        
+        return this;
+    }
+    
+    /// <summary>
+    /// Applies an overlay to the document using the specified configuration.
+    /// The overlay pages are drawn on top of the target pages in the output file, potentially obscuring the original content.
+    /// </summary>
+    public DocumentOperation OverlayFile(LayerConfiguration configuration)
+    {
+        if (!File.Exists(configuration.FilePath))
+            throw new Exception("The file could not be found");
+        
+        Configuration.Overlay ??= new List<JobConfiguration.LayerConfiguration>();
+        
+        Configuration.Overlay.Add(new JobConfiguration.LayerConfiguration
+        {
+            File = configuration.FilePath,
+            To = configuration.TargetPages,
+            From = configuration.SourcePages,
+            Repeat = configuration.RepeatSourcePages
+        });
+        
+        return this;
+    }
+
+    /// <summary>
+    /// Extends the current document's XMP metadata by adding content within the <c>rdf:Description</c> tag.
+    /// This allows for adding additional descriptive metadata to the PDF, which is useful for compliance standards
+    /// like PDF/A or for industry-specific metadata (e.g., ZUGFeRD).
+    /// </summary>
+    /// <param name="metadata">
+    /// A string containing the metadata to add. This metadata must be valid XML content and conform to the
+    /// RDF structure required by the PDF XMP metadata specification.
+    /// </param>
+    public DocumentOperation ExtendMetadata(string metadata)
+    {
+        Configuration.ExtendMetadata = metadata;
+        return this;
+    }
+    
+    /// <summary>
+    /// Adds an attachment to the document, with specified metadata and configuration options.
+    /// </summary>
+    public DocumentOperation AddAttachment(DocumentAttachment attachment)
+    {
+        Configuration.AddAttachment ??= new List<JobConfiguration.AddDocumentAttachment>();
+
+        if (!File.Exists(attachment.FilePath))
+            throw new Exception("The file could not be found");
+        
+        var file = new FileInfo(attachment.FilePath);
+        
+        Configuration.AddAttachment.Add(new JobConfiguration.AddDocumentAttachment
+        {
+            Key = attachment.Key ?? Path.GetFileName(attachment.FilePath),
+            File = attachment.FilePath,
+            FileName = attachment.AttachmentName ?? file.Name,
+            CreationDate = GetFormattedDate(attachment.CreationDate, File.GetCreationTimeUtc(attachment.FilePath)),
+            ModificationDate = GetFormattedDate(attachment.ModificationDate, File.GetLastWriteTime(attachment.FilePath)),
+            MimeType = attachment.MimeType ?? GetDefaultMimeType(),
+            Description = attachment.Description,
+            Replace = attachment.Replace ? string.Empty : null,
+            Relationship = GetRelationship(attachment.Relationship)
+        });
+        
+        return this;
+
+        string GetDefaultMimeType()
+        {
+            var fileExtension = Path.GetExtension(attachment.FilePath);
+            return MimeHelper.FileExtensionToMimeConversionTable.TryGetValue(fileExtension, out var value) ? value : "text/plain";
+        }
+        
+        string GetFormattedDate(DateTime? value, DateTime defaultValue)
+        {
+            return $"D:{(value ?? defaultValue).ToUniversalTime():yyyyMMddHHmmsss}Z";
+        }
+        
+        string? GetRelationship(DocumentAttachmentRelationship? relationship)
+        {
+            return relationship switch
+            {
+                DocumentAttachmentRelationship.Data => "/Data",
+                DocumentAttachmentRelationship.Source => "/Source",
+                DocumentAttachmentRelationship.Alternative => "/Alternative",
+                DocumentAttachmentRelationship.Supplement => "/Alternative",
+                DocumentAttachmentRelationship.Unspecified => "/Unspecified",
+                null => null,
+                _ => throw new ArgumentOutOfRangeException(nameof(relationship), relationship, null)
+            };
+        }
+    }
+    
+    /// <summary>
+    /// Encrypts the document using 40-bit encryption, applying specified owner and user passwords along with defined permissions.
+    /// </summary>
+    public DocumentOperation Encrypt(Encryption40Bit encryption)
+    {
+        if (Configuration.Encrypt != null)
+            throw new InvalidOperationException("Encryption process can be set only once");
+        
+        Configuration.Encrypt = new JobConfiguration.EncryptionSettings
+        {
+            UserPassword = encryption.UserPassword,
+            OwnerPassword = encryption.OwnerPassword,
+            Options40Bit = new JobConfiguration.Encryption40Bit
+            {
+                Annotate = FormatBooleanFlag(encryption.AllowAnnotation),
+                Extract = FormatBooleanFlag(encryption.AllowContentExtraction),
+                Modify = FormatBooleanFlag(encryption.AllowModification),
+                Print = FormatBooleanFlag(encryption.AllowPrinting)
+            }
+        };
+        
+        return this;
+    }
+    
+    /// <summary>
+    /// Encrypts the document using 128-bit encryption, applying specified owner and user passwords along with defined permissions.
+    /// </summary>
+    public DocumentOperation Encrypt(Encryption128Bit encryption)
+    {
+        if (Configuration.Encrypt != null)
+            throw new InvalidOperationException("Encryption process can be set only once");
+        
+        Configuration.Encrypt = new JobConfiguration.EncryptionSettings
+        {
+            UserPassword = encryption.UserPassword,
+            OwnerPassword = encryption.OwnerPassword,
+            Options128Bit = new JobConfiguration.Encryption128Bit
+            {
+                Annotate = FormatBooleanFlag(encryption.AllowAnnotation),
+                Assemble = FormatBooleanFlag(encryption.AllowAssembly),
+                Extract = FormatBooleanFlag(encryption.AllowContentExtraction),
+                Form = FormatBooleanFlag(encryption.AllowFillingForms),
+                Print = encryption.AllowPrinting ? "full" : "none",
+                CleartextMetadata = encryption.EncryptMetadata ? null : string.Empty
+            }
+        };
+        
+        return this;
+    }
+    
+    /// <summary>
+    /// Encrypts the document using 256-bit encryption, applying specified owner and user passwords along with defined permissions.
+    /// </summary>
+    public DocumentOperation Encrypt(Encryption256Bit encryption)
+    {
+        if (Configuration.Encrypt != null)
+            throw new InvalidOperationException("Encryption process can be set only once");
+        
+        Configuration.Encrypt = new JobConfiguration.EncryptionSettings
+        {
+            UserPassword = encryption.UserPassword,
+            OwnerPassword = encryption.OwnerPassword,
+            Options256Bit = new JobConfiguration.Encryption256Bit
+            {
+                Annotate = FormatBooleanFlag(encryption.AllowAnnotation),
+                Assemble = FormatBooleanFlag(encryption.AllowAssembly),
+                Extract = FormatBooleanFlag(encryption.AllowContentExtraction),
+                Form = FormatBooleanFlag(encryption.AllowFillingForms),
+                Print = encryption.AllowPrinting ? "full" : "none",
+                CleartextMetadata = encryption.EncryptMetadata ? null : string.Empty
+            }
+        };
+        
+        return this;
+    }
+
+    private string FormatBooleanFlag(bool value)
+    {
+        return value ? "y" : "n";
+    }
+    
+    /// <summary>
+    /// Creates linearized (web-optimized) output files.
+    /// Linearized files are structured to allow compliant PDF readers to begin displaying content before the entire file is downloaded.
+    /// Normally, a PDF reader requires the entire file to be present to render content, as essential cross-reference data typically appears at the file’s end.
+    /// </summary>
+    public DocumentOperation Linearize()
+    {
+        Configuration.Linearize = string.Empty;
+        return this;
+    }
+    
+    /// <summary>
+    /// Executes the configured operations on the document and saves the resulting file to the specified path.
+    /// </summary>
+    /// <param name="filePath">The path where the output file will be saved.</param>
+    public void Save(string filePath)
+    {
+        if (File.Exists(filePath))
+            File.Delete(filePath);
+        
+        Configuration.OutputFile = filePath;
+        var json = SimpleJsonSerializer.Serialize(Configuration);
+        QpdfAPI.ExecuteJob(json);
+    }
+}

+ 3 - 2
Source/QuestPDF/Fluent/ElementExtensions.cs

@@ -1,9 +1,10 @@
 using System;
+using System.Net.Mime;
 using System.Runtime.CompilerServices;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Elements;
-using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 namespace QuestPDF.Fluent
 {
@@ -11,7 +12,7 @@ namespace QuestPDF.Fluent
     {
         static ElementExtensions()
         {
-            NativeDependencyCompatibilityChecker.Test();
+            SkNativeDependencyCompatibilityChecker.Test();
         }
         
         internal static Container Create(Action<IContainer> factory)

+ 2 - 2
Source/QuestPDF/Fluent/MinimalApi.cs

@@ -1,7 +1,7 @@
 using System;
 using System.Collections.Generic;
-using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 namespace QuestPDF.Fluent
 {
@@ -9,7 +9,7 @@ namespace QuestPDF.Fluent
     {
         static Document()
         {
-            NativeDependencyCompatibilityChecker.Test();
+            SkNativeDependencyCompatibilityChecker.Test();
         }
         
         private Action<IDocumentContainer> ContentSource { get; }

+ 1 - 1
Source/QuestPDF/Helpers/Helpers.cs

@@ -15,7 +15,7 @@ namespace QuestPDF.Helpers
     {
         static Helpers()
         {
-            NativeDependencyCompatibilityChecker.Test();
+            SkNativeDependencyCompatibilityChecker.Test();
         }
         
         internal static byte[] LoadEmbeddedResource(string resourceName)

+ 84 - 8
Source/QuestPDF/Helpers/NativeDependencyCompatibilityChecker.cs

@@ -1,22 +1,98 @@
 using System;
-using System.Collections.Generic;
+using System.Linq;
 using System.Runtime.InteropServices;
-using QuestPDF.Drawing.Exceptions;
-using QuestPDF.Skia;
 
 namespace QuestPDF.Helpers
 {
-    internal static class NativeDependencyCompatibilityChecker
+    internal class NativeDependencyCompatibilityChecker
     {
-        private static bool IsCompatibilityChecked = false;
+        private bool IsCompatibilityChecked { get; set; } = false;
+
+        public Action ExecuteNativeCode { get; set; } = () => { };
+        public Func<string> ExceptionHint { get; set; } = () => string.Empty;
         
-        public static void Test()
+        public void Test()
         {
             if (IsCompatibilityChecked)
                 return;
-            
+
+            TestOnce();
             IsCompatibilityChecked = true;
-            SkNativeDependencyCompatibilityChecker.Test();
+        }
+        
+        private void TestOnce()
+        {
+            if (IsCompatibilityChecked)
+                return;
+            
+            const string exceptionBaseMessage = "The QuestPDF library has encountered an issue while loading one of its dependencies.";
+            const string paragraph = "\n\n";
+                
+            // test with dotnet-based mechanism where native files are provided
+            // in the "runtimes/{rid}/native" folder on Core, or by the targets file on .NET Framework
+            var innerException = CheckIfExceptionIsThrownWhenLoadingNativeDependencies();
+
+            if (innerException == null)
+                return;
+
+            if (!NativeDependencyProvider.IsCurrentPlatformSupported())
+                ThrowCompatibilityException(innerException);
+            
+            // detect platform, copy appropriate native files and test compatibility again
+            NativeDependencyProvider.EnsureNativeFileAvailability();
+            
+            innerException = CheckIfExceptionIsThrownWhenLoadingNativeDependencies();
+
+            if (innerException == null)
+                return;
+
+            ThrowCompatibilityException(innerException);
+            
+            void ThrowCompatibilityException(Exception innerException)
+            {
+                var supportedRuntimes = string.Join(", ", NativeDependencyProvider.SupportedPlatforms);
+                var currentRuntime = NativeDependencyProvider.GetRuntimePlatform();
+                
+                var message = 
+                    $"{exceptionBaseMessage}{paragraph}" +
+                    "Your runtime is currently not supported by QuestPDF. " +
+                    $"Currently supported runtimes are: {supportedRuntimes}. ";
+
+                if (NativeDependencyProvider.SupportedPlatforms.Contains(currentRuntime))
+                {
+                    message += $"{paragraph}It appears that your current operating system distribution may be outdated. For optimal compatibility, please consider updating it to a more recent version.";
+                }
+                else
+                {
+                    message += $"{paragraph}Your current runtime is detected as '{currentRuntime}'.";
+                }
+
+                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                    message += $"{paragraph}Please always set the 'Platform target' to either 'X86' or 'X64' in your startup project settings. Please do not use the 'Any CPU' option.";
+                
+                if (RuntimeInformation.ProcessArchitecture is Architecture.Arm)
+                    message += $"{paragraph}Please consider setting the 'Platform target' property to 'Arm64' in your project settings.";
+
+                var hint = ExceptionHint.Invoke();
+                
+                if (!string.IsNullOrEmpty(hint))
+                    message += $"{paragraph}{ExceptionHint}";
+                
+                throw new Exception(message, innerException);
+            }
+        }
+    
+        private Exception? CheckIfExceptionIsThrownWhenLoadingNativeDependencies()
+        {
+            try
+            {
+                ExecuteNativeCode();
+                return null;
+            }
+            catch (Exception exception)
+            {
+                return exception;
+            }
         }
     }
 }

+ 3 - 3
Source/QuestPDF/Skia/SkNativeDependencyProvider.cs → Source/QuestPDF/Helpers/NativeDependencyProvider.cs

@@ -4,9 +4,9 @@ using System.IO;
 using System.Linq;
 using System.Runtime.InteropServices;
 
-namespace QuestPDF.Skia;
+namespace QuestPDF.Helpers;
 
-internal static class SkNativeDependencyProvider
+internal static class NativeDependencyProvider
 {
     public static readonly string[] SupportedPlatforms =
     {
@@ -57,7 +57,7 @@ internal static class SkNativeDependencyProvider
             Environment.CurrentDirectory,
             AppContext.BaseDirectory,
             Directory.GetCurrentDirectory(),
-            new FileInfo(typeof(SkNativeDependencyProvider).Assembly.Location).Directory?.FullName
+            new FileInfo(typeof(NativeDependencyProvider).Assembly.Location).Directory?.FullName
         };
         
         foreach (var location in availableLocations)

+ 1 - 1
Source/QuestPDF/Helpers/Placeholders.cs

@@ -9,7 +9,7 @@ namespace QuestPDF.Helpers
     {
         static Placeholders()
         {
-            NativeDependencyCompatibilityChecker.Test();
+            SkNativeDependencyCompatibilityChecker.Test();
         }
         
         public static readonly Random Random = new Random();

+ 1 - 1
Source/QuestPDF/Infrastructure/Image.cs

@@ -26,7 +26,7 @@ namespace QuestPDF.Infrastructure
     {
         static Image()
         {
-            NativeDependencyCompatibilityChecker.Test();
+            SkNativeDependencyCompatibilityChecker.Test();
         }
         
         internal SkImage SkImage { get; }

+ 89 - 0
Source/QuestPDF/Qpdf/JobConfiguration.cs

@@ -0,0 +1,89 @@
+using System.Collections.Generic;
+
+namespace QuestPDF.Qpdf;
+
+using Name = SimpleJsonPropertyNameAttribute;
+
+class JobConfiguration
+{
+    [Name("inputFile")] public string InputFile { get; set; }
+    [Name("password")] public string? Password { get; set; }
+    
+    [Name("outputFile")] public string OutputFile { get; set; }
+    
+    [Name("pages")] public ICollection<PageConfiguration>? Pages { get; set; }
+    [Name("overlay")] public ICollection<LayerConfiguration>? Overlay { get; set; }
+    [Name("underlay")] public ICollection<LayerConfiguration>? Underlay { get; set; }
+
+    [Name("extendMetadata")] public string? ExtendMetadata { get; set; }
+    [Name("addAttachment")] public ICollection<AddDocumentAttachment>? AddAttachment { get; set; }
+    
+    [Name("encrypt")] public EncryptionSettings? Encrypt { get; set; } 
+    [Name("linearize")] public string? Linearize { get; set; }
+    [Name("newlineBeforeEndstream")] public string? NewlineBeforeEndstream { get; set; } = string.Empty;
+    
+    internal class PageConfiguration
+    {
+        [Name("file")] public string File { get; set; }
+        [Name("range")] public string Range { get; set; }
+    }
+    
+    internal class LayerConfiguration
+    {
+        [Name("file")] public string File { get; set; }
+        [Name("to")] public string? To { get; set; }
+        [Name("from")] public string? From { get; set; }
+        [Name("repeat")] public string? Repeat { get; set; }
+    }
+    
+    public class AddDocumentAttachment
+    {
+        [Name("key")] public string Key { get; set; }
+        [Name("file")] public string File { get; set; }
+        [Name("filename")] public string? FileName { get; set; }
+        [Name("creationdate")] public string? CreationDate { get; set; }
+        [Name("moddate")] public string? ModificationDate { get; set; }
+        [Name("mimetype")] public string? MimeType { get; set; }
+        [Name("description")] public string? Description { get; set; }
+        [Name("replace")] public string? Replace { get; set; }
+        [Name("relationship")] public string? Relationship { get; set; }
+    }
+
+    public class EncryptionSettings
+    {
+        [Name("userPassword")] public string? UserPassword { get; set; }
+        [Name("ownerPassword")] public string OwnerPassword { get; set; }
+        
+        [Name("40bit")] public Encryption40Bit? Options40Bit { get; set; }
+        [Name("128bit")] public Encryption128Bit? Options128Bit { get; set; }
+        [Name("256bit")] public Encryption256Bit? Options256Bit { get; set; }
+    }
+    
+    public class Encryption40Bit
+    {
+        [Name("annotate")] public string Annotate { get; set; }
+        [Name("extract")] public string Extract { get; set; }
+        [Name("modify")] public string Modify { get; set; }
+        [Name("print")] public string Print { get; set; }
+    }
+
+    public class Encryption128Bit
+    {
+        [Name("annotate")] public string Annotate { get; set; }
+        [Name("assemble")] public string Assemble { get; set; }
+        [Name("extract")] public string Extract { get; set; }
+        [Name("form")] public string Form { get; set; }
+        [Name("print")] public string? Print { get; set; }
+        [Name("cleartextMetadata")] public string? CleartextMetadata { get; set; }
+    }
+
+    public class Encryption256Bit
+    {
+        [Name("annotate")] public string Annotate { get; set; }
+        [Name("assemble")] public string Assemble { get; set; }
+        [Name("extract")] public string Extract { get; set; }
+        [Name("form")] public string Form { get; set; }
+        [Name("print")] public string? Print { get; set; }
+        [Name("cleartextMetadata")] public string? CleartextMetadata { get; set; }
+    }
+}

+ 561 - 0
Source/QuestPDF/Qpdf/MimeHelper.cs

@@ -0,0 +1,561 @@
+using System.Collections.Generic;
+
+namespace QuestPDF.Qpdf;
+
+class MimeHelper
+{
+    public static readonly IReadOnlyDictionary<string, string> FileExtensionToMimeConversionTable = new Dictionary<string, string>
+    {
+        ["3dmf"] = "x-world/x-3dmf",
+        ["3dm"] = "x-world/x-3dmf",
+        ["3g2"] = "video/3gpp2",
+        ["3gp"] = "video/3gpp",
+        ["7z"] = "application/x-7z-compressed",
+        ["aab"] = "application/x-authorware-bin",
+        ["aac"] = "audio/aac",
+        ["aam"] = "application/x-authorware-map",
+        ["aas"] = "application/x-authorware-seg",
+        ["abc"] = "text/vnd.abc",
+        ["acgi"] = "text/html",
+        ["acx"] = "application/internet-property-stream",
+        ["afl"] = "video/animaflex",
+        ["ai"] = "application/postscript",
+        ["aif"] = "audio/aiff",
+        ["aifc"] = "audio/aiff",
+        ["aiff"] = "audio/aiff",
+        ["aim"] = "application/x-aim",
+        ["aip"] = "text/x-audiosoft-intra",
+        ["ani"] = "application/x-navi-animation",
+        ["aos"] = "application/x-nokia-9000-communicator-add-on-software",
+        ["appcache"] = "text/cache-manifest",
+        ["application"] = "application/x-ms-application",
+        ["aps"] = "application/mime",
+        ["art"] = "image/x-jg",
+        ["asf"] = "video/x-ms-asf",
+        ["asm"] = "text/x-asm",
+        ["asp"] = "text/asp",
+        ["asr"] = "video/x-ms-asf",
+        ["asx"] = "application/x-mplayer2",
+        ["atom"] = "application/atom+xml",
+        ["au"] = "audio/x-au",
+        ["avi"] = "video/avi",
+        ["avs"] = "video/avs-video",
+        ["axs"] = "application/olescript",
+        ["bas"] = "text/plain",
+        ["bcpio"] = "application/x-bcpio",
+        ["bin"] = "application/octet-stream",
+        ["bm"] = "image/bmp",
+        ["bmp"] = "image/bmp",
+        ["boo"] = "application/book",
+        ["book"] = "application/book",
+        ["boz"] = "application/x-bzip2",
+        ["bsh"] = "application/x-bsh",
+        ["bz2"] = "application/x-bzip2",
+        ["bz"] = "application/x-bzip",
+        ["cat"] = "application/vnd.ms-pki.seccat",
+        ["ccad"] = "application/clariscad",
+        ["cco"] = "application/x-cocoa",
+        ["cc"] = "text/plain",
+        ["cdf"] = "application/cdf",
+        ["cer"] = "application/pkix-cert",
+        ["cha"] = "application/x-chat",
+        ["chat"] = "application/x-chat",
+        ["class"] = "application/x-java-applet",
+        ["clp"] = "application/x-msclip",
+        ["cmx"] = "image/x-cmx",
+        ["cod"] = "image/cis-cod",
+        ["coffee"] = "text/x-coffeescript",
+        ["conf"] = "text/plain",
+        ["cpio"] = "application/x-cpio",
+        ["cpp"] = "text/plain",
+        ["cpt"] = "application/x-cpt",
+        ["crd"] = "application/x-mscardfile",
+        ["crl"] = "application/pkix-crl",
+        ["crt"] = "application/pkix-cert",
+        ["csh"] = "application/x-csh",
+        ["css"] = "text/css",
+        ["c"] = "text/plain",
+        ["c++"] = "text/plain",
+        ["cxx"] = "text/plain",
+        ["dart"] = "application/dart",
+        ["dcr"] = "application/x-director",
+        ["deb"] = "application/x-deb",
+        ["deepv"] = "application/x-deepv",
+        ["def"] = "text/plain",
+        ["deploy"] = "application/octet-stream",
+        ["der"] = "application/x-x509-ca-cert",
+        ["dib"] = "image/bmp",
+        ["dif"] = "video/x-dv",
+        ["dir"] = "application/x-director",
+        ["disco"] = "text/xml",
+        ["dll"] = "application/x-msdownload",
+        ["dl"] = "video/dl",
+        ["doc"] = "application/msword",
+        ["docm"] = "application/vnd.ms-word.document.macroEnabled.12",
+        ["docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+        ["dot"] = "application/msword",
+        ["dotm"] = "application/vnd.ms-word.template.macroEnabled.12",
+        ["dotx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
+        ["dp"] = "application/commonground",
+        ["drw"] = "application/drafting",
+        ["dtd"] = "application/xml-dtd",
+        ["dvi"] = "application/x-dvi",
+        ["dv"] = "video/x-dv",
+        ["dwf"] = "drawing/x-dwf (old)",
+        ["dwg"] = "application/acad",
+        ["dxf"] = "application/dxf",
+        ["dxr"] = "application/x-director",
+        ["elc"] = "application/x-elc",
+        ["el"] = "text/x-script.elisp",
+        ["eml"] = "message/rfc822",
+        ["eot"] = "application/vnd.bw-fontobject",
+        ["eps"] = "application/postscript",
+        ["es"] = "application/x-esrehber",
+        ["etx"] = "text/x-setext",
+        ["evy"] = "application/envoy",
+        ["exe"] = "application/octet-stream",
+        ["f77"] = "text/plain",
+        ["f90"] = "text/plain",
+        ["fdf"] = "application/vnd.fdf",
+        ["fif"] = "image/fif",
+        ["flac"] = "audio/x-flac",
+        ["fli"] = "video/fli",
+        ["flo"] = "image/florian",
+        ["flr"] = "x-world/x-vrml",
+        ["flx"] = "text/vnd.fmi.flexstor",
+        ["fmf"] = "video/x-atomic3d-feature",
+        ["for"] = "text/plain",
+        ["fpx"] = "image/vnd.fpx",
+        ["frl"] = "application/freeloader",
+        ["f"] = "text/plain",
+        ["funk"] = "audio/make",
+        ["g3"] = "image/g3fax",
+        ["gif"] = "image/gif",
+        ["gl"] = "video/gl",
+        ["gsd"] = "audio/x-gsm",
+        ["gsm"] = "audio/x-gsm",
+        ["gsp"] = "application/x-gsp",
+        ["gss"] = "application/x-gss",
+        ["gtar"] = "application/x-gtar",
+        ["g"] = "text/plain",
+        ["gz"] = "application/x-gzip",
+        ["gzip"] = "application/x-gzip",
+        ["hdf"] = "application/x-hdf",
+        ["help"] = "application/x-helpfile",
+        ["hgl"] = "application/vnd.hp-HPGL",
+        ["hh"] = "text/plain",
+        ["hlb"] = "text/x-script",
+        ["hlp"] = "application/x-helpfile",
+        ["hpg"] = "application/vnd.hp-HPGL",
+        ["hpgl"] = "application/vnd.hp-HPGL",
+        ["hqx"] = "application/binhex",
+        ["hta"] = "application/hta",
+        ["htc"] = "text/x-component",
+        ["h"] = "text/plain",
+        ["htmls"] = "text/html",
+        ["html"] = "text/html",
+        ["htm"] = "text/html",
+        ["htt"] = "text/webviewhtml",
+        ["htx"] = "text/html",
+        ["ice"] = "x-conference/x-cooltalk",
+        ["ico"] = "image/x-icon",
+        ["ics"] = "text/calendar",
+        ["idc"] = "text/plain",
+        ["ief"] = "image/ief",
+        ["iefs"] = "image/ief",
+        ["iges"] = "application/iges",
+        ["igs"] = "application/iges",
+        ["iii"] = "application/x-iphone",
+        ["ima"] = "application/x-ima",
+        ["imap"] = "application/x-httpd-imap",
+        ["inf"] = "application/inf",
+        ["ins"] = "application/x-internett-signup",
+        ["ip"] = "application/x-ip2",
+        ["isp"] = "application/x-internet-signup",
+        ["isu"] = "video/x-isvideo",
+        ["it"] = "audio/it",
+        ["iv"] = "application/x-inventor",
+        ["ivf"] = "video/x-ivf",
+        ["ivr"] = "i-world/i-vrml",
+        ["ivy"] = "application/x-livescreen",
+        ["jam"] = "audio/x-jam",
+        ["jar"] = "application/java-archive",
+        ["java"] = "text/plain",
+        ["jav"] = "text/plain",
+        ["jcm"] = "application/x-java-commerce",
+        ["jfif"] = "image/jpeg",
+        ["jfif--tbnl"] = "image/jpeg",
+        ["jpeg"] = "image/jpeg",
+        ["jpe"] = "image/jpeg",
+        ["jpg"] = "image/jpeg",
+        ["jps"] = "image/x-jps",
+        ["js"] = "application/javascript",
+        ["json"] = "application/json",
+        ["jut"] = "image/jutvision",
+        ["kar"] = "audio/midi",
+        ["ksh"] = "text/x-script.ksh",
+        ["la"] = "audio/nspaudio",
+        ["lam"] = "audio/x-liveaudio",
+        ["latex"] = "application/x-latex",
+        ["list"] = "text/plain",
+        ["lma"] = "audio/nspaudio",
+        ["log"] = "text/plain",
+        ["lsp"] = "application/x-lisp",
+        ["lst"] = "text/plain",
+        ["lsx"] = "text/x-la-asf",
+        ["ltx"] = "application/x-latex",
+        ["m13"] = "application/x-msmediaview",
+        ["m14"] = "application/x-msmediaview",
+        ["m1v"] = "video/mpeg",
+        ["m2a"] = "audio/mpeg",
+        ["m2v"] = "video/mpeg",
+        ["m3u"] = "audio/x-mpequrl",
+        ["m4a"] = "audio/mp4",
+        ["m4v"] = "video/mp4",
+        ["man"] = "application/x-troff-man",
+        ["manifest"] = "application/x-ms-manifest",
+        ["map"] = "application/x-navimap",
+        ["mar"] = "text/plain",
+        ["mbd"] = "application/mbedlet",
+        ["mc$"] = "application/x-magic-cap-package-1.0",
+        ["mcd"] = "application/mcad",
+        ["mcf"] = "image/vasa",
+        ["mcp"] = "application/netmc",
+        ["mdb"] = "application/x-msaccess",
+        ["mesh"] = "model/mesh",
+        ["me"] = "application/x-troff-me",
+        ["mid"] = "audio/midi",
+        ["midi"] = "audio/midi",
+        ["mif"] = "application/x-mif",
+        ["mjf"] = "audio/x-vnd.AudioExplosion.MjuiceMediaFile",
+        ["mjpg"] = "video/x-motion-jpeg",
+        ["mm"] = "application/base64",
+        ["mme"] = "application/base64",
+        ["mny"] = "application/x-msmoney",
+        ["mod"] = "audio/mod",
+        ["mov"] = "video/quicktime",
+        ["movie"] = "video/x-sgi-movie",
+        ["mp2"] = "video/mpeg",
+        ["mp3"] = "audio/mpeg",
+        ["mp4"] = "video/mp4",
+        ["mp4a"] = "audio/mp4",
+        ["mp4v"] = "video/mp4",
+        ["mpa"] = "audio/mpeg",
+        ["mpc"] = "application/x-project",
+        ["mpeg"] = "video/mpeg",
+        ["mpe"] = "video/mpeg",
+        ["mpga"] = "audio/mpeg",
+        ["mpg"] = "video/mpeg",
+        ["mpp"] = "application/vnd.ms-project",
+        ["mpt"] = "application/x-project",
+        ["mpv2"] = "video/mpeg",
+        ["mpv"] = "application/x-project",
+        ["mpx"] = "application/x-project",
+        ["mrc"] = "application/marc",
+        ["ms"] = "application/x-troff-ms",
+        ["msh"] = "model/mesh",
+        ["m"] = "text/plain",
+        ["mvb"] = "application/x-msmediaview",
+        ["mv"] = "video/x-sgi-movie",
+        ["my"] = "audio/make",
+        ["mzz"] = "application/x-vnd.AudioExplosion.mzz",
+        ["nap"] = "image/naplps",
+        ["naplps"] = "image/naplps",
+        ["nc"] = "application/x-netcdf",
+        ["ncm"] = "application/vnd.nokia.configuration-message",
+        ["niff"] = "image/x-niff",
+        ["nif"] = "image/x-niff",
+        ["nix"] = "application/x-mix-transfer",
+        ["nsc"] = "application/x-conference",
+        ["nvd"] = "application/x-navidoc",
+        ["nws"] = "message/rfc822",
+        ["oda"] = "application/oda",
+        ["ods"] = "application/oleobject",
+        ["oga"] = "audio/ogg",
+        ["ogg"] = "audio/ogg",
+        ["ogv"] = "video/ogg",
+        ["ogx"] = "application/ogg",
+        ["omc"] = "application/x-omc",
+        ["omcd"] = "application/x-omcdatamaker",
+        ["omcr"] = "application/x-omcregerator",
+        ["opus"] = "audio/ogg",
+        ["oxps"] = "application/oxps",
+        ["p10"] = "application/pkcs10",
+        ["p12"] = "application/pkcs-12",
+        ["p7a"] = "application/x-pkcs7-signature",
+        ["p7b"] = "application/x-pkcs7-certificates",
+        ["p7c"] = "application/pkcs7-mime",
+        ["p7m"] = "application/pkcs7-mime",
+        ["p7r"] = "application/x-pkcs7-certreqresp",
+        ["p7s"] = "application/pkcs7-signature",
+        ["part"] = "application/pro_eng",
+        ["pas"] = "text/pascal",
+        ["pbm"] = "image/x-portable-bitmap",
+        ["pcl"] = "application/x-pcl",
+        ["pct"] = "image/x-pict",
+        ["pcx"] = "image/x-pcx",
+        ["pdb"] = "chemical/x-pdb",
+        ["pdf"] = "application/pdf",
+        ["pfunk"] = "audio/make",
+        ["pfx"] = "application/x-pkcs12",
+        ["pgm"] = "image/x-portable-graymap",
+        ["pic"] = "image/pict",
+        ["pict"] = "image/pict",
+        ["pkg"] = "application/x-newton-compatible-pkg",
+        ["pko"] = "application/vnd.ms-pki.pko",
+        ["pl"] = "text/plain",
+        ["plx"] = "application/x-PiXCLscript",
+        ["pm4"] = "application/x-pagemaker",
+        ["pm5"] = "application/x-pagemaker",
+        ["pma"] = "application/x-perfmon",
+        ["pmc"] = "application/x-perfmon",
+        ["pm"] = "image/x-xpixmap",
+        ["pml"] = "application/x-perfmon",
+        ["pmr"] = "application/x-perfmon",
+        ["pmw"] = "application/x-perfmon",
+        ["png"] = "image/png",
+        ["pnm"] = "application/x-portable-anymap",
+        ["pot"] = "application/vnd.ms-powerpoint",
+        ["potm"] = "application/vnd.ms-powerpoint.template.macroEnabled.12",
+        ["potx"] = "application/vnd.openxmlformats-officedocument.presentationml.template",
+        ["pov"] = "model/x-pov",
+        ["ppa"] = "application/vnd.ms-powerpoint",
+        ["ppam"] = "application/vnd.ms-powerpoint.addin.macroEnabled.12",
+        ["ppm"] = "image/x-portable-pixmap",
+        ["pps"] = "application/vnd.ms-powerpoint",
+        ["ppsm"] = "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
+        ["ppsx"] = "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
+        ["ppt"] = "application/vnd.ms-powerpoint",
+        ["pptm"] = "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
+        ["pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+        ["ppz"] = "application/mspowerpoint",
+        ["pre"] = "application/x-freelance",
+        ["prf"] = "application/pics-rules",
+        ["prt"] = "application/pro_eng",
+        ["ps"] = "application/postscript",
+        ["p"] = "text/x-pascal",
+        ["pub"] = "application/x-mspublisher",
+        ["pvu"] = "paleovu/x-pv",
+        ["pwz"] = "application/vnd.ms-powerpoint",
+        ["pyc"] = "applicaiton/x-bytecode.python",
+        ["py"] = "text/x-script.phyton",
+        ["qcp"] = "audio/vnd.qcelp",
+        ["qd3d"] = "x-world/x-3dmf",
+        ["qd3"] = "x-world/x-3dmf",
+        ["qif"] = "image/x-quicktime",
+        ["qtc"] = "video/x-qtc",
+        ["qtif"] = "image/x-quicktime",
+        ["qti"] = "image/x-quicktime",
+        ["qt"] = "video/quicktime",
+        ["ra"] = "audio/x-pn-realaudio",
+        ["ram"] = "audio/x-pn-realaudio",
+        ["ras"] = "application/x-cmu-raster",
+        ["rast"] = "image/cmu-raster",
+        ["rexx"] = "text/x-script.rexx",
+        ["rf"] = "image/vnd.rn-realflash",
+        ["rgb"] = "image/x-rgb",
+        ["rm"] = "application/vnd.rn-realmedia",
+        ["rmi"] = "audio/mid",
+        ["rmm"] = "audio/x-pn-realaudio",
+        ["rmp"] = "audio/x-pn-realaudio",
+        ["rng"] = "application/ringing-tones",
+        ["rnx"] = "application/vnd.rn-realplayer",
+        ["roff"] = "application/x-troff",
+        ["rp"] = "image/vnd.rn-realpix",
+        ["rpm"] = "audio/x-pn-realaudio-plugin",
+        ["rss"] = "application/rss+xml",
+        ["rtf"] = "text/richtext",
+        ["rt"] = "text/richtext",
+        ["rtx"] = "text/richtext",
+        ["rv"] = "video/vnd.rn-realvideo",
+        ["s3m"] = "audio/s3m",
+        ["sbk"] = "application/x-tbook",
+        ["scd"] = "application/x-msschedule",
+        ["scm"] = "application/x-lotusscreencam",
+        ["sct"] = "text/scriptlet",
+        ["sdml"] = "text/plain",
+        ["sdp"] = "application/sdp",
+        ["sdr"] = "application/sounder",
+        ["sea"] = "application/sea",
+        ["set"] = "application/set",
+        ["setpay"] = "application/set-payment-initiation",
+        ["setreg"] = "application/set-registration-initiation",
+        ["sgml"] = "text/sgml",
+        ["sgm"] = "text/sgml",
+        ["shar"] = "application/x-bsh",
+        ["sh"] = "text/x-script.sh",
+        ["shtml"] = "text/html",
+        ["sid"] = "audio/x-psid",
+        ["silo"] = "model/mesh",
+        ["sit"] = "application/x-sit",
+        ["skd"] = "application/x-koan",
+        ["skm"] = "application/x-koan",
+        ["skp"] = "application/x-koan",
+        ["skt"] = "application/x-koan",
+        ["sl"] = "application/x-seelogo",
+        ["smi"] = "application/smil",
+        ["smil"] = "application/smil",
+        ["snd"] = "audio/basic",
+        ["sol"] = "application/solids",
+        ["spc"] = "application/x-pkcs7-certificates",
+        ["spl"] = "application/futuresplash",
+        ["spr"] = "application/x-sprite",
+        ["sprite"] = "application/x-sprite",
+        ["spx"] = "audio/ogg",
+        ["src"] = "application/x-wais-source",
+        ["ssi"] = "text/x-server-parsed-html",
+        ["ssm"] = "application/streamingmedia",
+        ["sst"] = "application/vnd.ms-pki.certstore",
+        ["step"] = "application/step",
+        ["s"] = "text/x-asm",
+        ["stl"] = "application/sla",
+        ["stm"] = "text/html",
+        ["stp"] = "application/step",
+        ["sv4cpio"] = "application/x-sv4cpio",
+        ["sv4crc"] = "application/x-sv4crc",
+        ["svf"] = "image/x-dwg",
+        ["svg"] = "image/svg+xml",
+        ["svr"] = "application/x-world",
+        ["swf"] = "application/x-shockwave-flash",
+        ["talk"] = "text/x-speech",
+        ["t"] = "application/x-troff",
+        ["tar"] = "application/x-tar",
+        ["tbk"] = "application/toolbook",
+        ["tcl"] = "text/x-script.tcl",
+        ["tcsh"] = "text/x-script.tcsh",
+        ["tex"] = "application/x-tex",
+        ["texi"] = "application/x-texinfo",
+        ["texinfo"] = "application/x-texinfo",
+        ["text"] = "text/plain",
+        ["tgz"] = "application/x-compressed",
+        ["tiff"] = "image/tiff",
+        ["tif"] = "image/tiff",
+        ["tr"] = "application/x-troff",
+        ["trm"] = "application/x-msterminal",
+        ["ts"] = "text/x-typescript",
+        ["tsi"] = "audio/tsp-audio",
+        ["tsp"] = "audio/tsplayer",
+        ["tsv"] = "text/tab-separated-values",
+        ["ttf"] = "application/x-font-ttf",
+        ["turbot"] = "image/florian",
+        ["txt"] = "text/plain",
+        ["uil"] = "text/x-uil",
+        ["uls"] = "text/iuls",
+        ["unis"] = "text/uri-list",
+        ["uni"] = "text/uri-list",
+        ["unv"] = "application/i-deas",
+        ["uris"] = "text/uri-list",
+        ["uri"] = "text/uri-list",
+        ["ustar"] = "multipart/x-ustar",
+        ["uue"] = "text/x-uuencode",
+        ["uu"] = "text/x-uuencode",
+        ["vcd"] = "application/x-cdlink",
+        ["vcf"] = "text/vcard",
+        ["vcard"] = "text/vcard",
+        ["vcs"] = "text/x-vCalendar",
+        ["vda"] = "application/vda",
+        ["vdo"] = "video/vdo",
+        ["vew"] = "application/groupwise",
+        ["vivo"] = "video/vivo",
+        ["viv"] = "video/vivo",
+        ["vmd"] = "application/vocaltec-media-desc",
+        ["vmf"] = "application/vocaltec-media-file",
+        ["voc"] = "audio/voc",
+        ["vos"] = "video/vosaic",
+        ["vox"] = "audio/voxware",
+        ["vqe"] = "audio/x-twinvq-plugin",
+        ["vqf"] = "audio/x-twinvq",
+        ["vql"] = "audio/x-twinvq-plugin",
+        ["vrml"] = "application/x-vrml",
+        ["vrt"] = "x-world/x-vrt",
+        ["vsd"] = "application/x-visio",
+        ["vst"] = "application/x-visio",
+        ["vsw"] = "application/x-visio",
+        ["w60"] = "application/wordperfect6.0",
+        ["w61"] = "application/wordperfect6.1",
+        ["w6w"] = "application/msword",
+        ["wav"] = "audio/wav",
+        ["wb1"] = "application/x-qpro",
+        ["wbmp"] = "image/vnd.wap.wbmp",
+        ["wcm"] = "application/vnd.ms-works",
+        ["wdb"] = "application/vnd.ms-works",
+        ["web"] = "application/vnd.xara",
+        ["webm"] = "video/webm",
+        ["wiz"] = "application/msword",
+        ["wk1"] = "application/x-123",
+        ["wks"] = "application/vnd.ms-works",
+        ["wmf"] = "windows/metafile",
+        ["wmlc"] = "application/vnd.wap.wmlc",
+        ["wmlsc"] = "application/vnd.wap.wmlscriptc",
+        ["wmls"] = "text/vnd.wap.wmlscript",
+        ["wml"] = "text/vnd.wap.wml",
+        ["wmp"] = "video/x-ms-wmp",
+        ["wmv"] = "video/x-ms-wmv",
+        ["wmx"] = "video/x-ms-wmx",
+        ["woff"] = "application/x-woff",
+        ["word"] = "application/msword",
+        ["wp5"] = "application/wordperfect",
+        ["wp6"] = "application/wordperfect",
+        ["wp"] = "application/wordperfect",
+        ["wpd"] = "application/wordperfect",
+        ["wps"] = "application/vnd.ms-works",
+        ["wq1"] = "application/x-lotus",
+        ["wri"] = "application/mswrite",
+        ["wrl"] = "application/x-world",
+        ["wrz"] = "model/vrml",
+        ["wsc"] = "text/scriplet",
+        ["wsdl"] = "text/xml",
+        ["wsrc"] = "application/x-wais-source",
+        ["wtk"] = "application/x-wintalk",
+        ["wvx"] = "video/x-ms-wvx",
+        ["x3d"] = "model/x3d+xml",
+        ["x3db"] = "model/x3d+fastinfoset",
+        ["x3dv"] = "model/x3d-vrml",
+        ["xaf"] = "x-world/x-vrml",
+        ["xaml"] = "application/xaml+xml",
+        ["xap"] = "application/x-silverlight-app",
+        ["xbap"] = "application/x-ms-xbap",
+        ["xbm"] = "image/x-xbitmap",
+        ["xdr"] = "video/x-amt-demorun",
+        ["xgz"] = "xgl/drawing",
+        ["xht"] = "application/xhtml+xml",
+        ["xhtml"] = "application/xhtml+xml",
+        ["xif"] = "image/vnd.xiff",
+        ["xla"] = "application/vnd.ms-excel",
+        ["xlam"] = "application/vnd.ms-excel.addin.macroEnabled.12",
+        ["xl"] = "application/excel",
+        ["xlb"] = "application/excel",
+        ["xlc"] = "application/excel",
+        ["xld"] = "application/excel",
+        ["xlk"] = "application/excel",
+        ["xll"] = "application/excel",
+        ["xlm"] = "application/excel",
+        ["xls"] = "application/vnd.ms-excel",
+        ["xlsb"] = "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
+        ["xlsm"] = "application/vnd.ms-excel.sheet.macroEnabled.12",
+        ["xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        ["xlt"] = "application/vnd.ms-excel",
+        ["xltm"] = "application/vnd.ms-excel.template.macroEnabled.12",
+        ["xltx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
+        ["xlv"] = "application/excel",
+        ["xlw"] = "application/excel",
+        ["xm"] = "audio/xm",
+        ["xml"] = "text/xml",
+        ["xmz"] = "xgl/movie",
+        ["xof"] = "x-world/x-vrml",
+        ["xpi"] = "application/x-xpinstall",
+        ["xpix"] = "application/x-vnd.ls-xpix",
+        ["xpm"] = "image/xpm",
+        ["xps"] = "application/vnd.ms-xpsdocument",
+        ["x-png"] = "image/png",
+        ["xsd"] = "text/xml",
+        ["xsl"] = "text/xml",
+        ["xslt"] = "text/xml",
+        ["xsr"] = "video/x-amt-showrun",
+        ["xwd"] = "image/x-xwd",
+        ["xyz"] = "chemical/x-pdb",
+        ["z"] = "application/x-compressed",
+        ["zip"] = "application/zip",
+        ["zsh"] = "text/x-script.zsh"
+    };
+}

+ 102 - 0
Source/QuestPDF/Qpdf/QpdfAPI.cs

@@ -0,0 +1,102 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace QuestPDF.Qpdf;
+
+class QpdfAPI
+{
+    public static string? GetQpdfVersion()
+    {
+        var ptr = API.qpdf_get_qpdf_version();
+        return Marshal.PtrToStringAnsi(ptr);
+    }
+    
+    public static void ExecuteJob(string jobJson)
+    {
+        QpdfNativeDependencyCompatibilityChecker.Test();
+        
+        // create StringBuilder that will store the error message
+        var error = new StringBuilder();
+        var errorHandle = GCHandle.Alloc(error);
+        var errorPtr = GCHandle.ToIntPtr(errorHandle);
+        
+        // create logger
+        var logger = API.qpdflogger_create();
+        API.qpdflogger_set_error(logger, 4, LoggingCallbackPointer, errorPtr); // 4 = custom logger
+        
+        // perform the job
+        var jobHandle = API.qpdfjob_init();
+        API.qpdfjob_set_logger(jobHandle, logger);
+        API.qpdfjob_initialize_from_json(jobHandle, jobJson);
+        API.qpdfjob_run(jobHandle);
+        API.qpdfjob_cleanup(jobHandle);
+        
+        // logger cleanup
+        API.qpdflogger_cleanup(logger);
+        
+        // check errors
+        var errorMessage = error.ToString();
+        
+        if (!string.IsNullOrEmpty(errorMessage))
+            throw new Exception(errorMessage);
+    }
+    
+    #region Logging
+    
+    private static int LoggingCallback(IntPtr data, int length, IntPtr udata)
+    {
+        var bytes = new byte[length];
+        Marshal.Copy(data, bytes, 0, length);
+
+        var handle = GCHandle.FromIntPtr(udata);
+        var stringBuilder = (StringBuilder)handle.Target;
+        stringBuilder?.Append(Encoding.ASCII.GetString(bytes));
+
+        return 0;
+    }
+    
+    private delegate int CallbackDelegate(IntPtr data, int length, IntPtr udata);
+    private static readonly CallbackDelegate LoggingCallbackDelegate = LoggingCallback;
+    private static readonly IntPtr LoggingCallbackPointer = Marshal.GetFunctionPointerForDelegate(LoggingCallbackDelegate);
+    
+    #endregion
+    
+    private static class API
+    {
+        const string LibraryName = "qpdf";
+        
+        /* GENERAL */
+        
+        [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern IntPtr qpdf_get_qpdf_version();
+    
+        /* JOBS */
+        
+        [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern IntPtr qpdfjob_init();
+    
+        [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void qpdfjob_cleanup(IntPtr jobHandle);
+    
+        [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int qpdfjob_initialize_from_json(IntPtr jobHandle, string json);
+
+        [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int qpdfjob_run(IntPtr jobHandle);
+        
+        /* LOGGING */
+        
+        [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = nameof(qpdflogger_create))]
+        public static extern IntPtr qpdflogger_create();
+
+        [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = nameof(qpdflogger_cleanup))]
+        public static extern void qpdflogger_cleanup(IntPtr loggerHandle);
+        
+        [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = nameof(qpdflogger_set_error))]
+        public static extern void qpdflogger_set_error(IntPtr loggerHandle, int destination, IntPtr callBackHandler, IntPtr udata);
+        
+        [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = nameof(qpdfjob_set_logger))]
+        public static extern void qpdfjob_set_logger(IntPtr jobHandle, IntPtr loggerHandle);
+    }
+}

+ 45 - 0
Source/QuestPDF/Qpdf/QpdfNativeDependencyCompatibilityChecker.cs

@@ -0,0 +1,45 @@
+using System;
+using QuestPDF.Helpers;
+
+namespace QuestPDF.Qpdf;
+
+internal static class QpdfNativeDependencyCompatibilityChecker
+{
+    private static NativeDependencyCompatibilityChecker Instance { get; } = new()
+    {
+        ExecuteNativeCode = ExecuteNativeCode,
+        ExceptionHint = GetHint
+    };
+    
+    public static void Test()
+    {
+        Instance.Test();
+    }
+    
+    private static void ExecuteNativeCode()
+    {
+        var qpdfVersion = QpdfAPI.GetQpdfVersion();
+        
+        if (string.IsNullOrEmpty(qpdfVersion))
+            throw new Exception();
+    }
+
+    private static string GetHint()
+    {
+        var platform = NativeDependencyProvider.GetRuntimePlatform();
+        
+        if (!platform.StartsWith("linux"))
+            return string.Empty;
+        
+        const string openSslHint = "Please also ensure that the OpenSSL library is installed on your system with version at least 3.0.0.";
+        
+        var command = platform switch
+        {
+            "linux-x64" or "linux-arm64" => "apt install openssl-bin gnutls-bin libjpeg-dev",
+            "linux-musl-x64" => "apk add openssl gnutls libjpeg-turbo",
+            _ => throw new NotSupportedException()
+        };
+        
+        return $"Installing additional dependencies may help. Likely command: '{command}'. {openSslHint}";
+    }
+}

+ 118 - 0
Source/QuestPDF/Qpdf/SimpleJsonSerializer.cs

@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace QuestPDF.Qpdf;
+
+class SimpleJsonPropertyNameAttribute(string name) : Attribute
+{
+    public string Name { get; } = name;
+}
+
+/// <summary>
+/// Never use in performance critical scenarios!
+/// </summary>
+class SimpleJsonSerializer
+{
+    public static string Serialize(object obj)
+    {
+        if (obj == null) 
+            return "null";
+
+        var type = obj.GetType();
+        var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
+        
+        var stringBuilder = new StringBuilder();
+        stringBuilder.Append('{');
+
+        foreach (var property in properties)
+        {
+            var value = property.GetValue(obj);
+            
+            if (value == default)
+                continue;
+            
+            var name = property.GetCustomAttribute<SimpleJsonPropertyNameAttribute>().Name;
+            stringBuilder.Append($"\"{name}\": {SerializeValue(value)}");
+            stringBuilder.Append(", ");
+        }
+        
+        if (properties.Length > 1) 
+            stringBuilder.Length -= 2;
+
+        stringBuilder.Append('}');
+        return stringBuilder.ToString();
+    }
+
+    private static string SerializeValue(object value)
+    {
+        if (value == null) 
+            return "null";
+        
+        if (value is string text) 
+            return $"\"{EscapeStringForJson(text)}\"";
+        
+        if (value is bool)
+            return value.ToString().ToLower();
+        
+        if (value is IEnumerable<object> enumerable)
+        {
+            var stringBuilder = new StringBuilder();
+            stringBuilder.Append('[');
+
+            foreach (var item in enumerable)
+                stringBuilder.Append($"{SerializeValue(item)}, ");
+            
+            // remove trailing comma and space
+            if (enumerable.Any()) 
+                stringBuilder.Length -= 2;
+            
+            stringBuilder.Append(']');
+            return stringBuilder.ToString();
+        }
+        
+        if (!value.GetType().IsPrimitive)
+            return Serialize(value);
+        
+        return value.ToString();
+    }
+
+    private static string EscapeStringForJson(string input)
+    {
+        if (string.IsNullOrEmpty(input))
+            return input;
+
+        var builder = new StringBuilder(input.Length);
+
+        foreach (char c in input)
+        {
+            if (c == '\\')
+                builder.Append("\\\\");
+            
+            else if (c == '"')
+                builder.Append("\\\"");
+            
+            else if (c == '\b')
+                builder.Append("\\b");
+            
+            else if (c == '\f')
+                builder.Append("\\f");
+            
+            else if (c == '\n')
+                builder.Append("\\n");
+            
+            else if (c == '\r')
+                builder.Append("\\r");
+            
+            else if (c == '\t')
+                builder.Append("\\t");
+            
+            else
+                builder.Append(c);
+        }
+
+        return builder.ToString();
+    }
+}

+ 1 - 1
Source/QuestPDF/QuestPDF.csproj

@@ -3,7 +3,7 @@
         <Authors>MarcinZiabek</Authors>
         <Company>CodeFlint</Company>
         <PackageId>QuestPDF</PackageId>
-        <Version>2024.10.4</Version>
+        <Version>2024.12.0-rc3</Version>
         <PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API. Easily generate PDF reports, invoices, exports, etc.</PackageDescription>
         <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
         <LangVersion>12</LangVersion>

+ 148 - 0
Source/QuestPDF/Resources/Documentation.xml

@@ -575,4 +575,152 @@
             Instead of replicating the same header design across various documents, a single component can be created and referenced wherever needed.
         </example>
     </doc>
+
+    <!-- DOCUMENT OPERATIONS -->
+
+    <doc for="documentOperation.pageSelector">
+        <remarks>
+            <list type="table">
+                <listheader>
+                    <term>Syntax</term>
+                    <description>Description</description>
+                </listheader>
+                <item>
+                    <term>1, 2, 3</term>
+                    <description>Plain numbers indicate pages numbered from the start</description>
+                </item>
+                <item>
+                    <term>r1, r2</term>
+                    <description>Numbers with 'r' prefix count from the end (r1 = last page)</description>
+                </item>
+                <item>
+                    <term>z</term>
+                    <description>Represents the last page (equivalent to r1)</description>
+                </item>
+                <item>
+                    <term>1-5</term>
+                    <description>Dash-separated ranges are inclusive</description>
+                </item>
+                <item>
+                    <term>5-1</term>
+                    <description>Reversed ranges list pages in descending order</description>
+                </item>
+                <item>
+                    <term>x1-3</term>
+                    <description>Excludes specified pages from previous range</description>
+                </item>
+                <item>
+                    <term>:odd</term>
+                    <description>Selects odd-positioned pages from the resulting page-set</description>
+                </item>
+                <item>
+                    <term>:even</term>
+                    <description>Selects even-positioned pages from the resulting page-set</description>
+                </item>
+            </list>
+        </remarks>
+        
+        <example>
+            <list type="table">
+                <listheader>
+                    <term>Expression</term>
+                    <description>Result</description>
+                </listheader>
+                <item>
+                    <term>1,6,4</term>
+                    <description>pages 1, 6, and 4 in that order</description>
+                </item>
+                <item>
+                    <term>3-7</term>
+                    <description>pages 3 through 7 inclusive</description>
+                </item>
+                <item>
+                    <term>7-3</term>
+                    <description>pages 7, 6, 5, 4, and 3 in that order</description>
+                </item>
+                <item>
+                    <term>1-z</term>
+                    <description>all pages in order</description>
+                </item>
+                <item>
+                    <term>z-1</term>
+                    <description>all pages in reverse order</description>
+                </item>
+                <item>
+                    <term>r3-r1</term>
+                    <description>the last three pages of the document</description>
+                </item>
+                <item>
+                    <term>r1-r3</term>
+                    <description>the last three pages of the document in reverse order</description>
+                </item>
+                <item>
+                    <term>1-20:even</term>
+                    <description>even pages from 2 to 20</description>
+                </item>
+                <item>
+                    <term>5,7-9,12</term>
+                    <description>pages 5, 7, 8, 9, and 12</description>
+                </item>
+                <item>
+                    <term>5,7-9,12:odd</term>
+                    <description>pages 5, 8, and 12 (pages in odd positions from the original set of 5, 7, 8, 9, 12)</description>
+                </item>
+                <item>
+                    <term>5,7-9,12:even</term>
+                    <description>pages 7 and 9 (pages in even positions from the original set of 5, 7, 8, 9, 12)</description>
+                </item>
+                <item>
+                    <term>1-10,x3-4</term>
+                    <description>pages 1 through 10 except pages 3 and 4 (1, 2, and 5 through 10)</description>
+                </item>
+                <item>
+                    <term>4-10,x7-9,12-8,xr5</term>
+                    <description>In a 15-page file: pages 4, 5, 6, 10, 12, 10, 9, and 8 in that order (pages 4 through 10 except 7 through 9, followed by 12 through 8 descending, except 11 which is the fifth page from the end)</description>
+                </item>
+            </list>
+        </example>
+    </doc>
+    
+    <doc for="documentOperation.encryption.allow.annotation">
+        <summary>
+            Specifies whether the user is permitted to add signatures and annotations to the document.
+        </summary>
+    </doc>
+
+    <doc for="documentOperation.encryption.allow.contentExtraction">
+        <summary>
+            Specifies whether the user is allowed to copy text and graphics from the document.
+        </summary>
+    </doc>
+
+    <doc for="documentOperation.encryption.allow.modification">
+        <summary>
+            Specifies whether the user is permitted to insert, rotate, or delete pages within the document.
+        </summary>
+    </doc>
+
+    <doc for="documentOperation.encryption.allow.printing">
+        <summary>
+            Specifies whether the user can print the document.
+        </summary>
+    </doc>
+
+    <doc for="documentOperation.encryption.allow.assembly">
+        <summary>
+            Specifies whether the user is permitted to insert, rotate, or delete pages within the document.
+        </summary>
+    </doc>
+
+    <doc for="documentOperation.encryption.allow.fillingForms">
+        <summary>
+            Specifies whether the user is allowed to fill out existing form fields in the document.
+        </summary>
+    </doc>
+
+    <doc for="documentOperation.encryption.encryptMetadata">
+        <summary>
+            Determines whether the document's metadata is included in encryption.
+        </summary>
+    </doc>
 </documentation>

+ 202 - 0
Source/QuestPDF/Resources/ExternalDependencyLicenses/qpdf.txt

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 13 - 29
Source/QuestPDF/Resources/ReleaseNotes.txt

@@ -1,29 +1,13 @@
-Version 2024.10.0
-- This version includes integration with the new QuestPDF Companion application.
-- Updated the Skia dependency to version m130.
-- Improved NuGet package determinism.
-- Enhanced rendering of underline text decoration for certain fonts.
-
-
-Version 2024.10.1
-- Engine: Improved overflow annotation and layout algorithm for better handling of content overflow.
-- Companion App: Enhanced message clarity regarding compatibility with merged documents.
-- Companion App: Improved support for documents with multiple page configurations.
-- Font discovery: Fixed file-access related exceptions.
-- Font discovery: Improved default file path for font discovery.
-
-
-Version 2024.10.2
-- Fixed parsing of color values from both strings and uint.
-- Fixed a rare memory leak issue occurring on certain CPU architectures.
-
-
-Version 2024.10.3
-- Improved the layout debugging engine by fixing rare exceptions and increasing stability.
-- Updated the Skia dependency to version m131.
-- Changed conformance level to PDF/A-3b.
-- Strong-named the official nuget package.
-
-
-Version 2024.10.4
-- Fixed: The text underline decoration was not rendering correctly.
+Version 2024.12.0
+
+This release introduces several long-awaited features releated to document operations such as:
+- Assemble: Rearrange, select, and remove specific pages within a document.
+- Merge: Combine multiple PDF files into a single document.
+- Add Attachments: Embed additional files (such as text or images) as attachments within the PDF.
+- Underlay and Overlay: Apply other PDF as a background (underlay) or foreground (overlay) to another, adding layered content.
+- Linearize: Optimize files for fast web viewing, allowing compatible PDF readers to display the document before it’s fully downloaded.
+- Encrypt with Access Restrictions: Secure PDFs with user and owner passwords, applying access controls (like permissions for printing, filling forms, extracting or modifying content).
+
+These features are built using the qpdf library, available under the "Apache-2.0" license.
+The qpdf library is available at: https://github.com/qpdf/qpdf
+We extend our thanks to the authors of qpdf for their contributions to the open-source community.

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


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


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


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


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


BIN
Source/QuestPDF/Runtimes/win-x64/native/libgcc_s_seh-1.dll


BIN
Source/QuestPDF/Runtimes/win-x64/native/libstdc++-6.dll


BIN
Source/QuestPDF/Runtimes/win-x64/native/libwinpthread-1.dll


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


BIN
Source/QuestPDF/Runtimes/win-x86/native/libgcc_s_dw2-1.dll


BIN
Source/QuestPDF/Runtimes/win-x86/native/libstdc++-6.dll


BIN
Source/QuestPDF/Runtimes/win-x86/native/libwinpthread-1.dll


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


+ 2 - 2
Source/QuestPDF/Settings.cs

@@ -1,7 +1,7 @@
 using System;
 using System.Collections.Generic;
-using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
 
 namespace QuestPDF
 {
@@ -65,7 +65,7 @@ namespace QuestPDF
         
         static Settings()
         {
-            NativeDependencyCompatibilityChecker.Test();
+            SkNativeDependencyCompatibilityChecker.Test();
         }
     }
 }

+ 16 - 79
Source/QuestPDF/Skia/SkNativeDependencyCompatibilityChecker.cs

@@ -1,97 +1,34 @@
 using System;
 using System.Linq;
 using System.Runtime.InteropServices;
+using QuestPDF.Helpers;
 
 namespace QuestPDF.Skia;
 
 internal static class SkNativeDependencyCompatibilityChecker
 {
-    private static bool IsCompatibilityChecked = false;
-        
+    private static NativeDependencyCompatibilityChecker Instance { get; } = new()
+    {
+        ExecuteNativeCode = ExecuteNativeCode
+    };
+    
     public static void Test()
     {
-        const string exceptionBaseMessage = "The QuestPDF library has encountered an issue while loading one of its dependencies.";
-        const string paragraph = "\n\n";
-        
-        if (IsCompatibilityChecked)
-            return;
-            
-        // test with dotnet-based mechanism where native files are provided
-        // in the "runtimes/{rid}/native" folder on Core, or by the targets file on .NET Framework
-        var innerException = CheckIfExceptionIsThrownWhenLoadingNativeDependencies();
-
-        if (innerException == null)
-        {
-            IsCompatibilityChecked = true;
-            return;
-        }
-
-        if (!SkNativeDependencyProvider.IsCurrentPlatformSupported())
-            ThrowCompatibilityException(innerException);
-        
-        // detect platform, copy appropriate native files and test compatibility again
-        SkNativeDependencyProvider.EnsureNativeFileAvailability();
-        
-        innerException = CheckIfExceptionIsThrownWhenLoadingNativeDependencies();
-
-        if (innerException == null)
-        {
-            IsCompatibilityChecked = true;
-            return;
-        }
-
-        ThrowCompatibilityException(innerException);
-        
-        static void ThrowCompatibilityException(Exception innerException)
-        {
-            var supportedRuntimes = string.Join(", ", SkNativeDependencyProvider.SupportedPlatforms);
-            var currentRuntime = SkNativeDependencyProvider.GetRuntimePlatform();
-            
-            var message = 
-                $"{exceptionBaseMessage}{paragraph}" +
-                "Your runtime is currently not supported by QuestPDF. " +
-                $"Currently supported runtimes are: {supportedRuntimes}. ";
-
-            if (SkNativeDependencyProvider.SupportedPlatforms.Contains(currentRuntime))
-            {
-                message += $"{paragraph}It appears that your current operating system distribution may be outdated. For optimal compatibility, please consider updating it to a more recent version.";
-            }
-            else
-            {
-                message += $"{paragraph}Your current runtime is detected as '{currentRuntime}'.";
-            }
-
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-                message += $"{paragraph}Please always set the 'Platform target' to either 'X86' or 'X64' in your startup project settings. Please do not use the 'Any CPU' option.";
-            
-            if (RuntimeInformation.ProcessArchitecture is Architecture.Arm)
-                message += $"{paragraph}Please consider setting the 'Platform target' property to 'Arm64' in your project settings.";
-            
-            throw new Exception(message, innerException);
-        }
+        Instance.Test();
     }
-    
-    private static Exception? CheckIfExceptionIsThrownWhenLoadingNativeDependencies()
+
+    private static void ExecuteNativeCode()
     {
-        try
-        {
-            var random = new Random();
+        var random = new Random();
             
-            var a = random.Next();
-            var b = random.Next();
+        var a = random.Next();
+        var b = random.Next();
         
-            var expected = a + b;
-            var returned = API.check_compatibility_by_calculating_sum(a, b);
+        var expected = a + b;
+        var returned = API.check_compatibility_by_calculating_sum(a, b);
         
-            if (expected != returned)
-                throw new Exception();
-
-            return null;
-        }
-        catch (Exception exception)
-        {
-            return exception;
-        }
+        if (expected != returned)
+            throw new Exception();
     }
     
     private static class API