Barcodes with iText, ZXing & Okapi

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

A barcode on a business document has two jobs that pull in different directions. It must be machine readable, which means exact module sizes, a quiet zone around it and enough contrast, and it must sit inside a layout that was designed for people. In a PDF the best way to satisfy both is to draw the symbol as vector graphics: every dark module is a small filled rectangle in the page content, so the code stays razor sharp at any zoom level and at any printer resolution, and the file stays small. This page shows three Java libraries that can do that and how to combine them with iText 9.8: the barcodes module of iText itself, the ZXing ("Zebra Crossing") encoder, and Okapi Barcode, which covers many symbologies the other two do not. The running example is the Bookshelf Billing invoice, which carries an invoice number as a Code 128 barcode, an EAN-13 per book line and a QR code with the address of the online copy. What a symbology is, what error-correction levels, quiet zones and module sizes mean, and when to use vector or raster output is explained in Barcodes & QR Codes: Fundamentals, and the regulated codes that go on invoices (VeriFactu, TicketBAI, EPC and Swiss QR) in Regulated QR Codes on Invoices & Payments. Barcodes in JasperReports templates are in Barcodes in JasperReports.

Versions are stated as of 2026-10-09: iText Core 9.8.0 and pdfHTML 6.3.4 on Java 21, ZXing 3.5.4 and Okapi Barcode 0.5.6. Every example on this page was compiled and run against exactly these versions. The resulting PDFs were inspected for the absence of image objects (all symbols are vector content), and every symbol was then rendered to a bitmap at 300 dpi with Apache PDFBox 3.0.8 and decoded again with ZXing’s reader, which is the test shown at the end of the page. The Code 128, EAN-13, Code 39, QR, PDF417 and Data Matrix symbols decoded to the text that had been encoded; Okapi’s GS1 Code 128 and GS1 Data Matrix decoded to the application identifiers and values that had been given. No scanner hardware or Acrobat was used, so how a particular handheld reader behaves with the printed output was not tested.

Which library for which job

Library What it gives you Output you work with Licence

iText barcodes module

Code 128 (including GS1-128), EAN-13, EAN-8, UPC-A, UPC-E and supplements, Code 39, Codabar, Interleaved 2 of 5, MSI, POSTNET, PDF417, Data Matrix and QR

A PdfFormXObject (vector) or direct painting on a PdfCanvas

AGPLv3 or commercial, the same as iText Core

ZXing core 3.5.4

Encoders for QR, Data Matrix, Aztec, PDF417, Code 128, Code 39, Code 93, EAN, UPC, ITF and Codabar, and the readers for the same formats

A BitMatrix of dark and light modules, which you paint yourself

Apache-2.0

Okapi Barcode 0.5.6

More than 50 symbology classes, including GS1 DataBar, composite symbols, MaxiCode, Code 16K, many postal codes (Royal Mail, Japan Post, USPS Intelligent Mail, KIX, Australia Post), Micro QR, Swiss QR code and GS1 data entry with application identifiers in square brackets

A list of Rectangle (and hexagons or circles for MaxiCode), plus text boxes, which you paint yourself

Apache-2.0

The licence of the iText column matters: the barcodes module is part of iText Core, so a service that embeds it without a commercial licence must itself be offered under the AGPLv3 (see Introduction & Approaches for the consequences). ZXing and Okapi are Apache-2.0 and can be used in either case. That is a real reason to prefer ZXing or Okapi for the encoder in a closed-source service that draws on a different PDF library, for instance PDFBox. The licences were read from the pom.xml files of the Okapi 0.5.6 and ZXing 3.5.4 artifacts.

The practical rule that follows from the table is short. Use the iText module when you are already in an iText document and need one of its symbologies: it needs no extra dependency and its output is a ready-to-place object. Use ZXing when you need control over the matrix (colours, rounded modules, a logo in the centre of a QR code, SVG for the web, or a licence that is not AGPL) or a format iText lacks, such as Aztec or Code 93. Use Okapi for GS1 symbologies, postal codes and anything exotic. Barcode4J, which JasperReports uses, is the subject of the JasperReports page of this section, and the older Barbecue library is abandoned.

What changed since iText 5 and the old Barcode classes

Books written for iText 1 to 5 (among them iText in Action, which was consulted for the concept order of this page and is not reproduced) create a barcode with com.itextpdf.text.pdf.Barcode128 or BarcodeEAN, give it a PdfContentByte, and call createImageWithBarcode to obtain an Image that is then added to a Document or a PdfPCell. That works with the com.lowagie.text packages of iText 1 and 2 and with the com.itextpdf.text packages of iText 5; the packages were removed in iText 7, which is a rewrite, and the licence changed from MPL and LGPL to AGPLv3 or commercial (finding O1 of this section’s source analysis). In iText 7, 8 and 9 the classes live in the com.itextpdf.barcodes package of the barcodes module, the constructors take the PdfDocument (and optionally a PdfFont for the human-readable text), and the preferred result is createFormXObject, a vector object that you wrap in the layout Image. The old raster route, createAwtImage, still exists, but it needs java.awt and produces a bitmap you rarely want, so it is not used here. The AWT-based graphics export of iText 5 (PdfGraphics2D, finding O7) is not in iText Core either: drawing is done with PdfCanvas. All examples use Java 21 (finding O28): try-with-resources for the documents, Map.of, text blocks and small static helper methods instead of the anonymous classes of the old books.

Maven coordinates

<properties>
  <maven.compiler.release>21</maven.compiler.release>
  <itext.version>9.8.0</itext.version>
</properties>

<dependencies>
  <!-- iText Core: kernel, layout, barcodes, ... (AGPLv3 or commercial) -->
  <dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-core</artifactId>
    <version>${itext.version}</version>
    <type>pom</type>
  </dependency>
  <!-- ZXing encoder and readers (Apache-2.0) -->
  <dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.5.4</version>
  </dependency>
  <!-- Okapi Barcode (Apache-2.0) -->
  <dependency>
    <groupId>uk.org.okapibarcode</groupId>
    <artifactId>okapibarcode</artifactId>
    <version>0.5.6</version>
  </dependency>
  <!-- only for the round-trip test at the end of the page -->
  <dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>javase</artifactId>
    <version>3.5.4</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>3.0.8</version>
    <scope>test</scope>
  </dependency>
</dependencies>

The itext-core pom brings in the kernel, layout, barcodes and the other modules (see iText Architecture & Modules). If you want only the barcodes, depend on com.itextpdf:barcodes instead; it pulls in the kernel. For the HTML example you also need com.itextpdf:html2pdf:6.3.4 (see HTML to PDF with pdfHTML), and a logging binding for SLF4J. The examples were run with the slf4j-nop binding to keep the output clean. Okapi Barcode uses java.awt.Font for the text metrics, so the JDK module java.desktop must be present; it works in headless mode, which is the normal setting of a server (see Getting Started with Spring Boot).

Barcodes with the iText barcodes module

The invoice header: Code 128, EAN-13 and a QR code

All iText barcode classes follow the same pattern. You create the object (the 1D symbologies take the PdfDocument and a PdfFont, the 2D ones need neither), set the code and the geometry, and call createFormXObject, which returns a PdfFormXObject containing only vector path operators. new Image(xObject) wraps it so that the layout engine can place it like any other image: in a paragraph flow, in a table cell, at a fixed position or scaled.

The code below produces a one-page invoice header with a Code 128 barcode for the invoice number, a table row with an EAN-13 barcode for a book (the ISBN-13 9780306406157 is the textbook example used in many specifications; it is a valid number but it is used here only as sample data), and a QR code with the address of the online invoice. The address is the invented example.com.

package demo;

import com.itextpdf.barcodes.Barcode128;
import com.itextpdf.barcodes.BarcodeEAN;
import com.itextpdf.barcodes.BarcodeQRCode;
import com.itextpdf.barcodes.qrcode.EncodeHintType;
import com.itextpdf.barcodes.qrcode.ErrorCorrectionLevel;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.borders.Border;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Image;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.properties.UnitValue;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class ItextBarcodes {

    /** A QR code as a vector Form XObject wrapped in a layout Image. */
    static Image qr(PdfDocument pdf, String content, float moduleSize) {
        Map<EncodeHintType, Object> hints = new HashMap<>();
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
        hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name());
        BarcodeQRCode qr = new BarcodeQRCode(content, hints);
        PdfFormXObject xObject = qr.createFormXObject(ColorConstants.BLACK, moduleSize, pdf);
        Image image = new Image(xObject);
        // Tagged PDF / PDF/UA: describe the barcode as a figure (see the PDF/A and PDF/UA page).
        image.getAccessibilityProperties().setAlternateDescription("QR code with the address of the online invoice");
        return image;
    }

    static Image code128(PdfDocument pdf, PdfFont font, String text) {
        Barcode128 code = new Barcode128(pdf, font);
        code.setCodeType(Barcode128.CODE128);
        code.setCode(text);
        code.setBarHeight(28);   // user space units (points)
        code.setX(0.8f);         // width of the narrowest bar
        code.setSize(8);         // human-readable text size
        code.setBaseline(9);
        return new Image(code.createFormXObject(ColorConstants.BLACK, ColorConstants.BLACK, pdf));
    }

    /** Takes the first 12 digits and appends the EAN check digit computed by iText. */
    static Image ean13(PdfDocument pdf, PdfFont font, String twelveDigits) {
        BarcodeEAN code = new BarcodeEAN(pdf, font);
        code.setCodeType(BarcodeEAN.EAN13);
        code.setCode(twelveDigits + BarcodeEAN.calculateEANParity(twelveDigits));
        code.setBarHeight(26);
        code.setX(0.8f);
        code.setSize(8);
        code.setBaseline(8);
        return new Image(code.createFormXObject(ColorConstants.BLACK, ColorConstants.BLACK, pdf));
    }

    public static void main(String[] args) throws IOException {
        try (PdfDocument pdf = new PdfDocument(new PdfWriter("itext-barcodes.pdf"));
             Document doc = new Document(pdf)) {
            PdfFont font = PdfFontFactory.createFont();
            doc.add(new Paragraph("Bookshelf Billing invoice BKS-2026-000123"));
            doc.add(code128(pdf, font, "BKS-2026-000123"));

            Table lines = new Table(UnitValue.createPercentArray(new float[] {3, 2}))
                    .useAllAvailableWidth();
            lines.addCell(new Cell().add(new Paragraph("Sample book A")).setBorder(Border.NO_BORDER));
            lines.addCell(new Cell().add(ean13(pdf, font, "978030640615")).setBorder(Border.NO_BORDER));
            doc.add(lines);

            doc.add(qr(pdf, "https://example.com/invoices/BKS-2026-000123", 3));
        }
    }
}

