Skip to content

Repository files navigation

ModernPDF

CI

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, and V=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

Repository layout

  • src\ModernPDF — main library
  • tests\ModernPDF.Tests — unit/integration tests
  • tests\ModernPDF.CorpusTests — external corpus interoperability tests
  • spec\ — spec mapping and corpus source documentation

Prerequisites

  • .NET SDK 10.0.x
  • PowerShell (pwsh) for corpus fixture sync scripts

Build and test

dotnet build ModernPDF.slnx
dotnet test --solution ModernPDF.slnx

dotnet test uses Microsoft.Testing.Platform via global.json, with xunit.v3 in test projects.

GitHub Actions

  • CI runs 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 NuGet reads the package version from src\ModernPDF\ModernPDF.csproj, verifies the v<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_KEY in repository secrets for NuGet publishing.

Samples

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\samples

Embedded TrueType fonts

PdfTextOptions 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"],
});

Image pages (JPEG/PNG)

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.

Shapes

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");

Redaction

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>).

Save cross-reference style

You can choose classic xref tables or stream-style xref output during save:

byte[] streamStylePdf = document.Save(
    new PdfSaveOptions
    {
        CrossReferenceStyle = PdfCrossReferenceStyle.Stream,
    });

Security profiles

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.

Detached signatures (MVP)

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

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.

Current limitations

  • 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 when id-kp-OCSPSigning is present and the responder certificate chains to the OCSP certificate issuer, and enforces signature-scoped /DSS /VRI evidence 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 ShapeId markers 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, and V=5 / R=6
  • explicit PdfSaveOptions.Security with PdfSaveMode.Incremental is supported only for documents opened from encrypted PDFs, and must match the opened security context (password/profile/permissions)

About

A modern .NET library for reading, writing, editing, and securing PDF documents.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages