Generate Tagged Invoice

This module uses the New DevExpress PDF Document API (PdfDocument) to generate a tagged PDF file and attach ZUGFeRD-compliant data from the predefined XML file. The generated file is compliant with PDF/UA-1 and PDF/A-3b standards.

Deselect Include ZUGFeRD Invoice to generate a PDF without attached data.

Click Generate Invoice to generate the document and download the result.

ZUGFeRD Invoice XML
Northwind_Invoice.xml



using System.Drawing;
using System.Globalization;
using DevExpress.Drawing.Printing;
using DevExpress.Docs.Pdf;

Stream GenerateInvoiceDocument(CultureInfo documentCulture, int invoiceNumber, bool includeZugferd, Stream xmlStream) {
    var title = $"Invoice {invoiceNumber}";
    var darkFill = new SolidFill(PdfColor.FromRgb(0x2F, 0x3B, 0x4A));
    var mutedFill = new SolidFill(PdfColor.FromRgb(0x7A, 0x87, 0x94));
    var rowDivider = Outline.Create(new SolidFill(PdfColor.FromRgb(0xE1, 0xE5, 0xEA)), 0.5f);
    var heavyBorder = Outline.Create(new SolidFill(PdfColor.FromRgb(0x9A, 0xA5, 0xB1)), 2f);

    using var doc = new PdfDocument() {
        ViewerPreferences = new ViewerPreferences() { DisplayDocTitle = true },
        LanguageCulture = documentCulture,
        Metadata = new DocumentMetadata() { Xmp = new XmpMetadata() },
        MarkInfo = new MarkInfo() { Marked = true }
    };

    // ── metadata ─────────────────────────────────────────────────────
    doc.Metadata.Xmp.XmpPdfUASchema.Part = new XmpInteger(1);
    doc.Metadata.Xmp.XmpPdfAExtensions.Register(new XmpPdfAExtensionDescriptor(
        "http://www.aiim.org/pdfua/ns/id/",
        "pdfuaid",
        "PDF/UA identification schema",
        new List<XmpPdfAExtensionProperty> {
            new XmpPdfAExtensionProperty("part", "Indicates which part of ISO 14289 is followed.", "Integer")
        }
    ));
    doc.Metadata.Xmp.XmpPdfASchema.Part = new XmpInteger(3);
    doc.Metadata.Xmp.XmpPdfASchema.Conformance = new XmpString("B");
    doc.Metadata.Xmp.XmpDublinCoreSchema.Title.Add(documentCulture.Name, title);

    // ── new page and structure root ───────────────────────────────────────
    var root = doc.StructureTree.AddChildElement(Pdf17StructureType.Document);
    root.LanguageCulture = documentCulture;
    root.Title = title;
    var page = doc.Pages.Add(DXPaperKind.A4);

    // ── header ───────────────────────────────────────────────────────
    var headerSection = root.AddChildElement(Pdf17StructureType.Sect);
    var artifactGroupContent = new MarkedContentGroup("Artifact");
    page.AddFragment(artifactGroupContent);
    artifactGroupContent.Fragments.Add(PathFragment.Rectangle(0, page.CropBox.Height - 130, page.CropBox.Width, 130,
        new SolidFill(PdfColor.FromRgb(0xDF, 0xE3, 0xE8))));

    float cx = page.CropBox.Left + 60f;
     headerSection.AddChildElement(Pdf17StructureType.H1)
         .AddFragment(page, new TextFragment { Text = "Northwind Traders", Location = new PointF(cx, page.CropBox.Height - 40), ForegroundFill = darkFill, FontSize = 14});
    headerSection.AddChildElement( Pdf17StructureType.P).AddFragment(page,
        new TextFragment { Text = "One Portals Way, Twin Points WA, 98156", Location = new PointF(cx, page.CropBox.Height - 58) });

    headerSection.AddChildElement(Pdf17StructureType.P)
        .AddFragment(page, new TextFragment { Text = "1-206-555-1417", Location = new PointF(cx, page.CropBox.Height - 74) });
    headerSection.AddChildElement(Pdf17StructureType.P)
        .AddFragment(page, new TextFragment { Text = "northwind@mail.com", Location = new PointF(cx, page.CropBox.Height - 90) });
    headerSection.AddChildElement(Pdf17StructureType.P)
        .AddFragment(page,  new TextFragment { Text = "www.northwind.com", Location = new PointF(cx, page.CropBox.Height - 106) });
    headerSection.AddChildElement(Pdf17StructureType.H1)
        .AddFragment(page,new TextFragment { Text = "INVOICE", Location = new PointF(393f, page.CropBox.Height - 68), ForegroundFill = darkFill, FontSize = 28});

    float metaLabelX = page.CropBox.Right  - 200f, metaValX = page.CropBox.Right - 90f;
    var metaSection = headerSection.AddChildElement(Pdf17StructureType.Sect);

    metaSection.AddChildElement(Pdf17StructureType.P)
        .AddFragment(page, new TextFragment { Text = "Invoice №:", Location = new PointF(metaLabelX, page.CropBox.Height - 90), ForegroundFill = mutedFill })
        .AddFragment(new TextFragment { Text = invoiceNumber.ToString(), Location = new PointF(metaValX, page.CropBox.Height - 90) });

    metaSection.AddChildElement(Pdf17StructureType.P)
        .AddFragment(page, new TextFragment { Text = "Invoice Date:", Location = new PointF(metaLabelX, page.CropBox.Height - 106), ForegroundFill = mutedFill })
        .AddFragment(new TextFragment { Text = "04-21-26", Location = new PointF(metaValX, page.CropBox.Height - 106) });

    // ── bill to ───────────────────────────────────────────────────────
    var billSection = root.AddChildElement(Pdf17StructureType.Sect);

    billSection.AddChildElement(Pdf17StructureType.H2)
        .AddFragment(page, new TextFragment { Text = "Bill to:", Location = new PointF(page.CropBox.Left + 50, page.CropBox.Height - 155) });

    (string label, string value)[] billRows = {
        ("Company:", "Alfreds Futterkiste"),
        ("Contact Name:", "Maria Anders"),
        ("Address:", "Obere Str. 57, Berlin, Germany, 12209"),
        ("Phone:", "030-0074321"),
        ("Mail:", "alfredsfutterkiste@mail.com"),
    };
    float billValX = page.CropBox.Left + 150f;
    float billRowY = page.CropBox.Height - 175f;
    foreach(var (label, value) in billRows) {
        billSection.AddChildElement( Pdf17StructureType.P)
            .AddFragment(page, new TextFragment { Text = label, Location = new PointF(page.CropBox.Left + 50, billRowY), ForegroundFill = mutedFill })
            .AddFragment(new TextFragment { Text = value, Location = new PointF(billValX, billRowY) });
        billRowY -= 16f;
    }

    // ── table ─────────────────────────────────────────────────────────
    float tableTop = billRowY - 20f;
    float rowH = 20f;
    float tableW = page.CropBox.Right  - page.CropBox.Left - 100f;

    var table = root.AddChildElement(Pdf17StructureType.Table);
    var thead= table.AddChildElement(Pdf17StructureType.THead);

    artifactGroupContent.Fragments.Add(PathFragment.Rectangle(page.CropBox.Left + 50, tableTop - rowH, tableW, rowH, SolidFill.LightGray));
    artifactGroupContent.Fragments.Add(PathFragment.Line(page.CropBox.Left + 50, tableTop - rowH, page.CropBox.Right - 50f, tableTop - rowH, heavyBorder));

    var hRow = thead.AddChildElement(Pdf17StructureType.TR);
    float[] colX = { 55f, 95f, 305f, 375f, 435f, 495f };
    string[] headers = { "#", "Product Name", "Unit Price", "Quantity", "Discount", "Total" };
    for(int i = 0; i < headers.Length; i++) {
        var th = hRow.AddChildElement(Pdf17StructureType.TH);
        th.Attributes.Add(new TableAttribute { Scope = TableScope.Column });
        th.AddFragment(page,
            new TextFragment { Text = headers[i], Location = new PointF(colX[i], tableTop - rowH + 5), ForegroundFill = darkFill, });
    }

    (string num, string name, string price, string qty, string disc, string total)[] items = {
        ("01", "Rössle Sauerkraut", "$45.60",  "15", "$3.75", "$683.75"),
        ("02", "Chartreuse verte",  "$18.00",  "21", "$5.25", "$377.75"),
        ("03", "Spegesild",         "$12.00",   "2", "$0.50",  "$23.75"),
    };

    var tbody = table.AddChildElement( Pdf17StructureType.TBody);

    float rowY = tableTop - 2 * rowH;
    foreach(var (num, name, price, qty, disc, total) in items) {
        var tr = tbody.AddChildElement(Pdf17StructureType.TR);
        (string val, float x)[] cells = {
            (num, colX[0]), (name, colX[1]), (price, colX[2]),
            (qty, colX[3]), (disc, colX[4]), (total, colX[5]),
        };
        foreach(var (val, x) in cells)
            tr.AddChildElement(Pdf17StructureType.TD)
                .AddFragment(page, new TextFragment { Text = val, Location = new PointF(x, rowY + 5) });
        artifactGroupContent.Fragments.Add(PathFragment.Line(page.CropBox.Left + 50 , rowY, page.CropBox.Right - 50f, rowY, rowDivider));
        rowY -= rowH;
    }

    // ── totals ────────────────────────────────────────────────────────
    float totY = rowY + rowH - 25f;
    float totLabelX = colX[3], totValX = colX[5];

    var totalsSect = root.AddChildElement(Pdf17StructureType.Sect);
    artifactGroupContent.Fragments.Add(PathFragment.Line(totLabelX - 5f, totY - 22f, page.CropBox.Right - 50f, totY - 22f, heavyBorder));

     totalsSect.AddChildElement(Pdf17StructureType.P)
        .AddFragment(page, new TextFragment { Text = "Sub Total:", Location = new PointF(totLabelX, totY) })
        .AddFragment(new TextFragment { Text = "$1086.00", Location = new PointF(totValX, totY) });

    totalsSect.AddChildElement(Pdf17StructureType.P)
        .AddFragment(page, new TextFragment { Text = "Discount Total:", Location = new PointF(totLabelX, totY - 16f) })
        .AddFragment(new TextFragment { Text = "$9.50", Location = new PointF(totValX, totY - 16f) });

    float gtY = totY - 38f;
    totalsSect.AddChildElement(Pdf17StructureType.P)
        .AddFragment(page, new TextFragment { Text = "Grand Total:", Location = new PointF(totLabelX, gtY), ForegroundFill = darkFill })
        .AddFragment(new TextFragment { Text = "$1076.50", Location = new PointF(totValX, gtY), ForegroundFill = darkFill });

    if(includeZugferd && xmlStream != null) {
        doc.AttachZugferdInvoice(xmlStream);
    }

    var outputStream = new MemoryStream();
    doc.Save(outputStream, new SaveOptions() {SyncMetadata = true, UpdateCreatedAt = true, UpdateModifiedAt = true });
    return outputStream;
}