Source: iText Knowledge Base, How to generate 2D barcode as vector image, and the Java API of BarcodeQRCode, Barcode128 and BarcodeEAN.

Some details of that code are worth stating, because each one is a typical source of a barcode that does not scan.

  • The QR code takes hints. BarcodeQRCode has a constructor with a Map<EncodeHintType, Object>. The keys are ERROR_CORRECTION (with ErrorCorrectionLevel.L, M, Q or H, which recover about 7, 15, 25 and 30 per cent of damaged codewords), CHARACTER_SET and MIN_VERSION_NR. The class EncodeHintType of iText is not the one of ZXing and not a Java enum, so an EnumMap does not compile (the first attempt in the scratch project failed exactly there); use a HashMap or Map.of. Without hints iText chooses its own default (the hints map of a code created without them is null), so pass the level explicitly. Prefer M or Q for a printed invoice that will be folded, stapled or scanned from a screen, and keep in mind that a higher level makes a denser code.

  • The module size is a parameter. createFormXObject(Color, float moduleSize, PdfDocument) scales the code so that one module is moduleSize points; the box of the object then grows with the version of the QR code. Without the moduleSize, a module is one point. As a guide, a module of 3 points is about 1.06 mm, enough for a good scanner at print resolution, and the size must never go below what the fundamentals page recommends for the reading distance.

  • The quiet zone is included for the QR code. The form object for a version 1 code (21 by 21 modules) that was measured in the scratch project had a box of 29 by 29 units, which is 21 modules plus four on each side, the width the QR standard requires. For a 1D symbology the box of the form object (80.8 by 29.7 units for Code 128 of six characters with its text) starts at the first bar, so the quiet zone is not part of it, and an Image that touches text or a border needs a margin (image.setMarginLeft(...)), a point that is easy to forget in a table cell.

  • 1D codes are described in user space units. setX is the width of the narrowest bar, setN the ratio of the wide to the narrow bar, setBarHeight the height of the bars, and setSize and setBaseline the text under them. EAN codes with a human-readable line use the PdfFont given to the constructor. All of them are in points, so a bar width of 0.8 point is 0.28 mm.

  • EAN-13 wants 13 digits in iText 9.8.0. The code in the invoice passes the first twelve digits and appends the check digit, which BarcodeEAN.calculateEANParity computes; giving twelve digits to setCode makes createFormXObject fail with an ArrayIndexOutOfBoundsException (reproduced with the version of this page). The check digit of the example was verified by decoding the rendered symbol, which returned 9780306406157.

  • Barcodes are figures for accessibility. A PdfFormXObject wrapped in an Image is a figure in the tag tree of a tagged document. The call image.getAccessibilityProperties().setAlternateDescription(...) provides the text a screen reader speaks. A barcode that only contains an identifier is often marked as an artifact instead; the rules, and how to produce a conformant PDF/UA document, are in PDF/A & PDF/UA with iText.

Code 39, PDF417 and Data Matrix

The remaining symbologies of the module follow the same pattern. Code 39 has options for the characters printed under the bars; PDF417 has an error level and a column count; Data Matrix has a size in modules and returns a status code from setCode.

package demo;

import com.itextpdf.barcodes.Barcode39;
import com.itextpdf.barcodes.BarcodeDataMatrix;
import com.itextpdf.barcodes.BarcodePDF417;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Image;
import com.itextpdf.layout.element.Paragraph;

import java.io.IOException;

public class Symbologies {
    public static void main(String[] args) throws IOException {
        try (PdfDocument pdf = new PdfDocument(new PdfWriter("symbologies.pdf"));
             Document doc = new Document(pdf)) {
            PdfFont font = PdfFontFactory.createFont();

            Barcode39 c39 = new Barcode39(pdf, font);
            c39.setCode("BKS-000123");
            c39.setStartStopText(false);   // do not print the * start and stop characters
            c39.setBarHeight(24);
            doc.add(new Paragraph("Code 39"));
            doc.add(new Image(c39.createFormXObject(ColorConstants.BLACK, ColorConstants.BLACK, pdf)));

            BarcodePDF417 pdf417 = new BarcodePDF417();
            pdf417.setCode("Bookshelf Billing - delivery note DN-2026-000045");
            pdf417.setErrorLevel(4);
            pdf417.setCodeColumns(10);
            doc.add(new Paragraph("PDF417"));
            // Create the XObject at module size 1 and scale the Image: in 9.8.0 the bounding box of the
            // PDF417 and Data Matrix XObjects does not follow the module-size overloads (checked by running it).
            Image pdf417Image = new Image(pdf417.createFormXObject(ColorConstants.BLACK, pdf));
            pdf417Image.scale(2.4f, 3f).setMarginTop(6);
            doc.add(pdf417Image);

            BarcodeDataMatrix dm = new BarcodeDataMatrix();
            dm.setWidth(18);    // force a square 18 x 18 symbol instead of the smallest that fits
            dm.setHeight(18);
            int status = dm.setCode("BKS-2026-000123");   // DM_NO_ERROR (0) when it fits
            System.out.println("data matrix status " + status);
            doc.add(new Paragraph("Data Matrix"));
            Image dmImage = new Image(dm.createFormXObject(ColorConstants.BLACK, pdf));
            dmImage.scaleAbsolute(72, 72);
            dmImage.setMargins(8, 8, 8, 8);   // 4 points per module plus an 8 point quiet zone
            doc.add(dmImage);
        }
    }
}

