DEV Community

eSeGeCe
eSeGeCe

Posted on

Create, Fill, Encrypt and Sign PDFs in .NET With One Free NuGet Package

Sooner or later, most business apps need PDFs. Reports and invoices have to be generated, forms filled from your data, files protected with a password, archived in a long term format, or signed before they go out.

sgcPDF .NET covers all of those jobs in one NuGet package. Everything happens in code, headless, with nothing to open on screen, so the same calls work in a console app or in a background service.

This tour has one section per task: create a document with text, a table and an image, build and fill a form, extract text and images, encrypt with AES-256, save as PDF/A-2b, and sign with PAdES B-B. Every sample was compiled against the published packages and run, and the results quoted below are what it printed.

Setup

Create a console app and add the packages:

dotnet new console
dotnet add package esegece.sgcPDF.Community
dotnet add package esegece.sgcPDF.sgcSign.Community
Enter fullscreen mode Exit fullscreen mode

You only need the second package for the signing sample at the end. It brings in esegece.sgcSign.Community, which supplies the signing key provider.

The packages target netstandard2.0, net472, net6.0, net8.0, net9.0 and net10.0. Through netstandard2.0 they also reach .NET Framework 4.6.2 and later, Mono and Xamarin. The samples use net8.0:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="esegece.sgcPDF.Community" Version="2026.10.0" />
    <PackageReference Include="esegece.sgcPDF.sgcSign.Community" Version="2026.10.0" />
  </ItemGroup>

</Project>
Enter fullscreen mode Exit fullscreen mode

Each sample below is its own file, a static class with a Run() method, because C# allows top level statements in one file only. Drop the files into the project and call the one you want from Program.cs, for example CreateSample.Run();. Everything lives in the esegece.sgcPDF namespace, plus esegece.sgcSign for the signing key.

1. Create a PDF with text, a table and an image

TsgcPDFDocument is the entry point. Add a page, draw on it and save:

using esegece.sgcPDF;

static class CreateSample
{
    public static void Run()
    {
        using var doc = new TsgcPDFDocument();
        doc.Info.Title = "Quarterly report";
        doc.Info.Author = "sgcPDF sample";

        // A4 page, sizes in points (1/72 inch), origin at the bottom left
        TsgcPDFPage page = doc.AddPage(595, 842);

        // Title and a paragraph
        page.DrawText("Quarterly report", 50, 780, TsgcPDFStandardFont.sfHelveticaBold, 24);
        page.DrawLine(50, 770, 545, 770, 1.0);
        page.DrawText("Sales grew in every region this quarter, led by the North office.",
            50, 745, TsgcPDFStandardFont.sfHelvetica, 12);
        page.DrawText("The table below lists units sold and revenue per region.",
            50, 728, TsgcPDFStandardFont.sfHelvetica, 12);

        // A 3 column table with a header row
        var table = new TsgcPDFTable(3);
        table.SetColumnWidths(new double[] { 215, 140, 140 });
        table.SetHeaders("Region", "Units", "Revenue");
        table.AddRowData("North", "1,240", "EUR 49,600");
        table.AddRowData("South", "980", "EUR 39,200");
        table.AddRowData("East", "1,115", "EUR 44,600");
        table.AddRowData("West", "860", "EUR 34,400");
        table.SetHeaderFont(TsgcPDFStandardFont.sfHelveticaBold, 11);
        table.SetDefaultFont(TsgcPDFStandardFont.sfHelvetica, 11);
        table.SetHeaderBackground(0.20, 0.35, 0.60); // header text is white
        table.SetAlternateRowColors(0.96, 0.96, 0.96);
        table.DrawOnPage(page, 50, 700); // x, y of the top left corner

        // An image from a PNG file: x, y (bottom left), width, height
        page.DrawText("Logo:", 50, 565, TsgcPDFStandardFont.sfHelveticaBold, 12);
        page.DrawImageFile("logo.png", 50, 435, 180, 120);

        doc.SaveToFile("01_create.pdf");
        Console.WriteLine($"Saved 01_create.pdf, {doc.PageCount()} page(s)");
    }
}
Enter fullscreen mode Exit fullscreen mode

Positions and sizes are in points (1/72 inch), and the origin is the bottom left corner of the page, so y grows upward. The y you pass to DrawText is the text baseline. The table is the exception: DrawOnPage takes the top left corner, and the rows grow downward from there.

The table header text is white, so give SetHeaderBackground a dark colour. A light one leaves the header row unreadable.

