Example_55

An invoice that is also data: a PDF/A-3A document that carries factur-x.xml, as the e-invoice standards Factur-X and ZUGFeRD ask, with an SVG logo, a table of items with its totals and a QR code a banking app scans to pay.

Example_55.pdf — the PDF this example creates.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Diagnostics;
using PDFjet.NET;

/**
 * Example_55.cs
 * An invoice, as a business sends one: a PDF/A-3 document that carries the
 * invoice as data too, in the file factur-x.xml, the way the e-invoice
 * standards Factur-X and ZUGFeRD ask, so that accounting software reads the
 * amounts instead of someone typing them in again.
 *
 * The page has the logo of the seller, an SVG file written as a drawing
 * program exports one, with a style sheet, groups and shapes; the addresses
 * and the dates; a table of the items with a header row, striped rows and the
 * totals in its footer rows; and a QR code a banking app scans to pay, in the
 * format of the European Payments Council. The document is PDF/A-3a and
 * PDF/UA-1 at once, PDF_A_3A_UA_1, so it is kept as PDF/A and tagged for
 * screen readers: a heading, paragraphs, a table and a
 * figure.
 *
 * The XML is attached with AddAssociatedFile, with the relationship
 * Alternative, since it is the same invoice in another form, and
 * AddMetadata says in the metadata of the document which file is the
 * invoice, with the extension schema PDF/A asks for properties of its own.
 * The companies, the addresses and the numbers are made up.
 */
public class Example_55 {
    // The items of the invoice: the description, the quantity and the unit
    // price in cents. The amounts are in cents, so that they add up exactly.
    private static readonly String[] ITEMS = {
        "Letterpress business cards, 500",
        "A4 letterhead paper, box of 500",
        "DL envelopes, box of 250",
        "Logo design, hours",
        "Delivery",
    };
    private static readonly int[] QUANTITIES = {2, 3, 4, 6, 1};
    private static readonly long[] PRICES = {4500, 2850, 1275, 5500, 990};
    private const int VAT_PERCENT = 19;

    private const int NAVY = 0x1d3557;
    private const int RED = 0xe63946;
    private const int STRIPE = 0xf1f4f8;

    // The description of the invoice for the metadata of the document: the
    // file that holds it, its type and its profile, and the extension schema
    // that declares these properties, which PDF/A asks of properties of its own.
    private static readonly String FACTUR_X_METADATA =
            "<rdf:Description rdf:about=\"\"\n"
            + "    xmlns:fx=\"urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#\">\n"
            + "  <fx:DocumentType>INVOICE</fx:DocumentType>\n"
            + "  <fx:DocumentFileName>factur-x.xml</fx:DocumentFileName>\n"
            + "  <fx:Version>1.0</fx:Version>\n"
            + "  <fx:ConformanceLevel>BASIC</fx:ConformanceLevel>\n"
            + "</rdf:Description>\n"
            + "<rdf:Description rdf:about=\"\"\n"
            + "    xmlns:pdfaExtension=\"http://www.aiim.org/pdfa/ns/extension/\"\n"
            + "    xmlns:pdfaSchema=\"http://www.aiim.org/pdfa/ns/schema#\"\n"
            + "    xmlns:pdfaProperty=\"http://www.aiim.org/pdfa/ns/property#\">\n"
            + "  <pdfaExtension:schemas>\n"
            + "    <rdf:Bag>\n"
            + "      <rdf:li rdf:parseType=\"Resource\">\n"
            + "        <pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>\n"
            + "        <pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>\n"
            + "        <pdfaSchema:prefix>fx</pdfaSchema:prefix>\n"
            + "        <pdfaSchema:property>\n"
            + "          <rdf:Seq>\n"
            + Property("DocumentFileName", "The name of the embedded XML document")
            + Property("DocumentType", "The type of the hybrid document, in capital letters")
            + Property("Version", "The version of the specification of the XML schema")
            + Property("ConformanceLevel", "The profile of the XML document")
            + "          </rdf:Seq>\n"
            + "        </pdfaSchema:property>\n"
            + "      </rdf:li>\n"
            + "    </rdf:Bag>\n"
            + "  </pdfaExtension:schemas>\n"
            + "</rdf:Description>";

    private static String Property(String name, String description) {
        return "            <rdf:li rdf:parseType=\"Resource\">\n"
                + "              <pdfaProperty:name>" + name + "</pdfaProperty:name>\n"
                + "              <pdfaProperty:valueType>Text</pdfaProperty:valueType>\n"
                + "              <pdfaProperty:category>external</pdfaProperty:category>\n"
                + "              <pdfaProperty:description>" + description + "</pdfaProperty:description>\n"
                + "            </rdf:li>\n";
    }