Source: iText Java API, BarcodePDF417, BarcodeDataMatrix and Barcode39.

Two behaviours of iText 9.8.0 that were found by running the code are worth knowing. First, for BarcodePDF417 and BarcodeDataMatrix the bounding box of the PdfFormXObject returned by the overloads that take a module size did not follow the module size (for a Data Matrix of 32 by 8 modules the box stayed 32 by 8 units with a module size of 3, so a layout Image showed only the corner of the code). The example therefore creates the object at the natural size and scales the Image (scale for PDF417, scaleAbsolute for Data Matrix), which also scales the margins to the right size. BarcodeQRCode is not affected: its box grew from 29 to 87 units with a module size of 3. If a future version fixes the Data Matrix box, the scaled Image still works. Second, BarcodeDataMatrix with the default size chooses the smallest symbol that fits, which for a short text is a rectangular one (32 by 8 modules for the example text). Many readers prefer square symbols, so the example asks for 18 by 18 with setWidth and setHeight before calling setCode. The status returned by setCode is 0 when the text fits (DM_NO_ERROR in the API documentation).

The module also has BarcodeCodabar, BarcodeInter25, BarcodeMSI, BarcodePostnet and BarcodeEANSUPP (the two- and five-digit supplements of books and magazines). They are less used in invoices than in logistics and libraries. GS1-128 is Barcode128 with setCodeType(Barcode128.CODE128_UCC) and the application identifiers in parentheses in the text, for example (01)09506000134352(17)271231; a GS1 DataMatrix, a DataBar or a composite symbol are not in the module, which is one reason to use Okapi.

Placing a barcode on every page: a page event

A code that is the same on every page, such as the QR code of the online invoice, is created once and stamped by a page event handler, the technique of Page Events: Headers, Footers & Watermarks. The handler holds the PdfFormXObject, so the vector operators are stored once in the file and referenced from each page.

package demo;

import com.itextpdf.barcodes.BarcodeQRCode;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.event.AbstractPdfDocumentEvent;
import com.itextpdf.kernel.pdf.event.AbstractPdfDocumentEventHandler;
import com.itextpdf.kernel.pdf.event.PdfDocumentEvent;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;

import java.io.IOException;

public class QrEveryPage {

    /** Stamps the same QR code, created once, in the bottom-right corner of every page. */
    static class QrFooter extends AbstractPdfDocumentEventHandler {
        private final PdfFormXObject qr;
        private final float size;

        QrFooter(PdfDocument pdf, String url, float size) {
            BarcodeQRCode code = new BarcodeQRCode(url);
            this.qr = code.createFormXObject(ColorConstants.BLACK, pdf);   // module size 1: scaled below
            this.size = size;
        }

        @Override
        protected void onAcceptedEvent(AbstractPdfDocumentEvent event) {
            PdfDocumentEvent e = (PdfDocumentEvent) event;
            PdfPage page = e.getPage();
            Rectangle box = page.getPageSize();
            float scale = size / qr.getWidth();
            new PdfCanvas(page.newContentStreamAfter(), page.getResources(), e.getDocument())
                    .saveState()
                    .concatMatrix(scale, 0, 0, scale, box.getRight() - 36 - size, box.getBottom() + 36)
                    .addXObjectAt(qr, 0, 0)
                    .restoreState();
        }
    }

    public static void main(String[] args) throws IOException {
        try (PdfDocument pdf = new PdfDocument(new PdfWriter("qr-every-page.pdf"));
             Document doc = new Document(pdf)) {
            pdf.addEventHandler(PdfDocumentEvent.END_PAGE,
                    new QrFooter(pdf, "https://example.com/invoices/BKS-2026-000123", 72));
            for (int i = 0; i < 60; i++) {
                doc.add(new Paragraph("Invoice line " + (i + 1)));
            }
        }
    }
}

Source: iText Knowledge Base, Page events for headers and footers, adapted to AbstractPdfDocumentEventHandler of iText 9 and to a barcode. The code was run with a document of 60 paragraphs; the first page was rendered and its QR code decoded.

The handler scales the object with concatMatrix instead of asking for a module size, because the same object is used at one fixed size. The PdfCanvas is created with page.newContentStreamAfter(), so the code is drawn above the content of the page; use newContentStreamBefore() to put it behind. A code that differs on each page, such as a page number or a per-page document id, needs its own handler instance state or a PdfFormXObject per page; for a code that identifies the whole invoice, one object is both smaller and simpler.

Sizing and placement in tables and in flow

