๐Ÿ’Ž Bonus Features

Hidden gems โ€” file attachments, digital signatures, document parts, and PDF version control.

๐Ÿ“Ž File Attachments

Embed any file inside a PDF using associated files (ISO 32000-2 ยง14.13). Readers such as Adobe Acrobat display these in the Attachments panel. Ideal for bundling source data, XML, spreadsheets, or alternative representations alongside the visual document.

Attach a CSV file
using ObviousPDF;
using ObviousPDF.Accessibility;
using ObviousPDF.Fonts;
using System.IO;

var doc = new PdfDocument();
doc.Info.Title = "Report with Attachment";
doc.Language = "en-US";
doc.DisplayDocTitle = true;

var root = doc.EnableTaggedPdf();
var page = doc.AddPage();

var h1 = root.AddChild(StructureType.H1);
page.AddTaggedText(h1, "Sales Report โ€” Q4 2025", 72, 720,
    new PdfTextOptions { Font = StandardFont.HelveticaBold, FontSize = 20 });

var p = root.AddChild(StructureType.P);
page.AddTaggedText(p,
    "This PDF has an embedded CSV file as an associated file.",
    72, 690, new PdfTextOptions { FontSize = 12 });

// Embed a CSV file (PDF 2.0 / PDF/A-3)
var csvData = File.ReadAllBytes("sales.csv");
doc.AddAssociatedFile(
    "sales.csv", "text/csv", csvData,
    PdfAssociatedFileRelationship.Data,
    "Raw sales data for Q4 2025");

doc.Save("report_with_attachment.pdf");
Imports ObviousPDF
Imports ObviousPDF.Accessibility
Imports ObviousPDF.Fonts
Imports System.IO

Dim doc As New PdfDocument()
doc.Info.Title = "Report with Attachment"
doc.Language = "en-US"
doc.DisplayDocTitle = True

Dim root = doc.EnableTaggedPdf()
Dim page = doc.AddPage()

Dim h1 = root.AddChild(StructureType.H1)
page.AddTaggedText(h1, "Sales Report โ€” Q4 2025", 72, 720,
    New PdfTextOptions With { .Font = StandardFont.HelveticaBold, .FontSize = 20 })

Dim p = root.AddChild(StructureType.P)
page.AddTaggedText(p,
    "This PDF has an embedded CSV file as an associated file.",
    72, 690, New PdfTextOptions With { .FontSize = 12 })

' Embed a CSV file (PDF 2.0 / PDF/A-3)
Dim csvData As Byte() = File.ReadAllBytes("sales.csv")
doc.AddAssociatedFile(
    "sales.csv", "text/csv", csvData,
    PdfAssociatedFileRelationship.Data,
    "Raw sales data for Q4 2025")

doc.Save("report_with_attachment.pdf")
open ObviousPDF
open ObviousPDF.Accessibility
open ObviousPDF.Fonts
open System.IO

let doc = PdfDocument()
doc.Info.Title <- "Report with Attachment"
doc.Language <- "en-US"
doc.DisplayDocTitle <- true

let root = doc.EnableTaggedPdf()
let page = doc.AddPage()

let h1 = root.AddChild(StructureType.H1)
page.AddTaggedText(h1, "Sales Report โ€” Q4 2025", 72.0, 720.0,
    PdfTextOptions(Font = StandardFont.HelveticaBold, FontSize = 20.0))

let p = root.AddChild(StructureType.P)
page.AddTaggedText(p,
    "This PDF has an embedded CSV file as an associated file.",
    72.0, 690.0, PdfTextOptions(FontSize = 12.0))

// Embed a CSV file (PDF 2.0 / PDF/A-3)
let csvData = File.ReadAllBytes("sales.csv")
doc.AddAssociatedFile(
    "sales.csv", "text/csv", csvData,
    PdfAssociatedFileRelationship.Data,
    "Raw sales data for Q4 2025")

doc.Save("report_with_attachment.pdf")
$doc = [ObviousPDF.PdfDocument]::new()
$doc.Info.Title = "Report with Attachment"
$page = $doc.AddPage()
$page.AddText("Sales Report - Q4 2025", 72, 720,
    [ObviousPDF.PdfTextOptions]@{ FontSize = 20 })

# Embed a CSV file as an associated file
$csvData = [System.IO.File]::ReadAllBytes("sales.csv")
$doc.AddAssociatedFile(
    "sales.csv",
    "text/csv",
    $csvData,
    [ObviousPDF.PdfAssociatedFileRelationship]::Data,
    "Raw sales data for Q4 2025")

$doc.Save("report_with_attachment.pdf")

๐Ÿ“‹ Attachment Details

Open the resulting PDF in Adobe Acrobat or another compliant viewer and check the Attachments panel โ€” you'll see sales.csv listed with its description.