    public Example_55() {
        PDF pdf = new PDF(new BufferedStream(
                new FileStream("Example_55.pdf", FileMode.Create)));
        pdf.SetCompliance(Compliance.PDF_A_3A_UA_1);
        pdf.SetTitle("Invoice 2026-0147");
        pdf.SetAuthor("Lindenberg Paper & Print GmbH");
        pdf.SetLanguage("en-US");

        // The invoice as data, which the document carries, and the description
        // of it in the metadata. Files are added before the first page.
        pdf.AddAssociatedFile(new EmbeddedFile(
                pdf,
                "factur-x.xml",
                new FileStream("data/invoice/factur-x.xml", FileMode.Open, FileAccess.Read),
                true,
                "text/xml",
                Relationship.ALTERNATIVE,
                "The invoice as data, in the profile BASIC of Factur-X and ZUGFeRD."));
        pdf.AddMetadata(FACTUR_X_METADATA);

        Font regular = new Font(pdf, IBMPlexSans.Regular);
        regular.SetSize(10f);
        Font semiBold = new Font(pdf, IBMPlexSans.SemiBold);
        semiBold.SetSize(10f);

        Page page = new Page(pdf, A4.PORTRAIT);
        float left = 50f;
        float right = page.GetWidth() - 50f;
        float middle = 345f;

        // The letterhead: the logo, and the address of the seller beside it
        SVGImage logo = new SVGImage("data/invoice/logo.svg");
        logo.SetAltDescription("The logo of Lindenberg Paper & Print: a sheet of paper on a navy tile.");
        logo.SetLocation(left, 50f);
        logo.DrawOn(page);

        float y = 58f;
        Line(page, semiBold, "Lindenberg Paper & Print GmbH", middle, y, Color.black);
        foreach (String text in new String[] {"Hafenstraße 12", "20457 Hamburg, Germany", "VAT ID DE123456789"}) {
            y += 14f;
            Line(page, regular, text, middle, y, Color.dimgray);
        }

        page.AddArtifactBMC();
        page.SetPenColor(RED);
        page.SetPenWidth(1.5f);
        page.DrawLine(left, 125f, right, 125f);
        page.AddEMC();

        TextLine title = new TextLine(semiBold, "Invoice");
        title.SetStructureType(StructElem.H1);
        title.SetFontSize(26f);
        title.SetTextColor(NAVY);
        title.SetLocation(left, 172f);
        title.DrawOn(page);

        // The customer on the left, and the numbers of the invoice on the right
        y = 205f;
        TextLine billTo = new TextLine(regular, "BILL TO");
        billTo.SetFontSize(8f);
        billTo.SetTextColor(Color.dimgray);
        billTo.SetLocation(left, y);
        billTo.DrawOn(page);
        Line(page, semiBold, "Kranich Design Studio", left, y + 16f, Color.black);
        Line(page, regular, "Lindenallee 4", left, y + 30f, Color.black);
        Line(page, regular, "50668 Köln, Germany", left, y + 44f, Color.black);

        String[][] facts = {
            new String[] {"Invoice number", "2026-0147"},
            new String[] {"Invoice date", "23 September 2026"},
            new String[] {"Due date", "23 October 2026"},
            new String[] {"Customer number", "K-3310"},
        };
        for (int i = 0; i < facts.Length; i++) {
            float factY = y + 16f + i * 14f;
            Line(page, regular, facts[i][0], middle, factY, Color.dimgray);
            Line(page, semiBold, facts[i][1], right - semiBold.StringWidth(facts[i][1]), factY, Color.black);
        }

        // The items, and the totals in the footer rows of the table
        long net = 0;
        for (int i = 0; i < ITEMS.Length; i++) {
            net += QUANTITIES[i] * PRICES[i];
        }
        long vat = (net * VAT_PERCENT + 50) / 100;    // Rounded to the cent
        long total = net + vat;

        List<List<Cell>> rows = new List<List<Cell>>();
        rows.Add(Row(semiBold, "Description", "Quantity", "Unit price", "VAT", "Amount (EUR)"));
        for (int i = 0; i < ITEMS.Length; i++) {
            rows.Add(Row(regular, ITEMS[i], QUANTITIES[i].ToString(), Money(PRICES[i]),
                    VAT_PERCENT + "%", Money(QUANTITIES[i] * PRICES[i])));
        }
        rows.Add(TotalRow(regular, "Net amount", Money(net)));
        rows.Add(TotalRow(regular, "VAT " + VAT_PERCENT + "%", Money(vat)));
        rows.Add(TotalRow(semiBold, "Total due (EUR)", Money(total)));

        Table table = new Table();
        table.SetTableData(rows, 1);
        table.SetNumberOfFooterRows(3);
        table.SetHeaderRowStyle(semiBold, Color.white, NAVY);
        table.SetAlternateRowColor(STRIPE);
        table.SetCellBorders(false);
        table.SetWidth(right - left);
        table.SetColumnWidthsInPercent(46f, 12f, 16f, 8f, 18f);
        for (int column = 1; column < 5; column++) {
            table.SetTextAlignmentInColumn(column, Alignment.RIGHT);
        }
        table.SetLocation(left, 290f);
        float[] xy = table.DrawOn(page);

        // A line over the total due, an artifact
        page.AddArtifactBMC();
        page.SetPenColor(NAVY);
        page.SetPenWidth(1f);
        page.DrawLine(middle, xy[1] - 20f, right, xy[1] - 20f);
        page.AddEMC();

        // How to pay, and the QR code of the payment, which a banking app reads
        float paymentY = xy[1] + 40f;
        TextLine heading = new TextLine(semiBold, "Payment");
        heading.SetStructureType(StructElem.H2);
        heading.SetFontSize(13f);
        heading.SetTextColor(NAVY);
        heading.SetLocation(left, paymentY);
        heading.DrawOn(page);

        TextBlock payment = new TextBlock(regular,
                "Please transfer EUR " + Money(total) + " by 23 October 2026 to Lindenberg Paper & Print GmbH, "
                + "IBAN DE89 3704 0044 0532 0130 00, with the reference 2026-0147.\n\n"
                + "Or scan the code with your banking app, which fills in the transfer.");
        payment.SetLineSpacing(1.4f);
        payment.SetLocation(left, paymentY + 10f);
        payment.SetWidth(330f);
        payment.DrawOn(page);

        // An EPC QR code, which banking apps in Europe read as a SEPA credit
        // transfer: the version, the character set, the name, the IBAN, the
        // amount and the reference, one to a line, with error correction M.
        String epc = "BCD\n002\n1\nSCT\n\nLindenberg Paper & Print GmbH\nDE89370400440532013000\n"
                + "EUR" + Money(total) + "\n\n\nInvoice 2026-0147";
        QRCode qr = new QRCode(epc, ErrorCorrectionLevel.M);
        qr.SetModuleLength(2f);
        qr.SetLocation(right - 90f, paymentY - 8f);
        float[] qrXY = qr.DrawOn(page);
        TextLine caption = new TextLine(regular, "Scan to pay");
        caption.SetFontSize(8f);
        caption.SetTextColor(Color.dimgray);
        caption.SetLocation(right - 90f, qrXY[1] + 12f);
        caption.DrawOn(page);

        // What makes this invoice data as well
        TextBlock note = new TextBlock(regular,
                "This invoice is also data. It is a PDF/A-3 document that carries its content as "
                + "factur-x.xml, in the profile BASIC of Factur-X and ZUGFeRD, so that accounting "
                + "software reads the items and the amounts instead of someone typing them in. "
                + "Open the attachments of your PDF viewer to see it.");
        note.SetFontSize(9f);
        note.SetLineSpacing(1.4f);
        note.SetTextColor(NAVY);
        note.SetBackgroundColor(STRIPE);
        note.SetCornerRadius(6f);
        note.SetPadding(12f);
        note.SetLocation(left, qrXY[1] + 45f);
        note.SetWidth(right - left);
        note.DrawOn(page);

        TextLine footer = new TextLine(regular,
                "Lindenberg Paper & Print GmbH  ·  Hafenstraße 12, 20457 Hamburg  ·  Registered in Hamburg, HRB 000000");
        footer.SetFontSize(8f);
        footer.SetTextColor(Color.dimgray);
        footer.SetLocation((page.GetWidth() - footer.GetWidth()) / 2f, page.GetHeight() - 40f);
        footer.DrawOn(page);

        pdf.Complete();
    }