Four ways of positioning an iText barcode cover nearly every invoice:

  • In flow: document.add(image). The image is a block element sized by its box (modules times module size, plus the quiet zone for QR). Use setMarginBottom and friends for spacing.

  • In a table cell: cell.add(image), as the EAN-13 in the example. Set the cell border to Border.NO_BORDER for a clean look, and give the column a width that fits the box of the barcode plus the cell padding and the quiet zone, using the column widths of Tables. Check the output when a column is narrower than the symbol, because any scaling or overflow changes the module size, and a symbol below the minimum module size does not scan.

  • At a fixed position: image.setFixedPosition(pageNumber, x, y), for a pre-printed form or a label. Coordinates are in points from the lower-left corner, as explained in Low-level Canvas & Graphics.

  • Straight on the canvas: every symbology has placeBarcode(PdfCanvas, ...), which draws the bars at the current position and returns the Rectangle it covered. It is the lowest-overhead route inside a custom renderer or a handler (see Renderers & Custom Layout) when you do not need layout.

Whichever route you choose, measure rather than guess: getBarcodeSize() returns the Rectangle the symbol will need, so a layout can reserve space before drawing, and printed output should be tested with the real printer (see the testing section below for the automated half).

ZXing: a BitMatrix drawn as vector rectangles or as SVG

ZXing’s job is to turn a string into a BitMatrix: a grid in which get(x, y) is true for a dark module. It does not know about PDF. That is an advantage, because the matrix can be painted anywhere, and a small amount of code turns it into exactly the vector graphics that iText produces by itself. The example below encodes a QR code, a Code 128 and a Data Matrix, draws them as filled rectangles on a PdfCanvas, and writes the same QR matrix as an SVG file.

package demo;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.datamatrix.encoder.SymbolShapeHint;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;

import java.io.IOException;
import java.util.Map;
import java.util.Locale;

public class Zxing {

    /** Encodes a QR code with exactly one BitMatrix cell per module; the quiet zone is part of the matrix. */
    static BitMatrix qrMatrix(String content, ErrorCorrectionLevel level) throws WriterException {
        Map<EncodeHintType, Object> hints = Map.of(
                EncodeHintType.ERROR_CORRECTION, level,
                EncodeHintType.CHARACTER_SET, "UTF-8",
                EncodeHintType.MARGIN, 4);
        return new QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, 1, 1, hints);
    }

    /** Draws dark modules as vector rectangles, merging horizontal runs. Origin is the lower-left corner. */
    static void draw(PdfCanvas canvas, BitMatrix m, float x, float y, float moduleW, float moduleH) {
        canvas.saveState().setFillColor(ColorConstants.BLACK);
        for (int row = 0; row < m.getHeight(); row++) {
            int col = 0;
            while (col < m.getWidth()) {
                if (!m.get(col, row)) { col++; continue; }
                int start = col;
                while (col < m.getWidth() && m.get(col, row)) col++;
                canvas.rectangle(x + start * moduleW,
                        y + (m.getHeight() - 1 - row) * moduleH,
                        (col - start) * moduleW, moduleH);
            }
        }
        canvas.fill().restoreState();
    }

    /** The same matrix as an SVG document: one path, one "M x y h w v 1 h -w z" per run. */
    static String toSvg(BitMatrix m) {
        StringBuilder path = new StringBuilder();
        for (int row = 0; row < m.getHeight(); row++) {
            int col = 0;
            while (col < m.getWidth()) {
                if (!m.get(col, row)) { col++; continue; }
                int start = col;
                while (col < m.getWidth() && m.get(col, row)) col++;
                path.append(String.format(Locale.ROOT, "M%d %dh%dv1h-%dz", start, row, col - start, col - start));
            }
        }
        return "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 " + m.getWidth() + " " + m.getHeight()
                + "\" shape-rendering=\"crispEdges\"><rect width=\"100%\" height=\"100%\" fill=\"#fff\"/>"
                + "<path d=\"" + path + "\" fill=\"#000\"/></svg>";
    }

    public static void main(String[] args) throws IOException, WriterException {
        BitMatrix qr = qrMatrix("https://example.com/invoices/BKS-2026-000123", ErrorCorrectionLevel.M);
        // One row of Code 128 (height 1 asks for one row); bars are stretched to 40 points when drawn.
        BitMatrix c128 = new MultiFormatWriter().encode("BKS-2026-000123", BarcodeFormat.CODE_128, 1, 1);
        BitMatrix dm = new MultiFormatWriter().encode("(01)09506000134352", BarcodeFormat.DATA_MATRIX, 1, 1,
                Map.of(EncodeHintType.DATA_MATRIX_SHAPE, SymbolShapeHint.FORCE_SQUARE));
        try (PdfDocument pdf = new PdfDocument(new PdfWriter("zxing.pdf"))) {
            PdfCanvas canvas = new PdfCanvas(pdf.addNewPage(PageSize.A5));
            draw(canvas, qr, 40, 300, 2.5f, 2.5f);
            draw(canvas, c128, 40, 220, 0.8f, 40f);
            draw(canvas, dm, 40, 140, 3f, 3f);
        }
        java.nio.file.Files.writeString(java.nio.file.Path.of("zxing-qr.svg"), toSvg(qr));
        System.out.println("qr " + qr.getWidth() + "x" + qr.getHeight() + ", c128 " + c128.getWidth() + "x" + c128.getHeight()
                + ", dm " + dm.getWidth() + "x" + dm.getHeight());
    }
}

Source: ZXing project, repository and the API documentation of QRCodeWriter, MultiFormatWriter and BitMatrix.