PropertyDescription
FileNameName shown in the Attachments panel
MimeTypeMIME type (e.g. text/csv, application/xml)
DataRaw bytes of the file to embed
RelationshipSource, Data, Alternative, Supplement, Schema, EncryptedPayload, or Unspecified
DescriptionHuman-readable description (optional)
CreationDateEmbedded file creation date (optional)
ModificationDateEmbedded file modification date (optional)
Attach with PdfAssociatedFile object
using ObviousPDF;
using System.IO;

// For more control, create the object directly
var xmlData = File.ReadAllBytes("source.xml");
var af = new PdfAssociatedFile(
    "source.xml", "application/xml", xmlData)
{
    Relationship = PdfAssociatedFileRelationship.Source,
    Description  = "Original XML source data",
    CreationDate = new DateTime(2025, 6, 15),
    ModificationDate = DateTime.UtcNow
};
doc.AddAssociatedFile(af);
Imports ObviousPDF
Imports System.IO

' For more control, create the object directly
Dim xmlData = File.ReadAllBytes("source.xml")
Dim af As New PdfAssociatedFile(
    "source.xml", "application/xml", xmlData) With {
    .Relationship = PdfAssociatedFileRelationship.Source,
    .Description  = "Original XML source data",
    .CreationDate = New DateTime(2025, 6, 15),
    .ModificationDate = DateTime.UtcNow
}
doc.AddAssociatedFile(af)
open ObviousPDF
open System.IO

// For more control, create the object directly
let xmlData = File.ReadAllBytes("source.xml")
let af = PdfAssociatedFile(
    "source.xml", "application/xml", xmlData,
    Relationship = PdfAssociatedFileRelationship.Source,
    Description  = "Original XML source data",
    CreationDate = DateTime(2025, 6, 15),
    ModificationDate = DateTime.UtcNow)
doc.AddAssociatedFile(af)
# For more control, create the object directly
$xmlData = [System.IO.File]::ReadAllBytes("source.xml")
$af = [ObviousPDF.PdfAssociatedFile]::new(
    "source.xml", "application/xml", $xmlData)
$af.Relationship = [ObviousPDF.PdfAssociatedFileRelationship]::Source
$af.Description  = "Original XML source data"
$af.CreationDate = [datetime]::new(2025, 6, 15)
$af.ModificationDate = [datetime]::UtcNow
$doc.AddAssociatedFile($af)

๐Ÿ’ก PDF/A-3 Tip

Set doc.PdfAConformance = PdfAConformanceLevel.PdfA3B to create a PDF/A-3 document that allows embedded files of any format โ€” perfect for e-invoicing (ZUGFeRD / Factur-X) or archival workflows.

โœ๏ธ Digital Signatures

Sign PDFs with X.509 certificates for document integrity and non-repudiation. The signature field appears visually on the page and is verifiable in Adobe Acrobat and other readers.

Digitally sign a PDF
using ObviousPDF;
using ObviousPDF.Fonts;
using System.Security.Cryptography.X509Certificates;

var cert = new X509Certificate2(
    "signing-cert.pfx", "password");

var sig = new PdfDigitalSignature(cert)
{
    Reason      = "I approve this document",
    Location    = "New York, NY",
    ContactInfo = "jane@example.com",
    SignerName  = "Jane Smith",
    PageIndex   = 0,      // First page
    X = 72,  Y = 50,      // Bottom-left corner
    Width = 200, Height = 50,
    FieldName   = "ApprovalSig"
};

var doc = new PdfDocument();
var page = doc.AddPage();
page.AddText("Signed Contract", 72, 720,
    new PdfTextOptions { FontSize = 22 });

// Sign and save in one step
doc.Sign("signed_contract.pdf", sig);
Imports ObviousPDF
Imports ObviousPDF.Fonts
Imports System.Security.Cryptography.X509Certificates

Dim cert As New X509Certificate2(
    "signing-cert.pfx", "password")

Dim sig As New PdfDigitalSignature(cert) With {
    .Reason      = "I approve this document",
    .Location    = "New York, NY",
    .ContactInfo = "jane@example.com",
    .SignerName  = "Jane Smith",
    .PageIndex   = 0,      ' First page
    .X = 72,  .Y = 50,    ' Bottom-left corner
    .Width = 200, .Height = 50,
    .FieldName   = "ApprovalSig"
}

Dim doc As New PdfDocument()
Dim page = doc.AddPage()
page.AddText("Signed Contract", 72, 720,
    New PdfTextOptions With { .FontSize = 22 })

' Sign and save in one step
doc.Sign("signed_contract.pdf", sig)
open ObviousPDF
open ObviousPDF.Fonts
open System.Security.Cryptography.X509Certificates