    // Draws a line of text in the font and the color.
    private static void Line(Page page, Font font, String text, float x, float y, int color) {
        TextLine line = new TextLine(font, text);
        line.SetTextColor(color);
        line.SetLocation(x, y);
        line.DrawOn(page);
    }

    // A row of the table, a cell in the font for each text.
    private static List<Cell> Row(Font font, params String[] texts) {
        List<Cell> row = new List<Cell>();
        foreach (String text in texts) {
            Cell cell = new Cell(font, text);
            cell.SetPadding(6f);
            row.Add(cell);
        }
        return row;
    }

    // A row of the totals: its label over the first four columns, right
    // aligned, and the amount. The cells the label spans are there and empty.
    private static List<Cell> TotalRow(Font font, String label, String amount) {
        List<Cell> row = Row(font, label, "", "", "", amount);
        row[0].SetColSpan(4);
        row[0].SetTextAlignment(Alignment.RIGHT);
        return row;
    }

    // The amount of cents as euros and cents, with a comma between the thousands.
    private static String Money(long cents) {
        String euros = (cents / 100).ToString();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < euros.Length; i++) {
            if (i > 0 && (euros.Length - i) % 3 == 0) {
                sb.Append(',');
            }
            sb.Append(euros[i]);
        }
        long rest = cents % 100;
        return sb.Append(rest < 10 ? ".0" : ".").Append(rest).ToString();
    }

    public static void Main(String[] args) {
        Stopwatch sw = Stopwatch.StartNew();
        long time0 = sw.ElapsedMilliseconds;
        new Example_55();
        long time1 = sw.ElapsedMilliseconds;
        sw.Stop();
        Console.WriteLine($"Example_55 => {time1 - time0,4} ms");
    }
}   // End of Example_55.cs

©2026 PDFjet Software.