ZUGFeRD

Digital transformation to paperless

With companies and organizations caught up in the next wave of digital transformation, we offer a closer look at how PDF may overcome the traditional roadblocks on the way there and how it helps meet the goals of forward-thinking business strategies.

Get the eBook "ZUGFeRD: The Future of Invoicing"

Did you know that there's a standard for invoicing?

This standard is called ZUGFeRD, and it combines the best practices from the EDI world with the qualities of the PDF/A standard to introduce industrial-strength invoicing processing without degrading the user experience for those who deal with invoices manually.

The human-oriented representation using PDF and the machine-oriented EDI information stored within the PDF are the key strengths of ZUGFeRD.

ZUGFeRD, the new standard for invoicing
About the book
In this book, you'll find a series of examples that explain step by step how to create ZUGFeRD invoices that conform to the Basic or the Comfort profile.
Image
eBook cover ZUGFeRD
Subtitle
eBook - free copy
Campaign Details

How does it work?

  • Fill out the form on the right.
  • On the thank you page you will get a direct link to our online book page.
  • You will receive an e-mail with a link to download the free PDF or EPUB version on LeanPub.
Campaign Embed form

Get your free copy

ZUGFeRD: The Future of Invoicing

NOTE: We are continually updating our features- and have some new features that are not included in this eBook yet.  You can find a full list of features here.

Introduction

Whenever I receive an invoice as a PDF document in my Inbox, I take a look at the Document Properties of the file to find out which tool was used to create that invoice. I'm always happy when I see that iText was used.

Next I check if the PDF is future-proof. That is: if it complies with the PDF/A standard, and if it can easily be interpreted by a machine. Usually that's not the case. That makes me less happy.

Only when I've performed these two simple checks, I look at the actual content of the invoice. Whether or not that makes me happy, depends on the amount I have to pay.

In this tutorial

  • I'll explain why conforming to the PDF/A standard is important,

  • I'll show you how you can assure that a machine can read and process the invoices you create, and

  • I'll introduce the Central User Guide for Electronic Invoicing in Germany (ZUGFeRD), a standard that was developed to meet these requirements.

Using some simple examples, I'll demonstrate how you can easily create ZUGFeRD-compliant invoices by applying some small changes to your iText-driven invoicing process.

E-book cover image
Image
eBook cover ZUGFeRD
book nid
1422
Subtitle
eBook

"ZUGFeRD: The Future of Invoicing" - updated to iText 7 and new add-ons

With the release of iText 7, some part of the first edition of the book "ZUGFeRD: The Future of Invoicing" became obsolete. In the meantime, we have been busy updating the content to iText 7 code and including new add-ons such as pdfHTML. Today, ZUGFeRD is still a very important standard for invoicing, that makes processing invoices much easier by combining EDI standards (Electronic Data Interchange, e.g. Cross Industry Invoice or CII) with the PDF/A-3 standard. The human-oriented representation using PDF and the machine-oriented EDI information stored within the PDF are the key strengths of ZUGFeRD.

In this book, I will :

  • explain why conforming to the PDF/A standard is important,

  • show you how you can assure that a machine can read and process the invoices you create, and

  • introduce the Central User Guide for Electronic Invoicing in Germany (ZUGFeRD), a standard that was developed to meet these requirements.

Using some simple examples, I'll demonstrate how you can easily create ZUGFeRD-compliant invoices by applying some small changes to your iText-driven invoicing process.

Why should you read this book and get started with ZUGFeRD invoices?

  • Sending ZUGFeRD invoices will save you a lot of time and money.

    From printing, envelopes and posting, only a single output format is required for customers who process invoices by manual or automated means. Payments are received in a timely, reliable way. SMBs can meet the requirements of large corporations without any agreement.

  • Receiving ZUGFeRD invoices enables you to implement fully automatic, error-free Accounts Payable workflows.

    Mail clients can automatically detect incoming messages with a ZUGFeRD attachment. Electronic banking services can be used to transfer invoices directly into payment orders.

  • You will save on storage space and paper.

    When archiving, sharing or reproducing invoices,the cost of book keeping and auditing may be dramatically reduced, with fewer errors for senders and receivers.

Quickly download this e-book for free and get started with ZUGFeRD-compliant invoices!

Article type
iText news

Release iText 5.5.7

Release of version 5.5.7 for iText 5 Core  brings several improvements for tables, forms, digital signatures,  create ZUGFeRD invoices, and much more.

Conclusion ZUGFeRD

In the first chapter of this book, I explained the issues I have with many invoices:

  • They aren't future proof,

  • They aren't accessible,

  • Either they can be read by humans but not by machines,

  • or the can be read by machines but not by humans.

In the second chapter, I explained how we can fix the first two problems: we can use iText to create invoices in the PDF/A format so that they are future proof; we can introduce Tagged PDF and conform with PDF/A level A to make our invoices accessible.

Chapter three introduced a simple database. We have used this database in all the following chapters.

In chapter four, we created invoices in the XML format so that the can be correctly interpreted by machines. These XML invoices conformed to the Comfort level of the data model of the ZUGFeRD standard.

In chapter 5 we created invoices that can be read by humans. We added the XML version of the invoice as an attachment.

Chapter 6 started as a side-track: we wrote some XSL to convert ZUGFeRD XML files to HTML, and we used CSS to introduce some styles and colors.

We used the HTML files in chapter 7 to create ZUGFeRD invoices with XML Worker.

We hope that this tutorial shows that it's not that difficult to create ZUGFeRD invoices. First you implement the ComfortProfile interface. You do so to provide all the information that needs to be on the invoice. Then you create a design for your invoices using XSL that converts the ZUGFeRD data model into an HTML file. Optionally, you can add some CSS to define colors and styles. Finally you use iText and XML Worker to combine all these different elements into a finalized ZUGFeRD invoice.

This is enterprise-grade technology that every company can afford, whether it's a large, medium or even a small business. There is no reason not to adopt ZUGFeRD as your standard for invoices. You can help realize world-wide implementation of the ZUGFeRD standard. This will yield financial, technical and operational benefits across the entire economy and across all borders. What are you waiting for? 

Get started with our ZUGFERD compliant invoicing PDF tool - pdfInvoice.

**Still have questions on ZUGFeRD? Don't hesitate to contact us.

book nid
1498
Subtitle
eBook

7. Creating PDF invoices (Comfort)

Converting XML to HTML, and HTML to PDF