let cert = X509Certificate2(
    "signing-cert.pfx", "password")

let sig = PdfDigitalSignature(cert,
    Reason      = "I approve this document",
    Location    = "New York, NY",
    ContactInfo = "jane@example.com",
    SignerName  = "Jane Smith",
    PageIndex   = 0,      // First page
    X = 72.0,  Y = 50.0,  // Bottom-left corner
    Width = 200.0, Height = 50.0,
    FieldName   = "ApprovalSig")

let doc = PdfDocument()
let page = doc.AddPage()
page.AddText("Signed Contract", 72.0, 720.0,
    PdfTextOptions(FontSize = 22.0))

// Sign and save in one step
doc.Sign("signed_contract.pdf", sig)
Add-Type -Path "ObviousPDF.dll"

$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
    "signing-cert.pfx", "password")

$sig = [ObviousPDF.PdfDigitalSignature]::new($cert)
$sig.Reason      = "I approve this document"
$sig.Location    = "New York, NY"
$sig.ContactInfo = "jane@example.com"
$sig.SignerName  = "Jane Smith"
$sig.PageIndex   = 0      # First page
$sig.X = 72;  $sig.Y = 50 # Bottom-left corner
$sig.Width = 200; $sig.Height = 50
$sig.FieldName   = "ApprovalSig"

$doc = [ObviousPDF.PdfDocument]::new()
$page = $doc.AddPage()
$page.AddText("Signed Contract", 72, 720,
    [ObviousPDF.PdfTextOptions]@{ FontSize = 22 })

# Sign and save in one step
$doc.Sign("signed_contract.pdf", $sig)

๐Ÿ“‹ Signature Properties

The signature is a PKCS #7 (CMS) detached signature embedded in the PDF. Open the signed file in Adobe Acrobat to see the blue signature banner.

PropertyDescription
CertificateX.509 certificate with private key (.pfx)
ReasonWhy the document was signed
LocationPhysical or logical signing location
ContactInfoSigner's contact (email, phone)
SignerNameDisplay name (defaults to certificate CN)
PageIndexZero-based page for the visible signature field
X, Y, Width, HeightPosition and size of signature rectangle (points)
FieldNameUnique field name (default: Signature1)

๐Ÿ“‚ Document Parts (VDP)

Organize a single PDF into logical sub-documents using document parts (ISO 32000-2 ยง14.12). This is used in variable-data printing workflows where one file contains many individual letters, invoices, or statements.

Document parts for mail merge
using ObviousPDF;

var doc = new PdfDocument();

// Create 6 pages (2 pages per recipient)
string[] recipients = { "Alice", "Bob", "Carol" };
foreach (var name in recipients)
{
    var p1 = doc.AddPage();
    p1.AddText($"Dear {name},", 72, 700);
    p1.AddText("Page 1 of your letter.", 72, 670);

    var p2 = doc.AddPage();
    p2.AddText($"Page 2 โ€” {name}", 72, 700);
}

// Create the document part hierarchy
var root = doc.CreateDocumentPartRoot();

var part1 = root.AddChild(0, 1); // Pages 0-1
part1.Metadata["Recipient"] = "Alice";
part1.Metadata["JobId"] = "INV-001";

var part2 = root.AddChild(2, 3); // Pages 2-3
part2.Metadata["Recipient"] = "Bob";
part2.Metadata["JobId"] = "INV-002";

var part3 = root.AddChild(4, 5); // Pages 4-5
part3.Metadata["Recipient"] = "Carol";
part3.Metadata["JobId"] = "INV-003";

doc.Save("mail_merge.pdf");
Imports ObviousPDF

Dim doc As New PdfDocument()

' Create 6 pages (2 pages per recipient)
Dim recipients() As String = {"Alice", "Bob", "Carol"}
For Each name In recipients
    Dim p1 = doc.AddPage()
    p1.AddText($"Dear {name},", 72, 700)
    p1.AddText("Page 1 of your letter.", 72, 670)

    Dim p2 = doc.AddPage()
    p2.AddText($"Page 2 โ€” {name}", 72, 700)
Next

' Create the document part hierarchy
Dim root = doc.CreateDocumentPartRoot()

Dim part1 = root.AddChild(0, 1) ' Pages 0-1
part1.Metadata("Recipient") = "Alice"
part1.Metadata("JobId") = "INV-001"

Dim part2 = root.AddChild(2, 3) ' Pages 2-3
part2.Metadata("Recipient") = "Bob"
part2.Metadata("JobId") = "INV-002"

Dim part3 = root.AddChild(4, 5) ' Pages 4-5
part3.Metadata("Recipient") = "Carol"
part3.Metadata("JobId") = "INV-003"

doc.Save("mail_merge.pdf")
open ObviousPDF

let doc = PdfDocument()

// Create 6 pages (2 pages per recipient)
let recipients = [| "Alice"; "Bob"; "Carol" |]
for name in recipients do
    let p1 = doc.AddPage()
    p1.AddText(sprintf "Dear %s," name, 72.0, 700.0)
    p1.AddText("Page 1 of your letter.", 72.0, 670.0)

    let p2 = doc.AddPage()
    p2.AddText(sprintf "Page 2 โ€” %s" name, 72.0, 700.0)

// Create the document part hierarchy
let root = doc.CreateDocumentPartRoot()

let part1 = root.AddChild(0, 1) // Pages 0-1
part1.Metadata.["Recipient"] <- "Alice"
part1.Metadata.["JobId"] <- "INV-001"

let part2 = root.AddChild(2, 3) // Pages 2-3
part2.Metadata.["Recipient"] <- "Bob"
part2.Metadata.["JobId"] <- "INV-002"

let part3 = root.AddChild(4, 5) // Pages 4-5
part3.Metadata.["Recipient"] <- "Carol"
part3.Metadata.["JobId"] <- "INV-003"

doc.Save("mail_merge.pdf")
$doc = [ObviousPDF.PdfDocument]::new()

# Create 6 pages (2 pages per recipient)
$recipients = @("Alice", "Bob", "Carol")
foreach ($name in $recipients) {
    $p1 = $doc.AddPage()
    $p1.AddText("Dear $name,", 72, 700)
    $p1.AddText("Page 1 of your letter.", 72, 670)

    $p2 = $doc.AddPage()
    $p2.AddText("Page 2 - $name", 72, 700)
}

# Create the document part hierarchy
$root = $doc.CreateDocumentPartRoot()

# Each child covers a contiguous page range
$part1 = $root.AddChild(0, 1) # Pages 0-1
$part1.Metadata["Recipient"] = "Alice"
$part1.Metadata["JobId"] = "INV-001"

$part2 = $root.AddChild(2, 3) # Pages 2-3
$part2.Metadata["Recipient"] = "Bob"
$part2.Metadata["JobId"] = "INV-002"

$part3 = $root.AddChild(4, 5) # Pages 4-5
$part3.Metadata["Recipient"] = "Carol"
$part3.Metadata["JobId"] = "INV-003"

$doc.Save("mail_merge.pdf")

๐Ÿ“‹ Document Parts

The document part hierarchy is written to the catalog as /DPartRoot. Print workflows can use the metadata to split, route, or post-process individual sections.

PropertyDescription
StartPageIndexZero-based first page (inclusive) for a leaf node
EndPageIndexZero-based last page (inclusive) for a leaf node
MetadataKey-value dictionary serialized as DPM (ยง14.12)
RecordNameOptional name for this part's record
ChildrenChild nodes (for intermediate/grouping nodes)

๐Ÿ”ข PDF Version Control

ObviousPDF automatically selects the minimum PDF version required by the features you use. You can also set a version floor โ€” the library will never write a lower version than what you specify, but will raise it if features demand it.

Explicit PDF version
using ObviousPDF;

var doc = new PdfDocument();

// Set a minimum version floor
doc.PdfVersion = "2.0";

var page = doc.AddPage();
page.AddText("This is a PDF 2.0 document.", 72, 720);

doc.Save("pdf20.pdf");
// File header: %PDF-2.0
Imports ObviousPDF

Dim doc As New PdfDocument()

' Set a minimum version floor
doc.PdfVersion = "2.0"

Dim page = doc.AddPage()
page.AddText("This is a PDF 2.0 document.", 72, 720)

doc.Save("pdf20.pdf")
' File header: %PDF-2.0
open ObviousPDF

let doc = PdfDocument()

// Set a minimum version floor
doc.PdfVersion <- "2.0"

let page = doc.AddPage()
page.AddText("This is a PDF 2.0 document.", 72.0, 720.0)

doc.Save("pdf20.pdf")
// File header: %PDF-2.0
$doc = [ObviousPDF.PdfDocument]::new()

# Set a minimum version floor
$doc.PdfVersion = "2.0"

$page = $doc.AddPage()
$page.AddText("This is a PDF 2.0 document.", 72, 720)

$doc.Save("pdf20.pdf")
# File header: %PDF-2.0

๐Ÿ“‹ Auto-Detection Rules

When PdfVersion is null (default), the version is chosen automatically:

  • PDF 1.5 โ€” cross-reference streams or object streams
  • PDF 1.7 โ€” traditional cross-reference tables (baseline)
  • PDF 2.0 โ€” associated files, document parts, THead/TBody/TFoot structures, pronunciation hints, AES-256 encryption

Setting PdfVersion acts as a floor. For example, setting "1.7" while using associated files still produces %PDF-2.0.