The points to remember about this code are the following.

  • Ask for one cell per module. Both writers take a requested width and height in pixels and scale the symbol up to at least that size using whole numbers of pixels per module. Passing 1, 1 returns the exact module grid (41 by 41 for the sample QR code, which is a version 4 symbol of 33 modules plus its quiet zone of four modules on each side), and you choose the size in points when you draw. A matrix scaled to 300 or 600 pixels instead would need the same maths in reverse and invites rounding errors.

  • The QR quiet zone is a hint. QRCodeWriter adds a margin of four modules by default, which is the standard width; the hint EncodeHintType.MARGIN changes it. The example sets it explicitly, because the number lives inside the matrix. MultiFormatWriter does not add a margin to Data Matrix and Aztec; in the scratch tests a clean image of a Data Matrix without a margin could not be decoded by ZXing’s own reader, and one with a quiet zone of two modules or more could. Always leave white space around such a symbol on the page.

  • Rows are top to bottom, PDF is bottom to top. The draw method flips the row index. A 1D symbol is a matrix of one row, which is why the Code 128 call asks for height 1 and draws the bars 40 points high (moduleH of 40).

  • Merge neighbouring modules. draw emits one rectangle per horizontal run of dark modules instead of one per module, which keeps the content stream small. The SVG writer applies the same idea with a single path. A QR code with H correction has several hundred runs; this is nothing for a PDF, but a table of a thousand invoices with a code each benefits from the reuse of a form object (see the page-event example).

  • Pick the error-correction level with the enum of ZXing. The class here is com.google.zxing.qrcode.decoder.ErrorCorrectionLevel, and the hint type is com.google.zxing.EncodeHintType. Both differ from the iText classes of the same names, so import them with care when a file uses both libraries.

  • The Data Matrix shape hint. EncodeHintType.DATA_MATRIX_SHAPE with SymbolShapeHint.FORCE_SQUARE gives the 16 by 16 square symbol used above; without it ZXing chose a rectangular 32 by 8 symbol for the same text, just as iText did.

Painting colours other than black is trivial with this approach: set the fill colour on the canvas, keeping enough contrast between the modules and the background for a reader (dark modules on a light background, with the usual caution that inverted codes are not read by all scanners). A logo in the middle of a QR code is done by painting over the matrix after drawing it, with a high error-correction level, and testing with the scanner that your customers use.

The SVG route

The toSvg method writes the matrix as a single <path> in a viewBox equal to the module grid, so the SVG scales to any size and can be placed in a web page or a PDF. iText converts SVG with SvgConverter (see Colours, Images & SVG), but for a plain barcode painting the rectangles directly is shorter and does not need the svg module. SVG is the right output when the same code is also shown on a web page or sent by e-mail, and when HTML is converted to PDF with pdfHTML, as in the next section.

Okapi Barcode for what iText lacks

Okapi Barcode works like ZXing in that it computes geometry and leaves the painting to you, but its output is richer: a Symbol has the bars as a list of Rectangle (getRectangles()), the human-readable text as TextBox objects (getTexts()), and, for MaxiCode, getHexagons() and getTarget(). The coordinates are in the module units of the symbol with the origin at the top left, so painting on a PdfCanvas needs the same vertical flip as above.

package demo;

import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import uk.org.okapibarcode.backend.Code128;
import uk.org.okapibarcode.backend.DataMatrix;
import uk.org.okapibarcode.backend.Symbol;
import uk.org.okapibarcode.graphics.Rectangle;

import java.io.IOException;

public class Okapi {

    /** Okapi computes the geometry in its own units (top-left origin); we paint it as PDF rectangles. */
    static void draw(PdfCanvas canvas, Symbol symbol, float x, float yTop, float scale) {
        canvas.saveState().setFillColor(ColorConstants.BLACK);
        for (Rectangle r : symbol.getRectangles()) {
            canvas.rectangle(x + r.x * scale, yTop - (r.y + r.height) * scale, r.width * scale, r.height * scale);
        }
        canvas.fill().restoreState();
    }

    public static void main(String[] args) throws IOException {
        Code128 gs1 = new Code128();
        gs1.setDataType(Symbol.DataType.GS1);
        gs1.setContent("[01]09506000134352[17]271231[10]LOT42");   // GS1 AIs in square brackets

        DataMatrix dm = new DataMatrix();
        dm.setDataType(Symbol.DataType.GS1);
        dm.setContent("[01]09506000134352[10]LOT42");
        try (PdfDocument pdf = new PdfDocument(new PdfWriter("okapi.pdf"))) {
            PdfCanvas canvas = new PdfCanvas(pdf.addNewPage(PageSize.A5));
            draw(canvas, gs1, 40, 380, 1f);
            draw(canvas, dm, 40, 300, 3f);
        }
        System.out.println("gs1 " + gs1.getWidth() + "x" + gs1.getHeight() + " rects=" + gs1.getRectangles().size()
                + " dm " + dm.getWidth() + "x" + dm.getHeight());
    }
}

Source: Okapi Barcode, repository and documentation.

