๐ 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.
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")
| Property | Description |
|---|---|
FileName | Name shown in the Attachments panel |
MimeType | MIME type (e.g. text/csv, application/xml) |
Data | Raw bytes of the file to embed |
Relationship | Source, Data, Alternative, Supplement, Schema, EncryptedPayload, or Unspecified |
Description | Human-readable description (optional) |
CreationDate | Embedded file creation date (optional) |
ModificationDate | Embedded file modification date (optional) |
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)
โ๏ธ 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.
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)
| Property | Description |
|---|---|
Certificate | X.509 certificate with private key (.pfx) |
Reason | Why the document was signed |
Location | Physical or logical signing location |
ContactInfo | Signer's contact (email, phone) |
SignerName | Display name (defaults to certificate CN) |
PageIndex | Zero-based page for the visible signature field |
X, Y, Width, Height | Position and size of signature rectangle (points) |
FieldName | Unique 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.
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")
| Property | Description |
|---|---|
StartPageIndex | Zero-based first page (inclusive) for a leaf node |
EndPageIndex | Zero-based last page (inclusive) for a leaf node |
Metadata | Key-value dictionary serialized as DPM (ยง14.12) |
RecordName | Optional name for this part's record |
Children | Child 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.
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