Please take a look at the PdfInvoicesComfort example. We'll reuse some of the code from the example in chapter 6, but instead of merely a createHtml() method, we'll now call a createPdf() method:

public static void main(String[] args)
    throws SQLException, IOException,
    ParserConfigurationException, SAXException, TransformerException,
    DataIncompleteException, InvalidCodeException {
    LicenseKey.loadLicenseFile(
        System.getenv("ITEXT7_LICENSEKEY")
        + "/itextkey-html2pdf_typography.xml");
    File file = new File(DEST);
    file.getParentFile().mkdirs();
    PdfInvoicesComfort app = new PdfInvoicesComfort();
    PojoFactory factory = PojoFactory.getInstance();
    List invoices = factory.getInvoices();
    for (Invoice invoice : invoices) {
        app.createPdf(invoice,
                new FileOutputStream(String.format(DEST, invoice.getId())));
    }
    factory.close();
}

That createPdf() method will create an HTML file first, and then convert it to PDF:

public void createPdf(Invoice invoice, FileOutputStream fos)
    throws IOException, ParserConfigurationException,
    SAXException, TransformerException,
    DataIncompleteException, InvalidCodeException {
    IComfortProfile comfort =
        new InvoiceData().createComfortProfileData(invoice);
    InvoiceDOM dom = new InvoiceDOM(comfort);
    StreamSource xml = new StreamSource(
        new ByteArrayInputStream(dom.toXML()));
    StreamSource xsl = new StreamSource(new File(XSL));
    TransformerFactory factory = TransformerFactory.newInstance();
    Transformer transformer = factory.newTransformer(xsl);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    Writer htmlWriter = new OutputStreamWriter(baos);
    transformer.transform(xml, new StreamResult(htmlWriter));
    htmlWriter.flush();
    htmlWriter.close();
    byte[] html = baos.toByteArray();

    ZugferdDocument pdfDocument = new ZugferdDocument(
        new PdfWriter(fos), ZugferdConformanceLevel.ZUGFeRDComfort,
        new PdfOutputIntent("Custom", "", "http://www.color.org",
            "sRGB IEC61966-2.1", new FileInputStream(INTENT)));
    pdfDocument.addFileAttachment(
        "ZUGFeRD invoice", dom.toXML(), "ZUGFeRD-invoice.xml",
        PdfName.ApplicationXml, new PdfDictionary(), PdfName.Alternative);
    pdfDocument.setTagged();

    HtmlConverter.convertToPdf(
        new ByteArrayInputStream(html), pdfDocument, getProperties());
}

The body of this method consists of three parts:

  • Line 5-18 are copied from the createHtml() method we created in the previous chapter, but as you can see in line 19, we don't create an HTML file that is stored on disk. Instead, we keep the HTML in memory.

  • Line 21-28 start the same way as our example in chapter 5. We create a ZugferdDocument and we add the XML as an attachment. The line where we tell iText to tag the document isn't strictly necessary, but it's good practice.

  • Line 30-31 is new: in chapter 5, we created Paragraph and Table objects, and we added those object to a Document instance. In this case, we'll leave the creation of building blocks to the pdfHTML add-on. The add-on will convert the content of  <p> tags to Paragraph objects, the content of  <table> tags to the [blockcode] Table  objects, and so on.

There are a couple of caveats, though. As we created the HTML in memory, the relative links to resources such as images and CSS can't be resolved. That's why we define a ConverterProperties instance. That instance is obtained through the getProperties() method:

public ConverterProperties getProperties() {
    if (properties == null) {
        properties = new ConverterProperties().setBaseUri("resources/zugferd/");
    }
    return properties;
}

In the converter properties, we define a base URI. The resources/zugferd/ directory contains the logo.png and the invoice.css file. Without the base URI, the HTML to PDF conversion process would never know where to look for ./logo.png or ./css.html.

The ConverterProperties object can also be used to define other properties, such as the FontProvider. All ZUGFeRD documents are also PDF/A document, which means that all fonts need to be embedded. In this case, we used FreeSans as font, as defined in the first line of our CSS file: body { font-family: FreeSans; } Different fonts of the FreeSans font family are shipped with the pdfHTML add-on, and the default FontProvider knows where to find that font. If you want to use another font, you may need to create a custom FontProvider that tells iText where to find the fonts you need.

The final result

Figure 7.1 shows the resulting PDF. It looks much nicer than the invoices we produced in chapter 5, doesn't it?

Figure 7.1

Figure 7.1: a ZUGFeRD invoice created from HTML

If you look at the panel to the left, you can see the effect of the extra line we smuggled into our code (pdfDocument.setTagged();). As a result, the invoice is now also accessible. The HTML tables can now also be interpreted as tables by a PDF processor that understands tagged PDF. This is an example of a future-proof, archivable invoice that can be read by humans (including people who are visually impaired) as well as by machines.

Before we close, let's also take a look inside the PDF document. In figure 7.2, we see the root object of the PDF document, aka the catalog. We also see the /AF(Associated Files) and the /Names entry. We recognize the/EmbeddedFiles name tree that has a single element named "ZUGFeRD invoice". This refers to a dictionary of filetype /Filespec that is also referred to from the /AF array. The /AFRelationship is "Altermative", meaning that the PDF document and the attached XML are alternative presentations of the same content. This is required by the ZUGFeRD standard.

Figure 7.2

Figure 7.2: The Associated File

Figure 7.3 shows the XMP metadata. The document knows that it's a PDF/A-3B document because of the presence of pdfaid:part="3" andpdfaid:conformance="B" attributes in the rdf:Description. There are also a number of entries in the zf namespace, that define the profile (zf:ConformanceLevel = "COMFORT"), the name of the XML attachment (zf:DocumentFileName = "ZUGFeRD-invoice.xml"), the document type (zf:DocumentType = "INVOICE"), and the version of the XML schema for the invoice data (zf:Version = "1.0").

 

Fiture 7.3

Figure 7.3: The XMP Metadata

You don't have to worry about these Metadata values or the embedded XMP stream. The pdfInvoice add-on automatically takes care of creating this metadata.

book nid
1496
Subtitle
eBook

6. Creating HTML invoices

An XSL for Comfort XMLs

As explained in chapter 5, the XMLs created to comply with the Basic profile don't contain sufficient information to create the visual appearance of the corresponding invoice. The XML doesn't contain all the information that is needed if we want to render the line items.