The sample expects a logo.png in the working directory, but any PNG, JPEG, BMP or TIFF file will do. It prints Saved 01_create.pdf, 1 page(s) and writes a 21.7 KB file. Opened again, Info.Title reads back as "Quarterly report", and the PNG is stored as a compressed image object. Here is page 1, rendered to PNG with the library's own raster renderer, TsgcPDFRendererRaster:

Page 1 of the generated quarterly report, with a title, a paragraph, a table with a blue header and striped rows, and a logo

2. Build a form, then fill and flatten it

doc.AcroForm handles interactive forms. This sample builds a page with a text field and a check box, opens the file again to fill it, then saves a flattened copy in which the values become ordinary page content:

using esegece.sgcPDF;

static class FormSample
{
    public static void Run()
    {
        // Step 1: a PDF with a text field and a check box
        using (var doc = new TsgcPDFDocument())
        {
            TsgcPDFPage page = doc.AddPage(595, 842);
            page.DrawText("Newsletter sign up", 50, 780, TsgcPDFStandardFont.sfHelveticaBold, 20);

            page.DrawText("Name:", 50, 735, TsgcPDFStandardFont.sfHelvetica, 12);
            // name, page index, then the field rectangle: x1, y1, x2, y2
            doc.AcroForm.AddTextField("name", 0, 150, 728, 450, 750);
            page.DrawRectangle(150, 728, 300, 22, 0.5); // a visible box: x, y, width, height, line width

            page.DrawText("Subscribe:", 50, 700, TsgcPDFStandardFont.sfHelvetica, 12);
            doc.AcroForm.AddCheckBox("subscribe", 0, 150, 696, 166, 712);
            page.DrawRectangle(150, 696, 16, 16, 0.5);

            doc.SaveToFile("02_form.pdf");
        }

        // Step 2: open it, fill the fields and save
        using (var doc = new TsgcPDFDocument())
        {
            doc.LoadFromFile("02_form.pdf");
            doc.AcroForm.FillField("name", "Ada Lovelace");
            doc.AcroForm.SetCheckBox("subscribe", true);
            doc.SaveToFile("02_form_filled.pdf");
        }

        // Optional: flatten, the values become plain page content and the fields go away
        using (var doc = new TsgcPDFDocument())
        {
            doc.LoadFromFile("02_form_filled.pdf");
            doc.AcroForm.FlattenFields();
            doc.SaveToFile("02_form_flat.pdf");
        }

        // Read the values back
        using (var doc = new TsgcPDFDocument())
        {
            doc.LoadFromFile("02_form_filled.pdf");
            Console.WriteLine($"name      = {doc.AcroForm.GetFieldValue("name")}");
            Console.WriteLine($"subscribe = {doc.AcroForm.GetFieldValue("subscribe")}");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Two details are easy to miss. AddTextField and AddCheckBox take the two corners of the field (x1, y1, x2, y2), while DrawRectangle takes x, y, width and height. And generated fields have no visible border, which is why the sample draws a rectangle under each one, so the form reads well on screen and on paper.

The text field reads back as Ada Lovelace and the check box as Yes. In the flattened copy, AcroForm.FieldCount() returns 0 and the page text contains "Ada Lovelace", so the value is now part of the page itself. AcroForm also offers combo boxes, list boxes, radio groups, buttons and signature fields, and it exchanges field data as FDF or XFDF.

3. Extract text and images

This sample reads the report from sample 1 back, the text page by page and the images as PNG files:

using esegece.sgcPDF;

static class ExtractSample
{
    public static void Run()
    {
        using var doc = new TsgcPDFDocument();
        doc.LoadFromFile("01_create.pdf");

        // Text, page by page
        for (int i = 0; i < doc.PageCount(); i++)
        {
            Console.WriteLine($"--- page {i + 1} ---");
            Console.WriteLine(doc.ExtractTextW(i));
        }

        // Images, saved as PNG files
        using var images = new TsgcPDFImageExtractor();
        images.Document = doc;
        images.Scan();
        for (int i = 0; i < images.Count; i++)
        {
            TsgcPDFImageItem item = images.Items(i);
            Console.WriteLine($"image {i}: page {item.PageIndex + 1}, {item.Width} x {item.Height}, {item.ColorSpace}");
            images.SaveAsPNG(i, $"03_image_{i}.png");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Use ExtractTextW, which returns Unicode text. ExtractText returns an ANSI string. The text comes back line by line, from the title through the paragraph, and the table arrives as space separated rows such as North 1,240 EUR 49,600.

The extractor finds the one image and reports it as page 1, 120 x 80, DeviceRGB. The PNG it saved was byte identical to the original logo.png. It can also save JPEG files with SaveAsJPEG, or hand you the PNG bytes in memory with GetPNGBytes.

4. Encrypt with AES-256 and permissions

This sample encrypts the report with a user password to open it and an owner password for full rights, and allows the user nothing but printing. Then it opens the file twice, once with the password and once without:

using esegece.sgcPDF;

static class EncryptSample
{
    public static void Run()
    {
        // Encrypt the sample 1 PDF with AES-256: a user password to open it, an owner
        // password for full rights, and printing as the only permission for the user
        using (var doc = new TsgcPDFDocument())
        {
            doc.LoadFromFile("01_create.pdf");
            doc.Encryption.SetupEncryption(TsgcPDFEncryptionMethod.emAES_256, "reader", "owner-secret",
                TsgcPDFPermissions.ppPrint | TsgcPDFPermissions.ppPrintHigh);
            doc.SaveToFile("04_encrypted.pdf");
        }

        // Open it with the user password
        using (var doc = new TsgcPDFDocument())
        {
            doc.SetPassword("reader");
            doc.LoadFromFile("04_encrypted.pdf");
            Console.WriteLine($"With password: {doc.PageCount()} page(s), method {doc.Encryption.Method}, " +
                $"permissions {doc.Encryption.Permissions}");
            Console.WriteLine(doc.ExtractTextW(0).Split('\n')[0]);
        }

        // And without it
        try
        {
            using var doc = new TsgcPDFDocument();
            doc.LoadFromFile("04_encrypted.pdf");
            Console.WriteLine($"Without password: loaded, text = '{doc.ExtractTextW(0).Split('\n')[0]}'");
        }
        catch (Exception e)
        {
            Console.WriteLine($"Without password: {e.GetType().Name}: {e.Message}");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Use doc.Encryption.SetupEncryption with a permissions value, as above. The shorter doc.SetEncryption(method, user, owner) also encrypts, but it grants every permission, and Encryption.Permissions is read only, so you can't narrow them afterwards. Give the owner password a different value from the user password, otherwise the restrictions don't hold.

To open an encrypted file, call SetPassword before LoadFromFile. With the user password the file opens with 1 page, method emAES_256 and permissions ppPrint, ppPrintHigh, and the first line of text reads "Quarterly report". Without a password, LoadFromFile throws EsgcPDFEncryptionError with the message "Invalid password for encrypted PDF document". A wrong password throws the same error. The owner password opens the file too, with Encryption.IsOwnerPassword set to true.

In the saved bytes the encryption dictionary reads /V 5 /R 6 /CFM /AESV3, which is AES-256 at revision 6, and the title no longer appears in plain text. The other methods are emRC4_40, emRC4_128 and emAES_128.

5. Save as PDF/A-2b and validate it

For long term archiving, set a PDF/A level on the document, then check the saved file with the built-in validator:

using esegece.sgcPDF;

static class PdfASample
{
    public static void Run()
    {
        using (var doc = new TsgcPDFDocument())
        {
            doc.SetPDFALevel(TsgcPDFALevel.paA2b);
            doc.Info.Title = "Archived invoice";
            doc.Info.Author = "sgcPDF sample";

            // PDF/A needs embedded fonts, so use a TrueType font instead of the standard 14
            int font = doc.AddTrueTypeFont("Arial", false, false);

            TsgcPDFPage page = doc.AddPage(595, 842);
            page.DrawTextTT("Archived invoice", 50, 780, font, 22);
            page.DrawTextTT("Saved as PDF/A-2b, with the font embedded and XMP metadata.", 50, 750, font, 12);
            doc.SaveToFile("05_pdfa.pdf");
        }

        // Re-open and validate
        using (var doc = new TsgcPDFDocument())
        {
            doc.LoadFromFile("05_pdfa.pdf");
            var validator = new TsgcPDFAValidator(TsgcPDFALevel.paA2b);
            bool valid = validator.Validate(doc.Parser);
            Console.WriteLine($"PDFALevel = {doc.PDFALevel}, valid = {valid}, " +
                $"errors = {validator.Errors.Count}, warnings = {validator.Warnings.Count}");
            for (int i = 0; i < validator.Errors.Count; i++)
                Console.WriteLine("  error: " + validator.Errors[i]);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

PDF/A requires embedded fonts, and the standard 14 fonts are not embedded. That is why this sample registers a TrueType font with AddTrueTypeFont and draws with DrawTextTT. AddTrueTypeFont looks the font up by name among the installed system fonts, which was tested on Windows. On Linux or in a container, pass a .ttf path to AddTrueTypeFontFromFile instead.

The output is PDFALevel = paNone, valid = True, errors = 0, warnings = 0. The paNone is expected, because PDFALevel is not read back from the metadata of a loaded file. Check conformance with TsgcPDFAValidator instead. The file passes the library's own validator with no errors and no warnings. As a control, the same validator rejects the plain report from sample 1 with 7 errors, among them the missing metadata, the fonts that are not embedded and the missing output intent, so it really checks.

The saved file carries XMP metadata with PDF/A part 2 and conformance B, an sRGB output intent, the embedded font, and no encryption. Besides paA2b, TsgcPDFALevel offers paA1b, paA3b, paA1a, paA2a, paA3a, paA4 and paA4f.

6. Sign with PAdES B-B and verify the signature

Signing comes from the second package, esegece.sgcPDF.sgcSign.Community. This sample makes a self-signed test certificate in code with the BCL CertificateRequest, exports it to PFX bytes in memory, signs the report from sample 1 to PAdES B-B, and verifies the result. No certificate file touches the disk:

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using esegece.sgcPDF;
using esegece.sgcSign;

static class SignSample
{
    public static void Run()
    {
        // A self-signed test certificate, made in code (use your real one in production)
        using RSA rsa = RSA.Create(2048);
        var request = new CertificateRequest("CN=sgcPDF Demo Signer, O=Demo", rsa,
            HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        request.CertificateExtensions.Add(new X509KeyUsageExtension(
            X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation, true));
        using X509Certificate2 cert = request.CreateSelfSigned(
            DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1));
        byte[] pfx = cert.Export(X509ContentType.Pfx, "test");

        // Sign the sample 1 PDF to PAdES B-B
        var key = new TsgcPFXKeyProvider();
        key.LoadFromBytes(pfx, "test");

        var signer = new TsgcPDFPAdESSigner();
        signer.KeyProvider = key;
        signer.Level = TsgcPDFPAdESLevel.paBB;
        signer.SignPDFFile("01_create.pdf", "06_signed.pdf");

        // Verify it
        using var verifier = new TsgcPDFSignatureVerifier();
        verifier.LoadDocument("06_signed.pdf");
        for (int i = 0; i < verifier.GetSignatureCount(); i++)
        {
            bool valid = verifier.VerifySignature(i);
            TsgcPDFSignatureInfo info = verifier.GetSignature(i);
            Console.WriteLine($"signature {i}: valid = {valid}, subfilter = {info.SubFilter}, " +
                $"modified = {verifier.IsDocumentModified(i)}, trusted = {verifier.IsSignatureTrusted(i)}");
            Console.WriteLine($"  signer = {info.CertInfo.SubjectName}");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The verifier finds one signature and prints valid = True, subfilter = ETSI.CAdES.detached, modified = False, trusted = False. ETSI.CAdES.detached is the PAdES subfilter. The signer comes back as "Demo, sgcPDF Demo Signer", with the organization listed first.

The trusted = False is the expected result, not an error. A self-signed certificate verifies as valid and unmodified, but it isn't trusted, because no trust store knows it. In production you sign with a certificate from a trusted CA, loaded from its PFX file with key.LoadFromFile(file, password).

Verification also catches changes. In a tamper test, one byte flipped inside the signed range of a copy made VerifySignature return false.

B-B needs no network access. For B-T and above (paBT, paBLT, paBLTA), set signer.TSAURL to a time stamp server. The verifier itself is in the core package, so an app that only checks signatures needs just esegece.sgcPDF.Community.

About the Community edition

The Community edition is free for commercial and non-commercial use by individuals and organizations under EUR 1M annual gross revenue and with no more than 5 developers. Above either threshold, a paid licence is needed. It has the same feature set as the paid editions, with no functional limits, so everything in this tour runs exactly as shown.

There is one thing you may notice. Now and then, the Community edition prints a short licence notice to stderr when the first document or parser is created. It appears at most once per process and not on every run, and most runs print nothing at all. Because it goes to stderr, output you redirect from stdout stays clean. It doesn't block: timed runs with and without the notice differed by about a millisecond.

Links

Top comments (0)