ModernPDF is a managed, cross-platform .NET 10 PDF library built from first principles.
Current implemented scope includes:
- create/open/save PDF documents
- text extraction
- page/content editing
- JPEG/PNG image page authoring, replacement, and composition
- shape drawing (line, rectangle, rounded rectangle, circle, ellipse, polygon, arc/sector, and path) with stroke/fill, gradients, blending, transforms, clipping, and ID-based replace/remove
- hard/soft redaction APIs (literal and pattern-based)
- password security for Standard handler profiles
V=1 / R=2 / 40-bit RC4,V=2 / R=3 / 128-bit RC4,V=4 / R=4 / 128-bit AES, andV=5 / R=6 / 256-bit AES - detached digital signatures (callback-based CMS embedding with ByteRange patching)
- TrueType font embedding for generated/replaced text with automatic subsetting and OpenType shaping
- corpus-based interoperability/hardening test harness
src\ModernPDF— main librarytests\ModernPDF.Tests— unit/integration teststests\ModernPDF.CorpusTests— external corpus interoperability testsspec\— spec mapping and corpus source documentation
- .NET SDK
10.0.x - PowerShell (
pwsh) for corpus fixture sync scripts
dotnet build ModernPDF.slnx
dotnet test --solution ModernPDF.slnxdotnet test uses Microsoft.Testing.Platform via global.json, with xunit.v3 in test projects.
CIruns build + test on pushes, pull requests, and manual runs; coverage summary is always published, and coverage artifacts are uploaded only for pull requests (7-day retention).Release NuGetreads the package version fromsrc\ModernPDF\ModernPDF.csproj, verifies thev<version>tag and NuGet version do not already exist, then tags, pushes to NuGet, and creates a GitHub release with autogenerated change notes.- Configure
NUGET_API_KEYin repository secrets for NuGet publishing.
The repository includes a runnable sample console app at samples\ModernPDF.Samples built with System.CommandLine.
On Windows, sample commands configure PdfDocument.DefaultTextOptions automatically to use a system TrueType font when available.
# list commands and options
dotnet run --project samples\ModernPDF.Samples -- --help
# run individual samples
dotnet run --project samples\ModernPDF.Samples -- create
dotnet run --project samples\ModernPDF.Samples -- extract --output .\artifacts\samples
dotnet run --project samples\ModernPDF.Samples -- redact --output .\artifacts\samples
dotnet run --project samples\ModernPDF.Samples -- secure --output .\artifacts\samplesPdfTextOptions supports embedding a TrueType font file, fallback font chains (including mixed fallback runs within one line), OpenType shaping (including surrogate pairs), paragraph wrapping, justification, and subsetting glyphs by default.
You can configure this once per document via DefaultTextOptions and still override per call:
PdfDocument document = PdfDocument.Create();
document.DefaultTextOptions = new PdfTextOptions
{
TrueTypeFontPath = @"C:\fonts\MyFont.ttf",
SubsetFont = true, // default
};
document.AddTextPage("Uses the document default font");
document.AddTextPage(
"Per-call override still works",
textOptions: new PdfTextOptions
{
TrueTypeFontPath = @"C:\fonts\AnotherFont.ttf",
FallbackTrueTypeFontPaths = [@"C:\fonts\Fallback.ttf"],
SubsetFont = false,
MaxWidth = 420,
LineHeightMultiplier = 1.4,
Alignment = PdfTextAlignment.Justify,
Direction = PdfTextDirection.Auto,
WritingMode = PdfWritingMode.Horizontal,
EnableHyphenation = true,
});Rich spans in a single paragraph (including embedded TrueType rendering):
document.AddRichTextPage(
[
new PdfTextSpan { Text = "Normal " },
new PdfTextSpan { Text = "Big", FontSize = 24 },
new PdfTextSpan { Text = " text", FontSize = 12 },
],
textOptions: new PdfTextOptions
{
TrueTypeFontPath = @"C:\fonts\MyFont.ttf",
FallbackTrueTypeFontPaths = [@"C:\fonts\Fallback.ttf"],
});You can add a JPEG or PNG image as a page, replace a page with an image, or append multiple images onto a page:
PdfDocument document = PdfDocument.Create();
document.AddImagePage(@"C:\images\cover.jpg");
document.ReplacePageImage(0, @"C:\images\updated-cover.jpg");
document.AddPageImage(0, @"C:\images\badge.jpg", new PdfImageOptions { X = 24, Y = 24, Width = 64, Height = 64 });AddImagePage(...) creates an image-only page. Use AddPageImage(...) to compose multiple images on a page while preserving existing content.
You can append vector shapes to existing pages without replacing current content:
PdfDocument document = PdfDocument.Create();
document.AddTextPage("Shape overlay");
document.AddPageLine(0, 24, 24, 300, 24);
document.AddPageRectangle(
0,
24,
48,
120,
60,
new PdfShapeOptions
{
StrokeColor = new PdfRgbColor(0.1, 0.1, 0.1),
FillColor = new PdfRgbColor(0.8, 0.9, 1.0),
StrokeWidth = 2,
StrokeLineCap = PdfShapeLineCap.Round,
StrokeLineJoin = PdfShapeLineJoin.Round,
StrokeDashPattern = new PdfShapeDashPattern { Segments = [6, 3] },
});
document.AddPageCircle(0, 220, 78, 24, new PdfShapeOptions { FillColor = new PdfRgbColor(1, 0.8, 0.8) });
document.AddPageEllipse(0, 220, 140, 48, 20);
document.AddPagePolygon(0, [new PdfShapePoint(40, 160), new PdfShapePoint(96, 196), new PdfShapePoint(152, 160)]);
document.AddPagePath(
0,
[
new PdfPathMoveTo(200, 160),
new PdfPathLineTo(260, 160),
new PdfPathCurveTo(280, 160, 280, 210, 260, 210),
new PdfPathClosePath(),
],
new PdfShapeOptions { FillColor = new PdfRgbColor(0.9, 0.8, 0.3), StrokeColor = null, FillRule = PdfShapeFillRule.EvenOdd });
// gradients, transparency, and blend mode
document.AddPageRoundedRectangle(
0,
24,
220,
180,
80,
16,
16,
new PdfShapeOptions
{
StrokeOpacity = 0.6,
FillOpacity = 0.35,
BlendMode = PdfBlendMode.Multiply,
FillLinearGradient = new PdfShapeLinearGradient
{
StartX = 24,
StartY = 220,
EndX = 204,
EndY = 300,
StartColor = new PdfRgbColor(1, 0.6, 0.2),
EndColor = new PdfRgbColor(0.2, 0.4, 1),
},
});
// clipping and transforms
document.AddPagePathTransformed(
0,
[
new PdfPathMoveTo(0, 0),
new PdfPathLineTo(40, 0),
new PdfPathLineTo(40, 40),
new PdfPathClosePath(),
],
PdfShapeTransform.RotateAt(25, 120, 120),
new PdfShapeOptions { FillColor = new PdfRgbColor(0.2, 0.7, 0.3), StrokeColor = null });
document.AddPagePathClipped(
0,
[
new PdfPathMoveTo(260, 60),
new PdfPathLineTo(340, 60),
new PdfPathLineTo(340, 140),
new PdfPathLineTo(260, 140),
new PdfPathClosePath(),
],
[
new PdfPathMoveTo(240, 40),
new PdfPathLineTo(360, 160),
]);
// shape IDs for select/replace/remove
document.AddPageSector(
0,
320,
240,
36,
20,
140,
new PdfShapeOptions { ShapeId = "badge-sector", FillColor = new PdfRgbColor(0.9, 0.3, 0.3), StrokeColor = null });
IReadOnlyList<string> shapeIds = document.GetPageShapeIds(0);
document.ReplacePageShape(
0,
"badge-sector",
[
new PdfPathMoveTo(300, 220),
new PdfPathLineTo(340, 220),
new PdfPathLineTo(340, 260),
new PdfPathClosePath(),
],
new PdfShapeOptions { FillColor = new PdfRgbColor(0.3, 0.5, 0.9), StrokeColor = null });
document.RemovePageShape(0, "badge-sector");Use hard redaction when data must be permanently hidden, and soft redaction when data should be transformed:
PdfDocument document = PdfDocument.Create();
document.AddTextPage("SSN: 111-22-3333\nPhone: 555-123-4567");
// hard: removes underlying text and overlays opaque blackout rectangles
document.HardRedactText("111-22-3333");
// soft: keep only a suffix and box the hidden span (no replacement text needed)
document.SoftRedactText(
@"\b(\d{3})-(\d{3})-(\d{4})\b",
_ => PdfSoftRedactionDirective.KeepSuffix(3));
// explicit hard bounds redaction
document.HardRedactBounds(0, 70, 706, 140, 18);
// location-aware redaction: pick exact occurrence, then hard-redact only that handle
IReadOnlyList<PdfTextMatch> matches = document.FindText(@"\bSofia\b");
PdfTextMatch selected = matches.First(match => match.PageIndex == 1 && match.X > 120);
document.HardRedactText([selected]);Soft redaction keeps visual text flow stable by compensating glyph advance after hidden spans are removed, so surrounding content does not shift horizontally.
For geometry-driven workflows, ExtractTextRegions() returns text spans with page coordinates, and FindText(...) returns anchored matches that can be passed directly to HardRedactText(IReadOnlyList<PdfTextMatch>).
You can choose classic xref tables or stream-style xref output during save:
byte[] streamStylePdf = document.Save(
new PdfSaveOptions
{
CrossReferenceStyle = PdfCrossReferenceStyle.Stream,
});PdfSecurityOptions.Profile selects the Standard security handler profile used when saving:
byte[] encryptedPdf = document.Save(
new PdfSaveOptions
{
Security = new PdfSecurityOptions
{
UserPassword = "pw",
Profile = PdfSecurityProfile.Standard128BitAes,
Permissions = PdfPermissions.Print | PdfPermissions.Copy | PdfPermissions.FillForms,
},
});When you open an encrypted PDF with a password, subsequent Save() calls preserve encryption automatically, including append-only incremental saves.
The signature API is callback-based: ModernPDF computes and patches /ByteRange, provides the exact signed payload bytes, and embeds returned CMS bytes into /Contents.
PdfDocument document = PdfDocument.Create();
document.AddTextPage("Signed content");
byte[] signedBytes = document.SaveSignedDetached(
payloadToSign =>
{
// Replace with your CMS/PKCS#7 detached signer implementation.
return MyCmsSigner.SignDetached(payloadToSign.Span);
},
new PdfSignatureOptions
{
ContentsByteLength = 8192,
Reason = "Approval",
});You can call SaveSignedDetached(...) again on an opened signed document to append additional detached signatures incrementally.
Validate detached signatures (CMS/PKCS#7):
PdfDocument signed = PdfDocument.Open(signedBytes);
IReadOnlyList<PdfDetachedSignatureValidationResult> results =
signed.ValidateDetachedSignatures(
new PdfDetachedSignatureValidationOptions
{
VerifyCertificateChain = true,
RequireSigningTime = true,
RequireRevocationStatus = true,
RevocationCheckMode = PdfRevocationCheckMode.Online,
RequiredCertificatePolicyOids = ["1.2.3.4.5"],
ValidationTime = DateTimeOffset.UtcNow,
});Each PdfDetachedSignatureValidationResult reports cryptographic validity and trust diagnostics separately (CryptographicallyValid, TrustChecksPassed, chain/revocation/signing-time/policy outcomes, and diagnostic messages).
Corpus tests are opt-in and use fixtures downloaded from pinned external source commits with SHA-256 verification. The full corpus suite includes stream-writer round-trip compatibility tests for known open fixtures.
Smoke corpus:
pwsh tests\ModernPDF.CorpusTests\scripts\sync-fixtures.ps1 -Profile smoke
$env:MODERNPDF_RUN_CORPUS = "1"
dotnet test tests\ModernPDF.CorpusTests\ModernPDF.CorpusTests.csproj --filter "Category=Smoke"Full corpus:
pwsh tests\ModernPDF.CorpusTests\scripts\sync-fixtures.ps1 -Profile full
$env:MODERNPDF_RUN_CORPUS = "1"
dotnet test tests\ModernPDF.CorpusTests\ModernPDF.CorpusTests.csproj --filter "Category=Full"See spec\corpus-sources.md for source commits, license notes, and corpus policy.
- signature validation currently supports CMS subfilters
/adbe.pkcs7.detached,/ETSI.CAdES.detached,/adbe.pkcs7.sha1, and/ETSI.RFC3161 - revocation validation supports online retrieval (
RevocationCheckMode = PdfRevocationCheckMode.Online) and offline DSS OCSP/CRL evidence when embedded in/DSS; offline OCSP validation includes delegated responders whenid-kp-OCSPSigningis present and the responder certificate chains to the OCSP certificate issuer, and enforces signature-scoped/DSS /VRIevidence matching; deterministic offline mode remains the default (Offline) - image APIs currently support JPEG and PNG input, including alpha-channel PNG via soft masks (indexed-color and interlaced PNG remain unsupported)
- shape APIs include gradients, blending, opacity, transforms, clipping, and ID-based replace/remove, but replacement/removal currently targets shapes created with
ShapeIdmarkers rather than arbitrary pre-existing vector operators - security support is intentionally limited to Standard handler profiles
V=1 / R=2,V=2 / R=3,V=4 / R=4, andV=5 / R=6 - explicit
PdfSaveOptions.SecuritywithPdfSaveMode.Incrementalis supported only for documents opened from encrypted PDFs, and must match the opened security context (password/profile/permissions)