This means that we could create an XSL file that takes the info from the ZUGFeRD XML and converts it into HTML. We'll do that in the HtmlInvoicesComfort example. For instance:

xsl:template match="/rsm:CrossIndustryDocument">
    <html>
    <head><link rel="stylesheet" type="text/css" href="invoice.css" /></head>
    <body>
        <img src="logo.png" alt="Das Company - logo" /><br />
        <xsl:apply-templates /></body>
    </html>
</xsl:template>

In this snippet, we match the root tag of the ZUGFeRD XML and we introduce <html>,<head> and <body>. We'll use a CSS to apply some styles and colors and an <img> tag to introduce a logo.

The <xsl:apply-templates /> instruction will deal with all the other components. For instance:

<xsl:template match="rsm:HeaderExchangedDocument">
    <h1 id="header"><xsl:value-of select="ram:Name" />
    <xsl:text> </xsl:text><xsl:value-of select="ram:ID" /></h1>
    <xsl:apply-templates select="ram:IssueDateTime" />
</xsl:template>

This XSL snippet in turn applies the templates for what's inside the <ram:IssueDateTime> tag. In this case, the following template is called:

<xsl:template match="udt:DateTimeString">
   <h2 id="date"><xsl:choose>
       <xsl:when test="@format='610'">
           <xsl:call-template name="YYYYMM">
               <xsl:with-param name="date" select="." />
           </xsl:call-template>
       </xsl:when>
       <xsl:when test="@format='616'">
           <xsl:call-template name="YYYYWW">
               <xsl:with-param name="date" select="." />
           </xsl:call-template>
       </xsl:when>
       <xsl:otherwise>
           <xsl:call-template name="YYYYMMDD">
               <xsl:with-param name="date" select="." />
           </xsl:call-template>
       </xsl:otherwise>
   </xsl:choose></h2>
</xsl:template>

This is what a switch looks like in XSL. Depending on the date format, one of the following templates is called:

<xsl:template name="YYYYMMDD">
    <xsl:param name="date" />
    <xsl:value-of select="substring($date,1,4)" 
    />-<xsl:value-of select="substring($date,5,2)" 
    />-<xsl:value-of select="substring($date,7,2)" />
</xsl:template>
<xsl:template name="YYYYMM">
    <xsl:param name="date" />
    <xsl:value-of select="substring($date,1,4)" 
    />-<xsl:value-of select="substring($date,5,2)" />
</xsl:template>
<xsl:template name="YYYYWW">
    <xsl:param name="date" />
    <xsl:value-of select="substring($date,1,4)" 
    />; week <xsl:value-of select="substring($date,5,2)" />
</xsl:template>

We won't go into further detail, but it shouldn't be difficult to extend the XSL so that it covers all the data you want to visualize. You can get some inspiration by looking at the XSL I wrote for this example and that results in invoices such as the one shown in figure 6.1.

FIgure 6.1

Figure 6.1: HTML version of an invoice

That's not a very attractive invoice yet, so let's introduce some styles and some color. We'll do that using CSS.

Adding some simple CSS

Please compare the result shown in figure 6.1 with the result shown in figure 6.2.

FIgure 6.2

Figure 6.2: HTML + CSS version of an invoice

Now we're getting somewhere, aren't we? In our XSL, we've added some id and some class attributes, so that we can refer to these elements from a simple CSS file:

body { font-family: FreeSans; }
#header { color: #008080; font-size: 18pt; font-weight: bold; }
#date { font-size: 16pt; }
#addresses { margin-top: 20pt; font-size: 11pt; }
#products { margin-top: 10pt; border: 3px solid #008080; }
#totals { margin-top: 10pt; border: 3px solid #008080; }
#wireinfo { margin-top: 10pt; margin-left: 72pt; }
.name { font-weight: bold; color: #008080; }
.total { font-weight: bold; color: #008080; }
.headerrow { background-color: #008080; color: #FFFFFF; }
.bold { font-weight: bold; }
.wireheader { font-weight: bold; text-align: left; }
th { padding: 2pt; font-weight: bold; text-align: center; }
td { padding: 2pt; }

Let's take a look at the Java code that was used to produce these HTML pages.

Transforming XML to HTML using XSL and Java

We start by defining some constants, more specifically the pattern for the paths of the resulting HTML files, and the paths to the XSL, CSS, and logo file.

public static final String DEST = "results/zugferd/html/comfort%05d.html";
public static final String XSL = "resources/zugferd/invoice.xsl";
public static final String CSS = "resources/zugferd/invoice.css";
public static final String LOGO = "resources/zugferd/logo.png";

In the main method, we copy the resources (the CSS and the image) to the directory where we'll generate our HTML. The rest of the code is similar to what we had in chapter 5: we loop over the invoices obtained from the PojoFactory and we call a createHtml() method.

public static void main(String[] args)
    throws SQLException, IOException,
        ParserConfigurationException, SAXException, TransformerException,
        DataIncompleteException, InvalidCodeException {
    LicenseKey.loadLicenseFile(
        System.getenv("ITEXT7_LICENSEKEY")
        + "/itextkey-html2pdf_typography.xml");
    File file = new File(DEST);
    file.getParentFile().mkdirs();
    File css = new File(CSS);
    copyFile(css, new File(file.getParentFile(), css.getName()));
    File logo = new File(LOGO);
    copyFile(logo, new File(file.getParentFile(), logo.getName()));
    HtmlInvoicesComfort app = new HtmlInvoicesComfort();
    PojoFactory factory = PojoFactory.getInstance();
    List invoices = factory.getInvoices();
    for (Invoice invoice : invoices) {
        app.createHtml(invoice,
            new FileWriter(String.format(DEST, invoice.getId())));
    }
    factory.close();
}

In the createHtml() method, we use the InvoiceData class to create an IComfortProfile instance. We then use standard Java code to convert XML using XSL.

public void createHtml(Invoice invoice, Writer writer)
    throws IOException, ParserConfigurationException, SAXException,
    DataIncompleteException, InvalidCodeException, TransformerException {
    IComfortProfile comfort =
        new InvoiceData().createComfortProfileData(invoice);
    InvoiceDOM dom = new InvoiceDOM(comfort);
    StreamSource xml = new StreamSource(new ByteArrayInputStream(dom.toXML()));
    StreamSource xsl = new StreamSource(new File(XSL));
    TransformerFactory factory = TransformerFactory.newInstance();
    Transformer transformer = factory.newTransformer(xsl);
    transformer.transform(xml, new StreamResult(writer));
    writer.flush();
    writer.close();
}

I'm adding the copyFile() method for the sake of completeness. We're copying the files from the resources to the output directory because we defined the paths to the CSS and the image using relative links.

private static void copyFile(File source, File dest) throws IOException {
    InputStream input = new FileInputStream(source);
    OutputStream output = new FileOutputStream(dest);
    byte[] buf = new byte[1024];
    int bytesRead;
    while ((bytesRead = input.read(buf)) > 0) {
        output.write(buf, 0, bytesRead);
    }
    input.close();
    output.close();
}

We could tweak the XSL and the CSS to produce an even nicer HTML document, but as I explained: invoices in HTML are outside the scope of ZUGFeRD. Let's move on to the next chapter to find out why we introduced this intermezzo.

book nid
1481
Subtitle
eBook

5. Creating PDF invoices (Basic profile)

Creating PDF from scratch

In this chapter, we'll discuss a single example: PdfInvoicesBasic. In this example we'll create a PDF document for every invoice stored in our database. The PDF documents will be ZUGFeRD invoices using the Basic profile.

Let's start with the different constants defined in this class:

public static final String DEST = "results/zugferd/pdf/basic%05d.pdf";
public static final String ICC = "resources/color/sRGB_CS_profile.icm";
public static final String REGULAR = "resources/fonts/OpenSans-Regular.ttf";
public static final String BOLD = "resources/fonts/OpenSans-Bold.ttf";
public static final String NEWLINE = "\n";

We recognize the pattern for the destination files, the color profile (as discussed in chapter 2), two font files, and a string with a newline character.

The main method of this example is very straightforward:

public static void main(String[] args)
    throws IOException, ParserConfigurationException, SQLException,
        SAXException, TransformerException, ParseException
        DataIncompleteException, InvalidCodeException {
    LicenseKey.loadLicenseFile(
        System.getenv("ITEXT7_LICENSEKEY")
        + "/itextkey-html2pdf_typography.xml");
    File file = new File(DEST);
    file.getParentFile().mkdirs();
    PdfInvoicesBasic app = new PdfInvoicesBasic();
    PojoFactory factory = PojoFactory.getInstance();
    List invoices = factory.getInvoices();
    for (Invoice invoice : invoices) {
        app.createPdf(invoice);
    }
    factory.close();
}

In this method, we create an instance of the PdfInvoicesBasic class; we get a List of Invoice objects from the PojoFactory; for every invoice, we call the createPdf() method.

public void createPdf(Invoice invoice)
    throws ParserConfigurationException, SAXException, TransformerException,
    IOException, ParseException, DataIncompleteException, InvalidCodeException {

    String dest = String.format(DEST, invoice.getId());

    // Create the XML
    InvoiceData invoiceData = new InvoiceData();
    IBasicProfile basic = invoiceData.createBasicProfileData(invoice);
    InvoiceDOM dom = new InvoiceDOM(basic);

    // Create the ZUGFeRD document
    ZugferdDocument pdfDocument = new ZugferdDocument(
        new PdfWriter(dest), ZugferdConformanceLevel.ZUGFeRDBasic,
        new PdfOutputIntent("Custom", "", "http://www.color.org",
            "sRGB IEC61966-2.1", new FileInputStream(ICC)));
    pdfDocument.addFileAttachment(
        "ZUGFeRD invoice", dom.toXML(), "ZUGFeRD-invoice.xml",
        PdfName.ApplicationXml, new PdfDictionary(), PdfName.Alternative);

    // Create the document
    Document document = new Document(pdfDocument);
    document.setFont(PdfFontFactory.createFont(REGULAR, true))
            .setFontSize(12);
    PdfFont bold = PdfFontFactory.createFont(BOLD, true);

    // Add the header
    document.add(
        new Paragraph()
        .setTextAlignment(TextAlignment.RIGHT)
        .setMultipliedLeading(1)
            .add(new Text(String.format("%s %s\n", basic.getName(), basic.getId()))
                .setFont(bold).setFontSize(14))
            .add(convertDate(basic.getDateTime(), "MMM dd, yyyy")));
    // Add the seller and buyer address
    document.add(getAddressTable(basic, bold));
    // Add the line items
    document.add(getLineItemTable(invoice, bold));
    // Add the grand totals
    document.add(getTotalsTable(
        basic.getTaxBasisTotalAmount(), basic.getTaxTotalAmount(),
        basic.getGrandTotalAmount(), basic.getGrandTotalAmountCurrencyID(),
        basic.getTaxTypeCode(), basic.getTaxApplicablePercent(),
        basic.getTaxBasisAmount(), basic.getTaxCalculatedAmount(),
        basic.getTaxCalculatedAmountCurrencyID(), bold));
    // Add the payment info
    document.add(getPaymentInfo(basic.getPaymentReference(),
        basic.getPaymentMeansPayeeFinancialInstitutionBIC(),
        basic.getPaymentMeansPayeeAccountIBAN()));

    document.close();
}

This createPdf() method consists of different parts:

  • We construct a file name (line 5) and we use the InvoiceData class (line 8) that was discussed in chapter 4 to create an IBasicProfile instance (line 9). We use this IBasicProfile instance to create an InvoiceDOM object (line 8). InvoiceDOM is one of the classes available in the pdfInvoice add-on.

  • We construct a ZugferdDocument instance (line 13) and we set the conformance level to ZUGFeRDBasic for the basic profile (line 14). We also add the output intent (line 15-16). We add the XML invoice as an attachment (line 17-19).

  • Then we create the document (line 22), and we add the content: a header (line 28-34), the address information of the seller and the buyer (line 36), the line items (line 38), the grand total and the tax information (line 40-45), and the payment information (line 47-49).

We create the header paragraph in the createPdf() method (lines 28-34), but we're using helper methods for the other content. Let's take a closer look at those methods.

Adding the seller and buyer addresses

Creating a PDF from scratch using iText is easy, but not trivial. It's easy, because you can use many different high-level objects, but it's not trivial as you have to create a design by writing code. In chapter 7, we'll see an alternative way to create PDF invoices that doesn't require you to write much code.

Most of the data that needs to be rendered is available through the IBasicProfile interface. See for instance the getAddressTable() method:

public Table getAddressTable(IBasicProfile basic, PdfFont bold) {
    Table table = new Table(new UnitValue[]{
        new UnitValue(UnitValue.PERCENT, 50),
        new UnitValue(UnitValue.PERCENT, 50)})
            .setWidthPercent(100);
    table.addCell(getPartyAddress("From:",
        basic.getSellerName(),
        basic.getSellerLineOne(),
        basic.getSellerLineTwo(),
        basic.getSellerCountryID(),
        basic.getSellerPostcode(),
        basic.getSellerCityName(),
        bold));
    table.addCell(getPartyAddress("To:",
        basic.getBuyerName(),
        basic.getBuyerLineOne(),
        basic.getBuyerLineTwo(),
        basic.getBuyerCountryID(),
        basic.getBuyerPostcode(),
        basic.getBuyerCityName(),
        bold));
    table.addCell(getPartyTax(basic.getSellerTaxRegistrationID(),
        basic.getSellerTaxRegistrationSchemeID(), bold));
    table.addCell(getPartyTax(basic.getBuyerTaxRegistrationID(),
        basic.getBuyerTaxRegistrationSchemeID(), bold));
    return table;
}

We create a table with two columns, and we use convenience methods to create the Cell instances:

public Cell getPartyAddress(String who, String name,
    String line1, String line2, String countryID,
    String postcode, String city, PdfFont bold) {
    Paragraph p = new Paragraph()
        .setMultipliedLeading(1.0f)
        .add(new Text(who).setFont(bold)).add(NEWLINE)
        .add(name).add(NEWLINE)
        .add(line1).add(NEWLINE)
        .add(line2).add(NEWLINE)
        .add(String.format("%s-%s %s", countryID, postcode, city));
    Cell cell = new Cell()
        .setBorder(Border.NO_BORDER)
        .add(p);
    return cell;
}
public Cell getPartyTax(String[] taxId, String[] taxSchema, PdfFont bold) {
    Paragraph p = new Paragraph()
        .setFontSize(10).setMultipliedLeading(1.0f)
        .add(new Text("Tax ID(s):").setFont(bold));
    if (taxId.length == 0) {
        p.add("\nNot applicable");
    }
    else {
        int n = taxId.length;
        for (int i = 0; i 

With these helper methods, we already have the "upper part" of the invoice as shown in Figure 5.1.

Figure 5.1

Figure 5.1: first part of the invoice

We'll also use a Table to render the invoice lines.

Adding invoice lines

The part with the invoice lines is a Table with six columns that is created like this:

public Table getLineItemTable(Invoice invoice, PdfFont bold) {
    Table table = new Table(
        new UnitValue[]{
            new UnitValue(UnitValue.PERCENT, 43.75f),
            new UnitValue(UnitValue.PERCENT, 12.5f),
            new UnitValue(UnitValue.PERCENT, 6.25f),
            new UnitValue(UnitValue.PERCENT, 12.5f),
            new UnitValue(UnitValue.PERCENT, 12.5f),
            new UnitValue(UnitValue.PERCENT, 12.5f)})
        .setWidthPercent(100)
    .setMarginTop(10).setMarginBottom(10);
    table.addHeaderCell(createCell("Item:", bold));
    table.addHeaderCell(createCell("Price:", bold));
    table.addHeaderCell(createCell("Qty:", bold));
    table.addHeaderCell(createCell("Subtotal:", bold));
    table.addHeaderCell(createCell("VAT:", bold));
    table.addHeaderCell(createCell("Total:", bold));
    Product product;
    for (Item item : invoice.getItems()) {
        product = item.getProduct();
        table.addCell(createCell(product.getName()));
        table.addCell(createCell(
            InvoiceData.format2dec(InvoiceData.round(product.getPrice())))
                .setTextAlignment(TextAlignment.RIGHT));
        table.addCell(createCell(String.valueOf(item.getQuantity()))
            .setTextAlignment(TextAlignment.RIGHT));
        table.addCell(createCell(
            InvoiceData.format2dec(InvoiceData.round(item.getCost())))
            .setTextAlignment(TextAlignment.RIGHT));
        table.addCell(createCell(
            InvoiceData.format2dec(InvoiceData.round(product.getVat())))
            .setTextAlignment(TextAlignment.RIGHT));
        table.addCell(createCell(
            InvoiceData.format2dec(InvoiceData.round(
                item.getCost() + ((item.getCost() * product.getVat()) / 100))))
            .setTextAlignment(TextAlignment.RIGHT));
    }
    return table;
}

Again we use some helper methods to create Cell objects:

public Cell createCell(String text) {
    return new Cell().setPadding(0.8f)
        .add(new Paragraph(text)
            .setMultipliedLeading(1));
}
public Cell createCell(String text, PdfFont font) {
    return new Cell().setPadding(0.8f)
        .add(new Paragraph(text)
            .setFont(font).setMultipliedLeading(1));
}

The result looks like figure 5.2:

Figure 5.2

Figure 5.2: rendering the line items of an invoice

We need yet another table for the tax totals and the grand total.

Adding the totals

We add a table with all the totals like this:

public Table getTotalsTable(String tBase, String tTax, String tTotal,
    String tCurrency, String[] type, String[] percentage, String base[],
    String tax[], String currency[], PdfFont bold) {
    Table table = new Table(
        new UnitValue[]{
            new UnitValue(UnitValue.PERCENT, 8.33f),
            new UnitValue(UnitValue.PERCENT, 8.33f),
            new UnitValue(UnitValue.PERCENT, 25f),
            new UnitValue(UnitValue.PERCENT, 25f),
            new UnitValue(UnitValue.PERCENT, 25f),
            new UnitValue(UnitValue.PERCENT, 8.34f)})
        .setWidthPercent(100);
    table.addCell(createCell("TAX:", bold));
    table.addCell(createCell("%", bold)
        .setTextAlignment(TextAlignment.RIGHT));
    table.addCell(createCell("Base amount:", bold));
    table.addCell(createCell("Tax amount:", bold));
    table.addCell(createCell("Total:", bold));
    table.addCell(createCell("Curr.:", bold));
    int n = type.length;
    for (int i = 0; i 

The code is very similar to what we did for the line items table. The only thing that is out of the ordinary, is that we create a cell that spans two columns in line 24-25. Figure 5.3 shows the result.

Figure 5.3

Figure 5.3: rendering the totals

We're almost there. There only one piece of content missing.

Adding the payment info

Adding the payment info is just a matter of creating a Paragraph:

public Paragraph getPaymentInfo(String ref, String[] bic, String[] iban) {
    Paragraph p = new Paragraph(String.format(
        "Please wire the amount due to our bank account using "
        + " the following reference: %s",
        ref));
    int n = bic.length;
    for (int i = 0; i 

Note that we made some assumptions about the payment means. ZUGFeRD allows for different payment types, but we assumed that payments have to be done by bank wire. The result is shown in figure 5.4:

Figure 5.4

Figure 5.4: payment info

In the createPdf() method, we combined all this content with a PDF invoice as result.

The final result

The goal of this example was to create a proof of concept, and Figure 5.5 shows the final result.

Figure 5.5

Figure 5.5: the resulting invoice

People who want to process the invoice manually, can do so; if they don't open the attachment panel, they won't even notice that there's an XML attachment inside. People who want to process the invoice automatically, can extract the XML or have their software extract the XML for processing.

I am aware that this invoice doesn't look very sexy. We could create tables with rounded borders, introduce a logo and some colors, we could add an extra sheet with terms-of-use, and so on, but that would lead us too far. In chapter 7, we'll discover that there is an alternative way to create PDF invoices, but let's take a look at some HTML first.

book nid
1462
Subtitle
eBooks

4. Creating XML Invoices with iText

Interfaces for the Basic and Comfort profile

iText's pdfInvoice add-on ships with two interfaces that can be found in the com.itextpdf.text.zugferd.profiles package (shipped with the pdfInvoice add-on):

  • IBasicProfile: contains 56 get() methods for you to implement.

  • IComfortProfile extends the IBasicProfile interface and adds 89 more methods for you to implement (144 in total).

These methods look like this:

public String getSellerName();
public String getSellerPostcode();
public String getSellerLineOne();
public String getSellerLineTwo();
public String getSellerCityName();
public String getSellerCountryID();
public String[] getSellerTaxRegistrationID();
public String[] getSellerTaxRegistrationSchemeID();

As we're dealing with XML and as XML consists of data stored as text, most of the methods expect that you return String values, or arrays of String values, even when the data consists of numbers. In cases where dates or Boolean values are involved, the method has a Date, a Date[], a boolean, a Boolean[], or a Boolean[][] as return type.

iText ships with an implementation of these interfaces: BasicProfileImp implements the IBasicProfile interface; ComfortProfileImp extends BasicProfileImp and implements the IComfortProfile interface. These classes store all the data in boolean, Date, String, List, List, List, List, or List member-variables and provide set() methods to populate these variables. For instance:

public void setSellerName(String sellerName) {
    this.sellerName = sellerName;
}
public void setSellerPostcode(String sellerPostcode) {
    this.sellerPostcode = sellerPostcode;
}
public void setSellerLineOne(String sellerLineOne) {
    this.sellerLineOne = sellerLineOne;
}
public void setSellerLineTwo(String sellerLineTwo) {
    this.sellerLineTwo = sellerLineTwo;
}
public void setSellerCityName(String sellerCityName) {
    this.sellerCityName = sellerCityName;
}
public void setSellerCountryID(String sellerCountryID) {
    this.sellerCountryID = sellerCountryID;
}
public void addSellerTaxRegistration(String schemeID, String taxId) {
    sellerTaxRegistrationSchemeID.add(schemeID);
    sellerTaxRegistrationID.add(taxId);
}

The provided values are used to implement the getter methods defined in the interface:

public String getSellerName() {
    return sellerName;
}
public String getSellerPostcode() {
    return sellerPostcode;
}
public String getSellerLineOne() {
    return sellerLineOne;
}
public String getSellerLineTwo() {
    return sellerLineTwo;
}
public String getSellerCityName() {
    return sellerCityName;
}
public String getSellerCountryID() {
    return sellerCountryID;
}
public String[] getSellerTaxRegistrationID() {
    return to1DArray(sellerTaxRegistrationID);
}
public String[] getSellerTaxRegistrationSchemeID() {
    return to1DArray(sellerTaxRegistrationSchemeID);
}

Not all the getters need to be fully implemented. Some data is optional in the Basic and Comfort profile. For instance, it is perfectly OK to implement some of the methods like this:

public Date getBillingStartDateTime() {
    return null;
}
public String getBillingStartDateTimeFormat() {
    return null;
}
public Date getBillingEndDateTime() {
    return null;
}
public String getBillingEndDateTimeFormat() {
    return null;
}

How you implement these interfaces will largely depend on the CRM you're using. You'll have to query its database and use the results of that query to implement the methods of the interface corresponding with the profile you want to support.

Getting and setting the data

In this tutorial, we'll use BasicProfileImp and ComfortProfileImp (the IBasicProfile and IComfortProfile implementations that ship with iText) to store the information from the database we've discussed in chapter 3. See the InvoiceData class that we'll use in the next handful of examples.

public InvoiceData() {
}
public IBasicProfile createBasicProfileData(Invoice invoice) {
    BasicProfileImp profileImp = new BasicProfileImp();
    importData(profileImp, invoice);
    importBasicData(profileImp, invoice);
    return profileImp;
}
public IComfortProfile createComfortProfileData(Invoice invoice) {
    ComfortProfileImp profileImp = new ComfortProfileImp();
    importData(profileImp, invoice);
    importComfortData(profileImp, invoice);
    return profileImp;
}

We don't pass any data to the InvoiceData constructor. Instead, we pass our Invoice POJO to the createBasicProfileData() or the createComfortProfile() method to obtain an IBasicProfile or IComfortProfile implementation.

Internally, the default BasicProfileImp or ComfortProfileImp implementations are used. These data containers are populated using the importData(), importBasicData() and importComfortData() methods.

Basic and Comfort profile

The importData() method deals with data that is relevant for both the Basic and the Comfort profile:

public void importData(BasicProfileImp profileImp, Invoice invoice) {
    profileImp.setTest(true);
    profileImp.setId(String.format("I/%05d", invoice.getId()));
    profileImp.setName("INVOICE");
    profileImp.setTypeCode(DocumentTypeCode.COMMERCIAL_INVOICE);
    profileImp.setDate(invoice.getInvoiceDate(), DateFormatCode.YYYYMMDD);
    profileImp.setSellerName("Das Company");
    profileImp.setSellerLineOne("ZUG Business Center");
    profileImp.setSellerLineTwo("Highway 1");
    profileImp.setSellerPostcode("9000");
    profileImp.setSellerCityName("Ghent");
    profileImp.setSellerCountryID("BE");
    profileImp.addSellerTaxRegistration(
        TaxIDTypeCode.FISCAL_NUMBER, "201/113/40209");
    profileImp.addSellerTaxRegistration(TaxIDTypeCode.VAT, "BE123456789");
    Customer customer = invoice.getCustomer();
    profileImp.setBuyerName(String.format("%s, %s",
        customer.getLastName(), customer.getFirstName()));
    profileImp.setBuyerPostcode(customer.getPostalcode());
    profileImp.setBuyerLineOne(customer.getStreet());
    profileImp.setBuyerCityName(customer.getCity());
    profileImp.setBuyerCountryID(customer.getCountryId());
    profileImp.setPaymentReference(String.format("%09d", invoice.getId()));
    profileImp.setInvoiceCurrencyCode("EUR");
}

As you can see, we add some of the data, such as the name of the seller, in a hard-coded way. Obviously, this should be avoided in a real-world implementation.

We also use some constants such as DocumentTypeCode.COMMERCIAL_INVOICE, DateFormatCode.YYYYMMDD, and TaxIDTypeCode.VAT. DocumentTypeCode, DateFormatCode, TaxIDTypeCode and many other code list classes can be found in the com.itextpdf.text.zugferd.checkers packages. They all implement the CodeValidation class. This abstract class contains a check() method that throws an InvalidCodeException if the wrong code is provided. We'll look at these checkers in more detail in a moment.

Basic profile

All data that is necessary for the Basic profile is also necessary for the Comfort profile, but due to some differences between the Basic and Comfort profile, the implementation of these profiles requires different setters in iText.

public void importBasicData(BasicProfileImp profileImp, Invoice invoice) {
    profileImp.addNote(new String[]{
        "This is a test invoice.\nNothing on this invoice is real."
        + "\nThis invoice is part of a tutorial."});
    profileImp.addPaymentMeans("", "", "BE 41 7360 0661 9710",
        "", "", "KREDBEBB", "", "KBC");
    profileImp.addPaymentMeans("", "", "BE 56 0015 4298 7888",
        "", "", "GEBABEBB", "", "BNP Paribas");
    Map taxes = new TreeMap();
    double tax;
    for (Item item : invoice.getItems()) {
        tax = item.getProduct().getVat();
        if (taxes.containsKey(tax)) {
            taxes.put(tax, taxes.get(tax) + item.getCost());
        }
        else {
            taxes.put(tax, item.getCost());
        }
        profileImp.addIncludedSupplyChainTradeLineItem(
            format4dec(item.getQuantity()), "C62", item.getProduct().getName());
    }
    double total, tA;
    double ltN = 0;
    double ttA = 0;
    double gtA = 0;
    for (Map.Entry t : taxes.entrySet()) {
        tax = t.getKey();
        total = round(t.getValue());
        gtA += total;
        tA = round((100 * total) / (100 + tax));
        ttA += (total - tA);
        ltN += tA;
        profileImp.addApplicableTradeTax(format2dec(total - tA), "EUR",
            TaxTypeCode.VALUE_ADDED_TAX, format2dec(tA), "EUR", format2dec(tax));
    }
    profileImp.setMonetarySummation(format2dec(ltN), "EUR",
        format2dec(0), "EUR",
        format2dec(0), "EUR",
        format2dec(ltN), "EUR",
        format2dec(ttA), "EUR",
        format2dec(gtA), "EUR");
}

This is typical for the Basic profile:

  • The Basic profile allows free text in the header, see line 2-4: "This is a test invoice. Nothing on this invoice is real. This invoice is part of a tutorial."

  • You don't need that much information regarding the payment means.

  • You don't need that much information regarding the line items (the invoice lines).

Note that we loop over the different Item objects to calculate the monetary summation:

  • the total amount of the line items (line 36),

  • the tax basis amount (line 39), which is total of the line items after taking into account the charge total amount (line 37) and the allowance total amount (line 38), which are 0 in this case,

  • the total tax amount (line 40), and

  • the grand total (line 41).

Note that we use some helper methods to format numbers and percentages:

public static double round(double d) {
    d = d * 100;
    long tmp = Math.round(d);
    return (double) tmp / 100;
}
public static String format2dec(double d) {
    return String.format("%.2f", d);
}
public static String format4dec(double d) {
    return String.format("%.4f", d);
}

In some cases, numbers need to be expressed using 2 decimals; in other cases, numbers need to be expressed using 4 decimals. Once you create the XML, iText will throw an InvalidCodeException if you pass a numeric value with the wrong number of decimals.

Comfort profile

When we look at the importComfortData() method, we see that much more data can be provided. For instance: the notes in the header need to consist of qualified text. This means that we have to describe what the note is about using a code. In this case, we used FreeTextSubjectCode.REGULATORY_INFORMATION (see line 5).

public void importComfortData(ComfortProfileImp profileImp, Invoice invoice) {
    profileImp.addNote(new String[]{
        "This is a test invoice.\nNothing on this invoice is real."
        + "\nThis invoice is part of a tutorial."},
        FreeTextSubjectCode.REGULATORY_INFORMATION);
    profileImp.addPaymentMeans(
        PaymentMeansCode.PAYMENT_TO_BANK_ACCOUNT,
        new String[]{"This is the preferred bank account."},
        "", "",
        "", "",
        "BE 41 7360 0661 9710", "", "",
        "", "", "",
        "KREDBEBB", "", "KBC");
    profileImp.addPaymentMeans(
        PaymentMeansCode.PAYMENT_TO_BANK_ACCOUNT,
        new String[]{"Use this as an alternative account."},
        "", "",
        "", "",
        "BE 56 0015 4298 7888", "", "",
        "", "", "",
        "GEBABEBB", "", "BNP Paribas");
    Map taxes = new TreeMap();
    double tax;
    int counter = 0;
    for (Item item : invoice.getItems()) {
        counter++;
        tax = item.getProduct().getVat();
        if (taxes.containsKey(tax)) {
            taxes.put(tax, taxes.get(tax) + item.getCost());
        }
        else {
            taxes.put(tax, item.getCost());
        }
        profileImp.addIncludedSupplyChainTradeLineItem(
            String.valueOf(counter),
            null,
            format4dec(item.getProduct().getPrice()), "EUR", null, null,
            null, null, null, null,
            null, null, null, null,
            format4dec(item.getQuantity()), "C62",
            new String[]{TaxTypeCode.VALUE_ADDED_TAX},
            new String[1],
            new String[]{TaxCategoryCode.STANDARD_RATE},
            new String[]{format2dec(item.getProduct().getVat())},
            format2dec(item.getCost()), "EUR",
            null, null,
            String.valueOf(item.getProduct().getId()), null,
            item.getProduct().getName(), null
        );
    }
    double total, tA;
    double ltN = 0;
    double ttA = 0;
    double gtA = 0;
    for (Map.Entry t : taxes.entrySet()) {
        tax = t.getKey();
        total = round(t.getValue());
        gtA += total;
        tA = round((100 * total) / (100 + tax));
        ttA += (total - tA);
        ltN += tA;
        profileImp.addApplicableTradeTax(
            format2dec(total - tA), "EUR", TaxTypeCode.VALUE_ADDED_TAX,
            null, format2dec(tA), "EUR",
            TaxCategoryCode.STANDARD_RATE, format2dec(tax));
    }
    profileImp.setMonetarySummation(format2dec(ltN), "EUR",
        format2dec(0), "EUR",
        format2dec(0), "EUR",
        format2dec(ltN), "EUR",
        format2dec(ttA), "EUR",
        format2dec(gtA), "EUR");
}

When browsing the code, you see that we can leave certain values empty ("") or pass null as a value. This isn't always true. iText will throw a DataIncompleteException when you try to make an XML based on incomplete data. As explained earlier, an InvalidDataException is thrown when the wrong data is provided, although iText doesn't check all the codes you pass.

Validation of the data

Table 4.1 shows an overview of the checker classes that will be used once iText creates an XML file based on your implementation of the IBasicProfile or the IComfortProfile interface:

Table 4.1: Checker classes for the Basic and Comfort profile
Checker class Description Profile

NumberChecker

Can be used to check if a number is an integer or a decimal; if a decimal, checks if it has two or four decimals.

Basic and higher

CountryCode

Just checks if the code consists of two uppercase letters (no numbers). It doesn't check if the country code actually exists. That's your responsibility.

Basic and higher

CurrencyCode

Just checks if the code consists of three uppercase letters (no numbers). It doesn't check if the currency code actually exists. That's your responsibility.

Basic and higher

DateFormatCode

Contains the three acceptable date formats YYYYMMDD (code 102), YYYYMM (code 610) and YYYYWW (code 616). This class also allows you to convert a Date to a String when given a format, and vice-versa.

Basic and higher

DocumentTypeCode

Could be COMMERCIAL_INVOICE (code 380), DEBIT_NOTE_FINANCIAL_ADJUSTMENT (code 38) and SELF_BILLED_INVOICE (code 389). iText will check if you're using the right profile for the document type.

Basic and higher

LanguageCode

Just checks if the code consists of two lowercase letters (no numbers). It doesn't check if the language code actually exists. That's your responsibility.

Basic and higher

MeasurementUnitCode

Contains constants for every possible measurement unit and checks if a code that was provided is one of these values.

Basic and higher

TaxIDTypeCode

Contains the codes for two types of tax ids (VAT and FISCAL_NUMBER) and checks if the code that was provided is one of these values.

Basic and higher

TaxTypeCode

Contains the codes for three types of tax (VALUE_ADDED_TAX, INSURANCE_TAX and TAX_ON_REPLACEMENT_PART) and checks if the code that was provided is one of these values

Basic and higher

FreeTextSubjectCode

Contains constants for every possible free text subject (to make it qualified text) and checks if the code that was provided is one of these values.

Comfort and higher

GlobalIdentifierCode

Contains a handful of frequently used codes, but only checks if the value consists of four numeric values.

Comfort and higher

PaymentMeansCode

Contains constants for all the possible payment means and checks if a code that was provided is one of these values.

Comfort and higher

TaxCategoryCode

Contains the codes for different tax categories and checks if a code that was provided is one of these values.

Comfort and higher

Text also has checkers that are only important in the context of the Extended profile, but iText doesn't generate XMLs using the Extended profile. Companies that need the Extended profile are assumed to already use specialized EDI software that creates XML that complies with the requirements of the Extended profile.

Let's finish this chapter by creating a series of XML files that comply with the Comfort profile.

Creating an XML file with iText

Once you have an implementation of the IBasicProfile or the IComfortProfile interface, you can use it to create an InvoiceDOM object. The actual XML is created as a byte array when you use the toXML() method. This is shown in the XmlInvoicesComfort example:

public static void main(String[] args)
    throws SQLException, ParserConfigurationException, SAXException, IOException,
        TransformerException, DataIncompleteException, InvalidCodeException {
    LicenseKey.loadLicenseFile(
        System.getenv("ITEXT7_LICENSEKEY")
        + "/itextkey-html2pdf_typography.xml");
    File file = new File(DEST);
    file.getParentFile().mkdirs();
    PojoFactory factory = PojoFactory.getInstance();
    List invoices = factory.getInvoices();
    InvoiceData invoiceData = new InvoiceData();
    IBasicProfile comfort;
    InvoiceDOM dom;
    for (Invoice invoice : invoices) {
        comfort = invoiceData.createComfortProfileData(invoice);
        dom = new InvoiceDOM(comfort);
        byte[] xml = dom.toXML();
        FileOutputStream fos = new FileOutputStream(
            String.format(DEST, invoice.getId()));
        fos.write(xml);
        fos.flush();
        fos.close();
    }
    factory.close();
}

Observe that we have to load a license key for the pdfInvoice add-on (line 4). If you don't have a license key, you should get a trial license.

Just like in the example from chapter 3 where we tested the database, we use the PojoFactory to get a list of invoices and we loop over every Invoice object. We use the InvoiceData class that was already discussed to create a IComfortProfile. We pass this profile to the InvoiceDOM constructor as if it were a IBasicProfile instance, but InvoiceDOM is smart enough to see that it's actually a IComfortProfile instance.

In this case, we want the XML as a file, so we write the byte[] to a FileOutputStream. Figures 4.1, 4.2 and 4.3 show what such an XML looks like.

XML

We're halfway creating a ZUGFeRD invoice: we already have the XML, now we need to create the PDF. Go to the next chapter to discover more. 

 

 

book nid
1458
Subtitle
eBook
Contact

Still have questions? 

We're happy to answer your questions. Reach out to us and we'll get back to you shortly.

Contact us
Stay updated

Join 11,000+ subscribers and become an iText PDF expert by staying up to date with our new products, updates, tips, technical solutions and happenings.

Subscribe Now