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.

package examples;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import com.pdfjet.*;
import com.pdfjet.fonts.*;
import com.pdfjet.qrcode.*;

/**
 * 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.
 * <p>
 * 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.
 * </p>
 * <p>
 * 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.
 * </p>
 *
 * @see EmbeddedFile
 * @see PDF#addAssociatedFile(EmbeddedFile)
 * @see PDF#addMetadata(String)
 * @see SVGImage
 * @see Table
 */
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 final String[] ITEMS = {
        "Letterpress business cards, 500",
        "A4 letterhead paper, box of 500",
        "DL envelopes, box of 250",
        "Logo design, hours",
        "Delivery",
    };
    private static final int[] QUANTITIES = {2, 3, 4, 6, 1};
    private static final long[] PRICES = {4500, 2850, 1275, 5500, 990};
    private static final int VAT_PERCENT = 19;

    private static final int NAVY = 0x1d3557;
    private static final int RED = 0xe63946;
    private static final 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 final 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() throws Exception {
        PDF pdf = new PDF(new BufferedOutputStream(new FileOutputStream("Example_55.pdf")));
        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 FileInputStream("data/invoice/factur-x.xml"),
                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);
        for (String text : 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 = {
            {"Invoice number", "2026-0147"},
            {"Invoice date", "23 September 2026"},
            {"Due date", "23 October 2026"},
            {"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 ArrayList<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], String.valueOf(QUANTITIES[i]), 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)
            throws Exception {
        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, String... texts) {
        List<Cell> row = new ArrayList<Cell>();
        for (String text : 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.get(0).setColSpan(4);
        row.get(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 = String.valueOf(cents / 100);
        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.charAt(i));
        }
        long rest = cents % 100;
        return sb.append(rest < 10 ? ".0" : ".").append(rest).toString();
    }

    public static void main(String[] args) throws Exception {
        long time0 = System.currentTimeMillis();
        new Example_55();
        long time1 = System.currentTimeMillis();
        System.out.printf("Example_55 => %4d ms%n", time1 - time0);
    }
}   // End of Example_55.java

©2026 PDFjet Software.