Notes on the code:

  • GS1 data uses square brackets. With setDataType(Symbol.DataType.GS1) the content is written with the application identifiers in brackets, [01]09506000134352[17]271231[10]LOT42, and Okapi inserts the function characters. It rejects malformed identifiers ([99999]abc throws an OkapiInputException in the scratch test), but a GTIN with a wrong check digit was accepted, so validate the digits of the GTIN yourself. The numbers are invented sample values in the GS1 style; a real GTIN comes from the company prefix assigned to the publisher. The ZXing reader returned the text without the brackets for the Code 128 symbol (01095060001343521727123110LOT42, the identifiers and values run together), so the code that consumes a scan must know the GS1 application identifiers to split it.

  • The same symbol class does Data Matrix, Aztec, PDF417 and QR. The two-dimensional classes are DataMatrix, AztecCode, Pdf417, QrCode, MicroQrCode, MaxiCode and SwissQrCode; the linear ones include Code128, Ean, Upc, Code3Of9, Codabar, Code93, DataBar14, DataBarExpanded and DataBarLimited; postal codes include RoyalMail4State, UspsOneCode, KixCode, JapanPost and AustraliaPost. The list was read from the classes of the 0.5.6 jar. The Swiss QR-bill itself is covered by the regulated codes page of this section.

  • Module width and quiet zones are properties of the symbol. setModuleWidth, setBarHeight, setQuietZoneHorizontal and setQuietZoneVertical set the integer geometry in module units before calling setContent; getWidth() and getHeight() then give the size that your canvas code scales. The sample Data Matrix in the test was generated with 26 by 12 modules (rectangular by default), and decoded correctly once it was drawn on white paper with a margin.

  • setContent throws for invalid input. The exceptions extend OkapiException; catch OkapiInputException to report an invalid GTIN or an unsupported character to the caller instead of a generic failure.

  • Human-readable text. The text boxes carry the position and the string, but fonts are your business; draw them with an iText PdfFont on the same PdfCanvas if you need the digits under the bars, or print the number as a normal paragraph under the image.

Barcodes in pdfHTML

When the invoice is produced from an HTML template (a Thymeleaf template converted with pdfHTML, as in HTML to PDF with pdfHTML), the simplest barcode is an SVG. pdfHTML renders an inline <svg> element and an <img> element with a data:image/svg+xml;base64 source (both were tested for this page), and the result is vector content in the PDF. Generating the SVG with ZXing (the toSvg method above) and inserting it into the model of the template is therefore the shortest route; no custom tag worker is needed.

package demo;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.itextpdf.html2pdf.HtmlConverter;

import java.io.FileOutputStream;
import java.io.IOException;

public class HtmlBarcodes {
    public static void main(String[] args) throws IOException, WriterException {
        BitMatrix qr = Zxing.qrMatrix("https://example.com/invoices/BKS-2026-000123",
                com.google.zxing.qrcode.decoder.ErrorCorrectionLevel.M);
        String svg = Zxing.toSvg(qr);
        String html = """
                <html><head><style>
                  @page { size: A5; margin: 20mm; }
                  .qr { width: 30mm; height: 30mm; }
                </style></head><body>
                  <h1>Invoice BKS-2026-000123</h1>
                  <div class="qr">%s</div>
                  <p><img class="qr" alt="Invoice QR code" src="data:image/svg+xml;base64,%s"/></p>
                </body></html>
                """.formatted(svg, java.util.Base64.getEncoder().encodeToString(svg.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
        try (FileOutputStream out = new FileOutputStream("html-qr.pdf")) {
            HtmlConverter.convertToPdf(html, out);
        }
    }
}

Source: iText Knowledge Base, pdfHTML QR code examples, and How to generate 2D barcode as vector image. The code converts one HTML document with an inline SVG and an img with a data:image/svg+xml;base64 source into a one-page PDF; both QR codes were decoded from the rendered page.

The knowledge base page shows two other techniques: registering a custom tag worker so that an element such as <qrcode> is replaced by a BarcodeQRCode image (useful when the template author should only write the data), and loading the barcode through an image resource retriever. Use them when the HTML is written by people who should not handle SVG; the code above is enough for a template that the service itself fills. Two practical tips for the HTML route: put the svg in a div with a fixed width and height in CSS (millimetres give a predictable module size), and give the img an alt attribute for accessibility.

If the template is rendered by Thymeleaf, the SVG string is inserted unescaped with th:utext, which is safe only because the string was produced by your own code from your own data; never insert an SVG that came from a user, because it may contain scripts and links. The Thymeleaf side is covered by the sections of this site on templates.

Testing barcodes by decoding them

A barcode that looks right and does not scan is the worst failure, because it appears only in production. The most effective automated check is the one used for every example on this page: render the PDF to a bitmap and decode it with a reader. ZXing includes the readers, PDFBox renders the page, and the whole test is a few lines.

package demo;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

/** Renders page 1 of a PDF that holds ONE barcode and decodes it again with ZXing. */
public class RoundTrip {

    static String decode(Path pdf, BarcodeFormat format) throws IOException, NotFoundException {
        try (PDDocument document = Loader.loadPDF(pdf.toFile())) {
            BufferedImage page = new PDFRenderer(document).renderImageWithDPI(0, 300);
            BufferedImage symbol = cropToDarkPixels(page, 60);
            var bitmap = new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(symbol)));
            return new MultiFormatReader().decode(bitmap, Map.of(
                    DecodeHintType.POSSIBLE_FORMATS, List.of(format),
                    DecodeHintType.TRY_HARDER, Boolean.TRUE)).getText();
        }
    }

    /** Cuts the dark pixels out and puts them on a white canvas with a wide quiet zone. */
    static BufferedImage cropToDarkPixels(BufferedImage page, int pad) {
        int minX = page.getWidth(), minY = page.getHeight(), maxX = 0, maxY = 0;
        for (int y = 0; y < page.getHeight(); y++) {
            for (int x = 0; x < page.getWidth(); x++) {
                if ((page.getRGB(x, y) & 0xFF) < 128) {
                    minX = Math.min(minX, x); maxX = Math.max(maxX, x);
                    minY = Math.min(minY, y); maxY = Math.max(maxY, y);
                }
            }
        }
        int w = maxX - minX + 1, h = maxY - minY + 1;
        BufferedImage out = new BufferedImage(w + 2 * pad, h + 2 * pad, BufferedImage.TYPE_INT_RGB);
        var g = out.createGraphics();
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, out.getWidth(), out.getHeight());
        g.drawImage(page.getSubimage(minX, minY, w, h), pad, pad, null);
        g.dispose();
        return out;
    }

    public static void main(String[] args) throws Exception {
        // args: <format> <file.pdf>, for example QR_CODE zxing-qr.pdf
        System.out.println(decode(Path.of(args[1]), BarcodeFormat.valueOf(args[0])));
    }
}

Source: ZXing API documentation (MultiFormatReader, DecodeHintType) and Apache PDFBox rendering (see Signing with Apache PDFBox & EU DSS for the library itself, which is Apache-2.0).

The method crops the page to its dark pixels and adds a white border. That step matters: a detector such as the one for Data Matrix did not find a symbol on a full A5 page in the scratch tests, but found the same symbol after it was cropped with a margin, so a failed decode of a whole page proves little. The test assumes one symbol per page; for a page with several symbols, decode with GenericMultipleBarcodeReader (com.google.zxing.multi), which worked for the Code 128, EAN-13 and QR code of the invoice header, and for the Code 39 and PDF417 of the symbologies page. The single-symbol RoundTrip was used for the Data Matrix (iText, ZXing and Okapi) and QR examples. In a JUnit test, assert that decode returns exactly the string that went in, and keep a golden PDF for the layout with the CompareTool of Testing & Performance with iText. A decode proves that the symbol is valid and has sufficient contrast at 300 dpi; it does not prove that a hand scanner will read it at the printed size, so test the final print once with the real device.

Practical recommendations

  • Prefer vector output and check it. A bitmap barcode that is scaled by the viewer or the printer blurs and fails. Open the PDF in a text editor or inspect the objects (see How a PDF File Works): a vector barcode is a form XObject with path operators, an image is an object with /Subtype /Image.

  • Keep the quiet zone as white space, not as design. A border, a coloured box or a neighbouring line of text that eats into the quiet zone is the usual reason for a poor read rate.

  • Choose the error-correction level for the medium. Level M is a good default for paper; use Q or H for labels that will be scuffed, and remember that a bigger payload at a higher level makes a denser code that needs a larger module size.

  • Keep payloads short. A QR code that carries an address of a few dozen characters is easy to read; embedding the whole invoice as text is not. Link to an authenticated address instead.

  • Encode non-ASCII text deliberately. State the character set (UTF-8) with the hint when the payload has accents, and check the reader side agrees; many readers assume ISO-8859-1 without an ECI marker.

  • Do not put personal data in a code that is public. The QR code of an invoice is printed and may be photographed. Use an opaque identifier or a signed link, not names or amounts that you do not want to share.

  • Regulated codes follow their own rules. For the VeriFactu, TicketBAI, EPC or Swiss QR codes, the content, size and position are prescribed by the authority; use the library of this page as the drawing tool and the regulated codes page for what to draw.

  • Reuse form objects. When the same barcode appears many times, create the PdfFormXObject once; iText writes it once and references it.

  • Spring Boot. The generation code is a plain method that returns a byte[] or writes to an OutputStream; the controller and streaming patterns are in Getting Started with Spring Boot. Libraries that are stateless and thread-safe at the encoder level (ZXing’s writers) can be shared, but create a new iText PdfDocument and barcode object per request.

Outdated points to avoid

  1. com.itextpdf.text.pdf.Barcode128, BarcodeEAN, BarcodeQRCode of iText 5 and com.lowagie.text.pdf.* of iText 1 and 2: replaced by com.itextpdf.barcodes.*, whose constructors take a PdfDocument.

  2. barcode.createImageWithBarcode(cb, color, color), which produced an Image bound to a PdfContentByte: replaced by createFormXObject(...) wrapped in new Image(xObject), a vector object.

  3. createAwtImage and any Swing or Batik route to a bitmap barcode: unnecessary for PDF and depends on AWT.

  4. Treating the barcode image as raster and scaling it: a scaled bitmap blurs; draw modules as vectors.

  5. Assuming the module-size overloads of every iText barcode update the bounding box: check it, as the Data Matrix and PDF417 behaviour of 9.8.0 above shows.

  6. Assuming the iText EncodeHintType and ErrorCorrectionLevel are the ZXing classes with the same names, or that the former is an enum.

  7. Relying on a Barbecue or Barcode4J example from a 2000s book for new Java 21 code: those libraries are abandoned or dormant; use ZXing, Okapi or the iText module.