Skip to content
edragoev1Public

About

Standards-compliant PDFs, at jet speed, in Java, C#, Go and Swift

Topics

Resources

Security policy

Stars

32 stars

Watchers

1 watching

Forks

Latest commit

 

History

6,016 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PDFjet

Standards-compliant PDFs, at jet speed.

In the language you already use: Java, C#, Go or Swift.
PDF/UA and PDF/A in the same document: readable by screen readers, and made to last.

Build Documentation MIT license Java 8+ .NET 8 Go 1.26+ Swift 6.1+

Documentation · Examples · Changelog

PDFjet creates PDF documents: text in most scripts, tables, charts, barcodes and images, accessible and archival when you need it to be. The same API and the same output come in four languages, and none of them needs a third-party library.

Important

PDFjet includes every European language (Latin, Greek, Cyrillic). For other scripts, add one package: the fonts for Chinese, Japanese, Korean, Arabic, Hebrew and Thai, the same version as PDFjet.

Add
Maven <dependency><groupId>com.pdfjet</groupId><artifactId>pdfjet-fonts-world</artifactId><version>9.1.7</version></dependency>
Gradle implementation("com.pdfjet:pdfjet-fonts-world:9.1.7")
NuGet dotnet add package PDFjet.Fonts.World
Go import _ "github.com/edragoev1/pdfjet/v9/resources/fonts/world"
Swift .product(name: "PDFjetFontsWorld", package: "pdfjet")

From 9.1.7 the jar of Maven Central, the package of NuGet, a Go program and the Swift product carry IBM Plex Sans and Serif and Cascadia Mono, embedded in the documents that use them: the jar is about 5 MB. The add-on carries IBM Plex Sans JP, KR, SC and TC and IBM Plex Sans Arabic, Hebrew and Thai, about 41 MB. Without it, a constant such as IBMPlexSansJP.Regular or IBMPlexSansArabic.Regular throws an error that names the line to add.

No system fonts are needed, so the output is the same on a laptop, a server and in a Docker container, even a slim image with no fonts installed.

Want a much smaller jar? Take PDFjet.jar from the releases on GitHub: it is 0.7 MB. Its package has all the fonts as files, in resources/fonts, those of the add-on in resources/fonts/world, which PDFjet reads from there with the same code; keep that folder next to your program, or only the fonts you use.

This repository has all that the examples and the tests read: the fonts in resources/fonts and the examples' data in resources/data. Nothing is fetched.

Why PDFjet

  • Accessible and archival PDFs in one setting. PDF/UA-1 and PDF/A-1a, 1b, 2a, 2b, 3a and 3b are built in, and the examples are checked with veraPDF on every push.
  • One API, four languages, the same PDF. Java, C#, Go and Swift share the API, and CI compares the PDFs of every port with Java's, page by page.
  • Fast and light. Every port writes a 500-page document with an embedded font in under 100 ms once it is warm (the first run, with the runtime starting, takes up to about 260 ms), and each page is written out and freed as soon as the next one starts.
  • No third-party dependencies. The Java library is a single 0.6 MB jar, and the .NET library a single 0.9 MB DLL.
  • Layout built in. Tables that break across pages and repeat their header rows, text blocks, columns and charts.
  • Charts that read at a glance. SimpleBarChart compares a few values in horizontal bars, a name at the left of each and its value at the end, one bar highlighted, with no axis or legend to set up: the chart to put above a table of the same numbers (Example_20). It describes itself to screen readers from its data. SimpleLineChart and SimpleScatterChart in the same look, and bar and donut charts too.
  • Barcodes built in. EAN-13, UPC-A, Code 39, Code 128, QR, Data Matrix and PDF417.
  • Fonts ready to use. 71 TrueType and OpenType fonts, IBM Plex Sans and Serif, Cascadia Mono, and IBM Plex Sans JP, KR, SC, TC, Arabic, Hebrew and Thai, with Unicode text in Latin, Greek, Cyrillic, CJK and right to left scripts, embedded as subsets of the glyphs a document draws. IBM Plex Sans alone covers all 24 official languages of the European Union, from Czech ř and Romanian ș to Greek and Bulgarian, with the euro sign and each language's quotation marks: a one-page PDF in it is about 28 KB. A page of Chinese in IBM Plex Sans SC is about 180 KB, where the whole font is 7 MB.
  • Images. PNG, JPEG and BMP, and SVG drawn as vector graphics. For logos, icons and drawings, use SVG: it stays sharp at any size and in print, and it is far smaller and faster than a PNG with transparency, which is decoded and compressed again for every document. A letter with a 2 KB SVG logo takes a seventh of the time and a third of the size of the same letter with a PNG logo. Keep photos in JPEG.
  • Security. AES-256 encryption with passwords and permissions, reading existing and encrypted PDFs, and limits that keep untrusted input from exhausting memory.
Text blocks in English, Greek and Bulgarian
Text in many scripts
A table of country data
Tables
Two simple bar charts of electric vehicles, by make and by model year, over a table of the same numbers
Charts
QR, Data Matrix and PDF417 symbols
Barcodes

Quick start

Each snippet writes hello.pdf with a line in English, Greek and Bulgarian. The fonts PDFjet carries, such as IBMPlexSans.Regular, are paths like fonts/IBMPlexSans/IBMPlexSans-Regular.ttf: PDFjet reads the file of that path if there is one, else the font from the folder resources/fonts, else from the library itself, as the jar of Maven Central carries it. The same code runs with all three.

Java

PDFjet is on Maven Central, as com.pdfjet:pdfjet. With Maven:

<dependency>
  <groupId>com.pdfjet</groupId>
  <artifactId>pdfjet</artifactId>
  <version>9.1.7</version>
</dependency>

With Gradle:

implementation("com.pdfjet:pdfjet:9.1.7")

PDFjet includes every European language (Latin, Greek, Cyrillic). For other scripts, add one package, com.pdfjet:pdfjet-fonts-world, the same version: the fonts for Chinese, Japanese, Korean, Arabic, Hebrew and Thai.

<dependency>
  <groupId>com.pdfjet</groupId>
  <artifactId>pdfjet-fonts-world</artifactId>
  <version>9.1.7</version>
</dependency>
implementation("com.pdfjet:pdfjet-fonts-world:9.1.7")

From 9.1.7 the jar of Maven Central carries IBM Plex Sans and Serif and Cascadia Mono, so their constants in com.pdfjet.fonts, such as IBMPlexSans.Regular, work with nothing else to download; that jar is about 5 MB. pdfjet-fonts-world, about 41 MB, has IBM Plex Sans JP, KR, SC and TC and IBM Plex Sans Arabic, Hebrew and Thai, which PDFjet finds on the classpath; without it, IBMPlexSansJP, IBMPlexSansArabic and the others throw an error that names it. The PDFjet.jar of the releases on GitHub, and the one ./build-java.sh builds from this repository, is 0.7 MB: it reads the same fonts from the folder resources/fonts, which its package has.

import com.pdfjet.*;
import com.pdfjet.fonts.*;
import java.io.*;

public class Hello {
    public static void main(String[] args) throws Exception {
        PDF pdf = new PDF(new BufferedOutputStream(new FileOutputStream("hello.pdf")));
        Font font = new Font(pdf, IBMPlexSans.Regular).setSize(18f);
        Page page = new Page(pdf, Letter.PORTRAIT);
        new TextLine(font, "Hello, World! Γειά σου, κόσμε! Здравей, свят!").setLocation(50f, 100f).drawOn(page);
        pdf.complete();
    }
}
C#

PDFjet is on NuGet, as PDFjet, for .NET 8 or later and .NET Framework 4.8, strong-named. With the .NET CLI:

dotnet add package PDFjet --version 9.1.7

Or in the project file:

<PackageReference Include="PDFjet" Version="9.1.7" />

PDFjet includes every European language (Latin, Greek, Cyrillic). For other scripts, add one package, PDFjet.Fonts.World, the same version: the fonts for Chinese, Japanese, Korean, Arabic, Hebrew and Thai.

dotnet add package PDFjet.Fonts.World

From 9.1.7 the package of NuGet carries IBM Plex Sans and Serif and Cascadia Mono, so their constants, such as IBMPlexSans.Regular, work with nothing else to download; it is about 10 MB. PDFjet.Fonts.World, about 41 MB, has IBM Plex Sans JP, KR, SC and TC and IBM Plex Sans Arabic, Hebrew and Thai, which PDFjet finds on its own; without it, IBMPlexSansJP, IBMPlexSansArabic and the others throw an error that names it. The PDFjet.dll of the releases on GitHub is small: it reads the same fonts from the folder resources/fonts, which its package has.

using System.IO;
using PDFjet.NET;

public class Hello {
    public static void Main() {
        var pdf = new PDF(new BufferedStream(new FileStream("hello.pdf", FileMode.Create)));
        var font = new Font(pdf, IBMPlexSans.Regular).SetSize(18f);
        var page = new Page(pdf, Letter.PORTRAIT);
        new TextLine(font, "Hello, World! Γειά σου, κόσμε! Здравей, свят!").SetLocation(50f, 100f).DrawOn(page);
        pdf.Complete();
    }
}
Go
go get github.com/edragoev1/pdfjet/v9@latest

If go get reports 404 Not Found from proxy.golang.org, the Go module proxy has not made the module available yet. Fetch it straight from GitHub instead:

GOPROXY=direct GONOSUMDB=github.com/edragoev1/pdfjet go get github.com/edragoev1/pdfjet/v9@latest
package main

import (
	"log"

	pdfjet "github.com/edragoev1/pdfjet/v9/src"
	"github.com/edragoev1/pdfjet/v9/src/IBMPlexSans"
	"github.com/edragoev1/pdfjet/v9/src/letter"
)

func main() {
	pdf, err := pdfjet.NewPDFFile("hello.pdf")
	if err != nil {
		log.Fatal(err)
	}
	font := pdfjet.NewFontFromFile(pdf, IBMPlexSans.Regular).SetSize(18)
	page := pdfjet.NewPage(pdf, letter.Portrait())
	pdfjet.NewTextLine(font, "Hello, World! Γειά σου, κόσμε! Здравей, свят!").SetLocation(50, 100).DrawOn(page)
	if err := pdf.Complete(); err != nil {
		log.Fatal(err)
	}
}

The module path carries the major version, as Go requires from v2 on. The Go module has the source of the library and the examples, and the fonts PDFjet carries, in resources/fonts. The data files of the examples, in resources/data, and the images, in resources/images, are not in it: each folder has a go.mod of its own, which keeps it out. The module of 9.1.5 has no fonts: use the font files of its package from the release on GitHub.

PDFjet includes every European language (Latin, Greek, Cyrillic). For other scripts, add one line to your program: the fonts for Chinese, Japanese, Korean, Arabic, Hebrew and Thai.

import _ "github.com/edragoev1/pdfjet/v9/resources/fonts/world"

Go builds the fonts a program embeds into the program itself, uncompressed. So only IBM Plex Sans and Serif and Cascadia Mono are built in, 11 MB: a program like the one above is about 15 MB. The fonts of the other scripts, IBM Plex Sans JP, KR, SC and TC and IBM Plex Sans Arabic, Hebrew and Thai, are 52 MB more, built in only when the program has the line above, which makes it about 70 MB. Without it, IBMPlexSansJP.Regular, IBMPlexSansArabic.Regular and the others are read from the folder resources/fonts/world of the working directory when it has them, and are an error that names this line when it does not.

A program that draws only with font files of its own, such as one built to WebAssembly for a browser, can leave the 11 MB out with the build tag pdfjet_nofonts:

go build -tags pdfjet_nofonts

The constants of the fonts PDFjet carries are then read from the folder resources/fonts of the working directory, or are an error that names the tag, and an SVG image with text needs SetFont.

Swift

PDFjet for Swift runs on Linux as well as on macOS, so it serves a Swift server too. It uses only the parts of Foundation that Linux has, such as Data and the streams, and none of Apple's frameworks (no CoreGraphics, PDFKit or CryptoKit): its compression, encryption, fonts, images and layout are its own. The Build workflow tests it on Linux and on macOS on every push.

Add PDFjet to the Package.swift of your package:

dependencies: [
    .package(url: "https://github.com/edragoev1/pdfjet.git", from: "9.1.7"),
],
targets: [
    .executableTarget(name: "App", dependencies: [
        .product(name: "PDFjet", package: "pdfjet"),
        .product(name: "PDFjetFontsWorld", package: "pdfjet"),   // Only for scripts not European
    ]),
]

PDFjet includes every European language (Latin, Greek, Cyrillic). For other scripts, add the PDFjetFontsWorld line above; leave it out otherwise: the fonts for Chinese, Japanese, Korean, Arabic, Hebrew and Thai. The product PDFjet carries IBM Plex Sans and Serif and Cascadia Mono, about 11 MB, so their constants, such as IBMPlexSans.Regular, work anywhere. A Swift program is often an iPhone or Mac app, which should not carry the 52 MB of the fonts of the other scripts, IBM Plex Sans JP, KR, SC and TC and IBM Plex Sans Arabic, Hebrew and Thai, unless it draws in them: with the PDFjetFontsWorld line, they are built into your app, and no code is needed, as PDFjet finds their bundle on its own. Without it, PDFjet reads them from the folder resources/fonts/world of the working directory, as in this repository and the release packages, and a constant of one of them that is in neither place throws an error that names the line to add. An app that named the product PDFjetFonts before 9.1.7 still builds; that product is now part of PDFjet.

import Foundation
import PDFjet

let pdf = PDF(OutputStream(toFileAtPath: "hello.pdf", append: false)!)
let font = try Font(pdf, IBMPlexSans.Regular).setSize(18.0)
let page = Page(pdf, Letter.PORTRAIT)
TextLine(font, "Hello, World! Γειά σου, κόσμε! Здравей, свят!").setLocation(50.0, 100.0).drawOn(page)
try pdf.complete()

Performance

Each port writing a document with 60 lines of Latin, Greek and Cyrillic text per page in IBM Plex Sans, the median of 7 runs on an AMD Ryzen 5 5600G, at 9.1.7 (5 November 2026, benchmarks/ports/results/2026-11-05-c652db89.log). Peak memory is that of the whole process writing the 500-page document, runtime included.

Port 100 pages 500 pages Peak memory
Java 15 ms 61 ms 86 MB
C# 43 ms 60 ms 50 MB
Go 12 ms 59 ms 11 MB
Swift 20 ms 97 ms 25 MB

Examples

The examples folder has 50 examples, the same in every port. They draw in the fonts PDFjet carries, which the library reads from resources/fonts or from itself, and read the data files of resources/data, so they need nothing fetched. Build a port and run all of its examples, or run one example by its number:

Port All examples One example
Java ./build-java.sh ./run-java.sh 01
C# ./build-dotnet.sh ./run-dotnet.sh 01
Go ./build-go.sh ./run-go.sh 01
Swift ./build-swift.sh ./run-swift.sh 01

The examples read the data files by paths relative to the repository root, such as resources/data/the-idiot.txt, and the scripts run them there. resources/data/README.md says where each file comes from and under which license.

On Linux, install these first:

sudo apt install libc6-dev gcc

On Windows, use the .cmd scripts of the same names.

booklet/build.sh writes the PDFjet booklet with PDFjet: an introduction to the library and a guide to its API, each feature with the smallest code that shows it, as a PDF/UA document with bookmarks and a linked table of contents. It writes one booklet for each port and one with the code of the four ports, booklet/PDFjet-Booklet-*.pdf, from booklet/content.txt and the snippet programs in booklet/snippets. booklet/check-snippets.sh builds and runs the snippets in the four ports and checks that they write the same PDFs.

Builds

The Build GitHub Actions workflow runs build-java.sh, build-dotnet.sh, build-go.sh and build-swift.sh on every push to main and on pull requests, one job per port. A job fails if the library or an example does not compile or compiles with a warning, go vet reports a problem in the Go port, an example exits with an error, an example does not create its PDF file, or a unit test fails. A second job then checks the PDFs with .github/scripts/check-example-pdfs.py: each example must render the same, with its text in the same fonts, sizes, colors and positions and the same drawing instructions on every page, in the C#, Go and Swift ports as in Java, and each example that declares PDF/A or PDF/UA compliance must pass veraPDF in all four ports.

To run the same checks locally before pushing, run ./check-examples.sh. It builds the four ports and runs their unit tests one after another in the repository folder, builds and tests the Java port again with JDK 8, and then checks their PDFs, so it needs the four toolchains, a JDK 8 (in JAVA8_HOME, /opt/jdk8* or /usr/lib/jvm), Python 3, veraPDF, Node 22.13 or later with npm, for PDF.js, and Poppler's pdftoppm and pdftotext (poppler-utils on Ubuntu). It stops, and says what to install, before it builds anything when one of them is missing or Node is too old.

The viewers the PDFs are checked in

The examples are checked in the engines of five viewers, with MuPDF (SumatraPDF) as the reference the others are compared against:

Viewer Engine Covers How it is checked
Chrome PDFium Chrome, Brave, Opera, Vivaldi, Android automatically, in CI and check-examples.sh
Firefox PDF.js Firefox automatically (PDF.js in Node, in CI and check-examples.sh), and in Firefox by hand now and then
Safari PDFKit Preview, Safari, iPhone and iPad automatically, in CI on macOS
Acrobat Reader Adobe PDF Library Acrobat, Edge, Adobe Document Cloud by hand, on Windows
Evince / Okular Poppler Linux desktop viewers, CUPS printing, command-line tools automatically, in CI and check-examples.sh

Checked automatically means that .github/scripts/check-viewers.py opens every example, and the encrypted ones with their passwords, in the engine: each page must render, not be blank, and look like MuPDF's render of it within set limits, and the engine must extract every character of its text that MuPDF does. The renders are kept, a contact sheet of each PDF beside MuPDF's, to look at.

The Windows workflow runs the Windows scripts, build-java.cmd, build-dotnet.cmd, build-go.cmd and build-swift.cmd, on a Windows runner and checks that every example creates its PDF file. It only runs when started from the Actions tab.

Unit tests

Each port has unit tests with the same cases and the same expected values, so a difference between the ports fails a test. They cover the numbers written in content streams, the stream filters, page sizes, fonts, text lines, text blocks, text boxes, cells and tables, right to left text, the page graphics state, shapes, charts, SVG, PNG and BMP images, bookmarks, layers, writing and reading back PDFs, encryption with passwords and permissions, and the barcodes. A test for a known bug is skipped, with the TODO.md item as its reason.

Port Tests Framework Run
Java tests/java JUnit 5 ./test-java.sh
C# tests/dotnet xUnit ./test-dotnet.sh
Go src/**/*_test.go testing ./test-go.sh
Swift tests/swift Swift Testing ./test-swift.sh

The tests are not part of the libraries and add no dependency to them. test-java.sh downloads the JUnit console launcher once into build/junit and checks its SHA-256, and builds and runs the tests on Java 8 as on later releases. The C# tests compile the library sources into the test project, so they can test internal classes, and restore xUnit from NuGet. The tests of every port read the PngSuite images of tests/data/PngSuite, the fonts of resources/fonts and the data files of resources/data from the repository. The PDFs the tests make stay in memory, and the files they need to write go to the system's temporary directory. ./check-examples.sh runs the unit tests of each port after its examples.

Documentation

The API references and the example pages are published at https://edragoev1.github.io/pdfjet/. The Documentation GitHub Actions workflow rebuilds and publishes the site on every push to main, so the generated HTML is not kept in git. The workflow also runs the Java examples and publishes the PDF files they create, which the example pages link to. This needs the repository's Pages source (Settings > Pages) set to GitHub Actions.

To build them locally, ./generate-documentation.sh runs Javadoc for the Java port into docs/java, and DocFX for the C# port into docs/dotnet. DocFX reads the XML doc comments (/// <summary>) in net/pdfjet; its configuration is in docfx/. Install DocFX once with dotnet tool install -g docfx. The Go reference is built with doc2go from the doc comments in src into docs/go, leaving out the example programs in src/examples. The script runs doc2go v0.12.2 with go run, so it needs no install. The Swift reference is built with DocC, which comes with the Swift toolchain, from the doc comments in Sources/PDFjet into docs/swift. Its pages expect to be served from /pdfjet/swift/, so they do not work when opened straight from disk.

Java compatibility

The Java library and examples compile and run on Java 8 and later. With JDK 9 or newer, the Java build scripts pass javac --release 8, so PDFjet.jar and the examples run on Java 8 whichever JDK builds them, and any API newer than Java 8 is rejected. Java 8's javac has no --release option, so the scripts leave it out there; that javac builds Java 8 class files anyway.

Which class to use

PDFjet has several classes that draw text and two that group content. Each has one job:

Class Use it for
TextLine One line of text in one font: a title, a label, a value. It does not wrap.
TextBlock One run of text in one font that wraps at its width: a note, a table cell, a paragraph in a box. It breaks words that do not fit, wraps right to left and Thai text, highlights keywords, and can be cut to a height with the lines aligned to the top, center or bottom.
TextColumn Paragraphs of TextLine objects that differ in font, size or color, drawn in one place: an article page with bold or colored words, justified paragraphs, CJK paragraphs, or a rotated column.
TextFrame Paragraphs that continue from one frame to the next: the columns of an article or the pages of a long text. hasMoreText says whether a next frame is needed.
Container A group of drawable elements, shapes, text, images, annotations and other containers, that are moved, rotated and scaled together and drawn into a page.
Stamp Content that repeats on many pages, like a header, a footer or a watermark. It is written once as a form XObject and each placement is a single operator, so the file stays small.

Table and BigTable draw tables, with a TextBlock in a cell when the cell text needs wrapping. Example_01, 10, 11, 12, 15 and 31 show these classes. Their data files are comma, pipe or tab separated values read as RFC 4180 reads them, so a quoted field can hold a delimiter or, as a spreadsheet exports a cell of several lines, line breaks, which a table draws as spaces. A cell holds text or one drawable, set with Cell.setDrawable (or setImage, setBarcode, setTextBlock, setTextColumn): an image, a QR code, a chart or another table, placed at the padding and aligned as the text is. BigTable is for tables of any length: it reads its rows from a delimited file, or with setTableData(header, rows) from an Iterable in Java, an IEnumerable in C#, a sequence in Swift or an iter.Seq in Go (SetTableRows). It goes through the rows twice, once to measure the columns and once to draw them, keeps none of them and writes each page when it is full, so its memory stays flat; Example_24, given the full data file of its sample, draws 2,546 pages that way. A tagged document, PDF/UA or PDF/A of level A, keeps a little of each row until complete(), for the structure tree it writes at the end.

Table or BigTable? Use BigTable for a table of plain one-line rows, however long: statements, transaction lists, reports and exports. Every row has one height and one style, a field too wide for its column is cut short, and it is the faster of the two: a 600-row statement takes 5.6 ms in Java to Table's 7.7. Use Table when cells need more: text that wraps onto several lines, an image, a barcode or a chart in a cell, cells that span columns or rows, page totals and totals carried forward, or a style for each cell. setColumns(3, 0, 11) draws the fourth, first and twelfth fields of every row and of the header, in that order, where setNumberOfColumns(n) draws the first n. setShadingColor and setBorderColor change the colors of every other row and of the lines, or leave them out with Color.transparent, setPadding sets the space on each side of the text, and setFooter("Seite {page} von {pages}", font) replaces the "Page {page} of {pages}" footer, or leaves it out with a null text.

Fonts, and what a PDF embeds

The fonts PDFjet carries are TrueType .ttf files, and IBM Plex Sans JP, KR, SC and TC are .otf, fonts of CFF outlines. Any .ttf or .otf file on the computer is a font for PDFjet as well. Every font is embedded as a subset when the document is completed, the outlines of the glyphs it does not draw left out. What a PDF embeds:

  • A TrueType font, /FontFile2, keeps its glyphs drawn and the tables a reader needs: head, hhea, hmtx, maxp, loca, glyf, cvt , fpgm, prep, gasp, cmap, OS/2, name and post (without the names of the glyphs).
  • A font of CFF outlines, /FontFile3 with /Subtype /CIDFontType0C, the way PDF carries such a font since PDF 1.3, embeds its CFF table alone: the charstrings of the glyphs drawn, and the subroutines they call, the others emptied, every glyph and subroutine keeping its number. A CID-keyed font, as Source Han Sans and Noto Sans CJK are, gets the identity charset, CID = glyph number, which is how PDFjet writes its text. A page of Japanese in Source Han Sans JP embeds about 115 KB of its 4.2 MB.
  • Every glyph keeps its number, so the pages, the widths and the text a reader copies are those of the whole font; the font's name has the six capitals and a plus of a subset, its widths and its ToUnicode map list the glyphs kept, and a PDF/A-1 document has the CIDSet PDF/A-1 asks of a subset. The four ports embed the same subset, byte for byte.
  • setSubset(false) keeps a font whole, and so does a font whose license forbids subsetting, by the fsType of its OS/2 table. Fonts read from one file are one font program in a PDF, and the glyphs any of them draws are in it.
  • A font added to the objects of an existing PDF, new Font(objects, stream), is a subset too, from 9.0.5: its objects are numbered when it is added and filled in when the objects are added to the PDF, after the pages are drawn. setSubset(false) is called before then.

The SIL Open Font License of the fonts PDFjet carries allows a document to embed them, whole or subset: a document is not a distribution of the font.

Choosing a font you may embed

A font is software with a license, and a PDF that embeds a font carries a copy of it, a subset at least. That a font is on a computer does not mean that any program may embed it in documents. This is why PDFjet carries a set of fonts, chosen for their scripts and their licenses: so that a program has the fonts it needs, for most of the world's languages, that any document may embed. In this order:

  1. The fonts PDFjet carries first, in resources/fonts and read from the library itself: IBM Plex Sans and Serif and Cascadia Mono; and, in the add-on of the other scripts, IBM Plex Sans Arabic, Hebrew and Thai and IBM Plex Sans JP, KR, SC and TC. All are under the SIL Open Font License, which lets any document embed them, for any use.
  2. Google Fonts, fonts.google.com, next: every family there is under the SIL Open Font License, the Apache License 2.0 or the Ubuntu Font Licence, each of which lets a document embed the font. Use the files of the static folder of the download, one for each weight. A variable font, such as Roboto[wght].ttf, is drawn in its default weight only, so it gives Regular even for Bold, and a CFF2 variable font is refused.
  3. Any other font only once its license says so. The SIL Open Font License is the one to look for. The fonts that come with Windows or macOS, such as Georgia, Tahoma, Arial, Calibri or Helvetica Neue, are licensed for use on that computer: copying the file to a server to make PDFs there, or shipping it with an application, is usually not allowed, whatever embedding the font's own fsType permits. A font bought from a foundry has its own terms, often a license for embedding in documents or apps apart from the one for the desktop.

The fsType of a font's OS/2 table is its maker's signal of what embedding allows, and PDFjet keeps a font that forbids subsetting whole; but the license is what the maker granted, and it is the one to read. This is not legal advice: when in doubt, ask the font's maker.

Up to 9.0.3 the bundled fonts came as .otf.stream and .ttf.stream files, their metrics and their outlines compressed ahead of time, so that a document only copied them. Subsetting made them unneeded: a subset of the .ttf is a fraction of the stream's size, and as fast to make. From 9.0.5 PDFjet reads .otf and .ttf fonts alone, and a path to a .ttf.stream or .otf.stream file opens the .ttf file of the same name beside it, so code written for 9.0.3 runs as it is with the fonts of 9.0.5.

Right to left text

Bidi.reorderVisually prepares a line of Hebrew, Arabic, Persian or Urdu text for drawing. It puts the line in visual order, with left to right text such as Latin words and numbers nested in it one level deep, and replaces the Arabic, Persian and Urdu letters with their joined forms, including the lam-alef ligature. TextBlock.setRightToLeft(true) wraps right to left text at the width of the text block and prepares each line that way. Example_04 shows both.

The letters are shaped by replacing them with the presentation forms that fonts map to Unicode. PDFjet does not use a font's OpenType substitution table (GSUB), and uses only the mark positioning of its positioning table (GPOS), so:

  • Urdu is drawn in the Naskh style of the Arabic fonts, not in Nastaliq, the style Urdu is usually printed in. Nastaliq fonts, such as Noto Nastaliq Urdu, depend on the OpenType tables, so they are not expected to work.
  • Diacritics are positioned on their letters by the GPOS table of the font, as described in Marks.
  • The marks on a lam-alef ligature are put on its lam.
  • A font's localized forms, such as the Urdu shapes of some digits, and its optional ligatures and kerning are not used.
  • Only the letters of Arabic, Persian and Urdu are shaped. Letters used only by other languages written in Arabic script, such as Pashto, Sindhi or Kurdish, are drawn unjoined.
  • Each string is laid out as a right to left line. The explicit embedding, override and isolate controls are left out, and text that is already shaped into presentation forms is not supported.

Right to left text is drawn in visual order, and PDF viewers put it back in logical order when the text is copied or extracted. The joined letters map back to the letters they stand for, so the words come out as typed, but:

  • A bracket in right to left text is drawn with the glyph of its mirror image, and the viewers reverse the line without mirroring the brackets back. So Bidi.reorderVisually puts a right-to-left mark (U+200F) before each bracket it mirrors, and PDFjet draws the bracket in a marked content span that has the bracket that was typed as its ActualText, and does not draw the mark. Brackets around left to right text, as in مرحبا (hello) عالم, stay with that text when it is copied, so Bidi.reorderVisually puts a left-to-right mark (U+200E) on each side of the run instead, and PDFjet draws the run, brackets included, in one span whose ActualText is its text between the two marks. Poppler and MuPDF then copy the brackets on the right sides of the text, and the two marks, which are invisible, with it. MuPDF can move a bracket at either end of a line to the other end, and Poppler puts a space before a closing bracket after a word whose last letter has a mark, as in (كَتَبَ ).
  • The zero width non-joiner and joiner, as in می‌خواهم, are not drawn, but each is put in the ActualText of the glyph before it, with a space glyph that takes no room standing in for it, so Poppler and MuPDF copy them. A font that has glyphs for them draws those instead. A joiner inside a lam-alef ligature is left out.
  • Poppler separates an Arabic comma or a period from the word before it, as in مادر ،, and MuPDF moves numbers, such as ۱۴۰۳, next to a word beside them.

Marking the lines with their text in logical order as ActualText does not help: Poppler and MuPDF reverse that text as well.

Text without spaces between words

TextBlock wraps text at its spaces. Thai, Lao, Khmer and Burmese text has no spaces between its words, so put a zero width space (U+200B) between the words where a line may break. It is not drawn. A word too wide for a line by itself is broken between its characters, so text without zero width spaces still fits in the text block, but its lines can break inside words. A right to left word is shaped as a whole before it is broken, so its letters keep their joined forms on both sides of the break. The Thai text of Example_04, resources/data/languages/thai.txt, has a zero width space between its words.

Marks

A mark, like a Hebrew or Arabic vowel mark or a Thai tone mark, is moved to where the GPOS table of the font puts it: on its letter or ligature, or on the mark it attaches to, like a Thai tone mark above an upper vowel or an Arabic fatha above a shadda. The marks of a letter are stacked in the order HarfBuzz puts them in, so the stacking does not depend on the order they were typed in. A font without the table draws its marks where the font puts them by default, and a mark on another mark on top of it.

Text extraction tools take a mark that is moved up or down for text off the line, and break the word at it. Each word with a moved mark is drawn in a marked content span that has the text of the word as its ActualText, so Poppler extracts the word whole. MuPDF 1.27 extracts the letters and marks in order, but puts spaces inside some of these words, after a moved mark, going by the positions of the glyphs rather than by the ActualText.

Scripts whose letters change shape or order by the letters around them, such as Devanagari (Hindi), Bengali, Tamil and the other Indic scripts, Khmer, Myanmar, Sinhala and the Nastaliq style of Urdu, are not supported: they need the substitution table (GSUB) and the reordering of a shaping engine, which PDFjet does not have. Their letters are drawn one after another, so a vowel written before its consonant comes after it, and conjuncts are not joined.

Encryption

Encryption encrypts a PDF with 256-bit AES, revision 6 of the standard security handler of ISO 32000-2, with a user password, an owner password and the permissions granted to the user. Example_47 shows it in all four ports. The Java, C# and Go ports use the ciphers and hash functions of their platforms. Swift has no cryptography in its standard library on Linux, and this library has no dependencies on external packages, so the Swift port has its own AES, SHA-2, MD5 and RC4 in Sources/PDFjet/Cryptography.swift, which its Decryptor uses as well. The four ports write the same encryption dictionary, with random salts in the password hashes.

A password is used as typed, in UTF-8, and at most 127 bytes of it are used, which is where PDF readers cut it too. (The Poppler command line tools, such as pdftotext -upw, take passwords of at most 32 characters; MuPDF and qpdf take the full length.) ISO 32000-2 also asks for the SASLprep normalization of the password, which none of the ports applies: Go has no Unicode normalization without an external package, and the ports are kept the same. Type passwords in precomposed (NFC) form, as keyboards produce them. A password that is not set is empty, so a PDF whose user password is not set opens without a prompt, with the permissions applied.

PDF.read also reads encrypted PDFs, made by PDFjet or by other producers: RC4 and AES-128 (revisions 2 to 4 of the standard security handler) and AES-256 (revisions 5 and 6). A PDF that opens without a password is decrypted by the plain read. For one that needs a password, pass its user or owner password to read(inputStream, password) in Java and C#, ReadWithPassword(buf, password) in Go, or read(from:password:) in Swift. A wrong password raises an error that says so, and so does a missing one; in Go, Read and ReadWithPassword return it.

A PDF/UA file may be encrypted; a PDF/A file may not. ISO 14289-1 requires an encrypted PDF/UA file to grant the permission to extract content for accessibility, so Encryption grants it when the compliance is PDF/UA, whatever the Permissions say. Set the compliance before the encryption for this to apply. Such a file passes veraPDF's ua1 check with --password in all four ports. veraPDF 1.30.2 computes a wrong file key for about one in forty AES-256 files, whichever producer made them, because the loop of its password hash (ISO 32000-2 Algorithm 2.B) runs one round too many when the last byte of the round's output is exactly the round number minus 31; it then reports Can't decrypt string and a metadata parsing failure. Poppler and MuPDF open those files, and so does PDF.read. The Swift AES in Cryptography.swift encrypts about 40 MB per second in a release build and 1 MB per second in a debug build.

Merging documents

merge adds all the pages of a document that was read after the pages of a PDF, in their order. A PDF can merge several documents and draw pages of its own before, between and after them; Example_49 puts a cover page in front of three documents:

PDF pdf = new PDF(new BufferedOutputStream(new FileOutputStream("merged.pdf")));
Page cover = new Page(pdf, Letter.PORTRAIT);
// ... draw the cover page
pdf.merge(pdf.read(new FileInputStream("report.pdf")));
pdf.merge(pdf.read(new FileInputStream("terms.pdf")));
pdf.complete();

The merged pages keep their content, fonts, images, annotations and links, and the pages that inherit their size, rotation or resources from the page tree of their document get them written on the page. What belongs to the whole document is left out: its bookmarks, form fields, tagging, named destinations and optional content settings, so a link to a named destination no longer leads anywhere. An encrypted document is merged once read has decrypted it with its password, and an encrypted PDF encrypts the pages it merges. A PDF/UA or PDF/A document cannot merge pages, which were not made for its compliance, and merge cannot be combined with addObjects, which rewrites one document. The objects that the pages use are written when merge is called, so the list that read returned is not needed afterwards. The method is Merge in C# and Go, where it returns an error, and merge in Swift, where it throws.

read decodes the cross-reference and object streams of a PDF as it reads, and leaves every other stream, such as an image or the content of a page, to be decoded the first time getData asks for it, so that reading a PDF of large images takes the memory of none of them. getData keeps what it decoded in the object, so its first call changes the object: call it from one thread at a time for the same object. All the streams of one PDF may decode to 256 MiB together, a budget the objects that one read returns share. A cross-reference or an object stream past it is an error of read, as the PDF cannot be read without its objects; any other stream past it, or one that cannot be decoded, has no data: getData returns null, nil in Go, and an empty array in Swift.

Splitting documents

merge with page numbers adds the listed pages of a document that was read, counted from 1, in the order they are listed. A document is split by merging each part of it into a PDF of its own, as the objects that read returned can be merged into any number of PDFs. Example_49 writes each page of a document to a PDF of its own, and all of its pages in reverse order to one more:

List<PDFobj> objects = new PDF().read(new FileInputStream("report.pdf"));

PDF summary = new PDF(new BufferedOutputStream(new FileOutputStream("summary.pdf")));
summary.merge(objects, 1, 2);
summary.complete();

PDF appendix = new PDF(new BufferedOutputStream(new FileOutputStream("appendix.pdf")));
appendix.merge(objects, 3, 4, 5);
appendix.complete();

The listed pages are merged as merge merges a whole document, and a link to a page that is not in the same PDF leads nowhere. A page number that the document does not have, or one that is listed twice, is refused. The method is Merge(objects, params int[] pageNumbers) in C#, MergePages(objects, pageNumbers...) in Go and merge(objects, pageNumbers) with an array in Swift.

Untrusted input

The libraries read PDFs, images and fonts that can come from anywhere, so the sizes in them are checked before they are used, in the four ports. A stream of a PDF, a font stream, or the samples of a PNG or BMP image may decode to at most 256 MiB, and a larger one fails with an error instead of taking the memory: a few kilobytes of Flate, LZW or RunLength data can decode to gigabytes. The size, bit depth, color type and palette of a PNG or BMP image are checked before any buffer is allocated for the image, a PNG chunk is read as far as the file has it, so a length that the file does not have fails at its end, and the rows of a PNG image are decoded up to the size of the image. A PDF that the libraries write can be up to 9,999,999,999 bytes long, the largest offset that an entry of a cross-reference table holds; a larger one fails with an error.

Mistakes that are refused

PDFjet does not write a broken PDF when a program uses the API the wrong way. The call that finds the mistake fails with a message that names it, and complete() then refuses to finish the document, even when the program caught the exception and carried on. These mistakes are refused:

  • a coordinate, size or width that is NaN, infinite, or 2^31 or more;
  • a dash pattern that is not an array of non-negative numbers, not all zero, followed by a phase, such as "[3 3] 0", and a negative pen width;
  • a restoreGraphicsState without its saveGraphicsState, an addEMC without its addBDC or addArtifactBMC, and a page that ends with one still open;
  • drawing on a page after the next page was created or the PDF was completed, adding a page twice, after complete() or to another PDF, calling complete() twice, and completing a PDF that has no pages;
  • a font, image, stamp, optional content group, embedded file or bookmark page that belongs to another PDF, a stamp drawn before its own complete(), and stamp text without a font or a text, or in a core or CJK font;
  • setEncryption or setCompliance after a font, an image or a page was added;
  • a page smaller than 3 or larger than 14,400 points, the limits of the PDF specification.

Characters that XML does not allow are left out of the XMP metadata, and an image, stamp or container scaled to nothing draws nothing.

Java throws an IllegalArgumentException for a bad value and an IllegalStateException for a call at the wrong time; C# throws an ArgumentException and an InvalidOperationException. Go and Swift record the first mistake and carry on, leaving out what they can, and Complete returns the mistake as an error, or complete() throws it; a Go function that already returns an error, or a Swift function that already throws, reports it at once as well.

Port differences

Public setters return the object they were called on, so calls can be chained. The Drawable interface declares drawOn and setLocation in all four ports. Java, C# and Swift let each class return its own type from setLocation. Go does not: a Go type only satisfies an interface with an exact signature match, so in Go the SetLocation of a Drawable type returns Drawable, and in a chain of setter calls it goes last, right before DrawOn:

image.ScaleBy(0.5).SetLocation(50.0, 50.0).DrawOn(page)

DrawOn returns the x and y coordinates of the bottom right corner of the component, as a float[] in Java and C#, a [Float] in Swift and a [2]float32 in Go. DonutChart.setLocation sets the top left corner of the chart, its title above the circle when it has one, and drawOn returns the bottom right corner of all it draws: the title, the circle, and the leader lines and labels or the legend.

Bidi.reorderVisually(str, from, to) counts from and to in the units that each language indexes its strings with: UTF-16 code units in Java and C#, as length and substring do, bytes in Go (ReorderVisuallyPart), as len and slicing do, and Unicode scalars in Swift, where a String has no integer indexes, as its unicodeScalars view does.

Where Java and C# throw an Exception with a message, Swift throws a PDFjetError, whose message and description are that message, and Go returns an error, or panics where the function returns none. drawOn throws in Java and C# and panics in Go, but not in Swift, so the Swift Barcode initializer checks what the other ports check in drawOn, Code 39 text and the barcode type, and throws the same messages from init. A mistake that would break the PDF is reported through Complete in Go and complete() in Swift; see "Mistakes that are refused".

Permissions takes typed UserAccess values in the four ports. C# has a [Flags] enum, Go a UserAccess bit set and Swift an OptionSet, and the values are combined with |. A Java enum is not a set of flags, so Java grant and revoke take a list of values and getAccess and setAccess a Set<UserAccess>, as Example_47 shows. In all four ports the values are the permission bits of the standard (getValue() in Java and Swift), getAccess holds the /P entry of the encryption dictionary without its reserved bits, the constructor from an int (Go NewPermissionsFromInt) keeps the permission bits of a /P value, and UserAccess.isSetIn(access) (Go IsSetIn, C# HasFlag) asks whether one permission is in it.

Names, overloads and constructors

The Java name is the reference. C# and Go use its PascalCase form (setLocation, SetLocation), Swift the same camelCase name with unlabelled arguments. Coordinates and sizes are float in Java and C#, Float in Swift and float32 in Go, with no double overloads: a Java or C# caller writes 50f or casts a double. Swift has one form with default arguments where Java has a shorter overload, and Go, which cannot overload, gives the other form a suffix: SetTextColorRGB for setTextColor(float[]), DrawStringUsingFontSize and DrawStringUsingSpacing for the drawString overloads, StringWidthUsingFallbackFont for stringWidth with a fallback font, MergePages for merge with page numbers, SetTableRows for the BigTable.setTableData that takes a header and rows, and AddCoreFontResource, AddFontResource and AddImageResource for the addResource overloads of Page and PDFobj. Every color setter takes an int like Color.blue or the red, green and blue components from 0 to 1 as a float[] ([Float] in Swift, [3]float32 in Go, with the RGB suffix).

Constructors are New<Type> functions in Go, again with a suffix for an overload: NewBookmarkAt, NewEmbeddedFileAtPath, NewImageForObjects, NewPageDetached for Page.DETACHED, NewTableFromFile, and NewFont, NewFontFromFile, NewCoreFont, NewCJKFont and NewFontForObjects for the overloads of the Font constructor, and NewEmptyCell(font) and NewEmptyTextLine(font) for Cell(font) and TextLine(font). Where the other ports have an overload with fewer arguments, Go has the full form only: NewLine, NewRect and NewPoint with their coordinates, NewParagraph(), Table.SetTableData(data, headerRows), Page.AddBDC with the language, and Font.GetAscent, GetDescent, GetBodyHeight, GetUnderlinePosition and GetUnderlineThickness with the font size. The PDF constructors are NewPDF(writer), NewPDFFile(path), which opens the file and returns an error when it cannot, and NewPDFReader() for the PDF() of the other ports that only reads documents; Complete returns the first error writing the document, where the other ports throw it. content.GetFromReader is Content.getFromStream, and Go's PDF.Read and ReadWithPassword take the whole PDF as a []byte where the other ports read it from a stream. Java's Encryption is in com.pdfjet, next to PDF, so the two share their package-private members; Passwords, Permissions and UserAccess are in com.pdfjet.encryption.

Floating point on ARM

The four ports compute the same results on x86-64 and on ARM, AWS Graviton and Apple Silicon among them, as they all use the same 32- and 64-bit IEEE 754 arithmetic. One thing stood in the way, in Go alone: the Go specification lets the compiler fuse x*y + z into one fused multiply-add, which rounds once instead of twice, and Go's arm64 compiler does. A result could then differ from x86-64's and the other ports' in its last bit, and, at a boundary, a line of text break at another word. Java never fuses of itself, and C# and Swift do not by default.

So in the Go port every float multiplication, and every division by a constant, which the compiler makes a multiplication, is wrapped in a conversion to its own type, which the specification says rounds it and stops the fusing:

width := float32(fontSize*scale) + padding // not fontSize*scale + padding

The conversion looks redundant, and an IDE may offer to remove it; it must stay. Each file that has them says so at its top. check-no-fma.sh, run by the Build workflow, has the compiler print the arm64 code of the Go port and fails, naming the file and the line, if a fused multiply-add appears in it; it needs no ARM computer. Go's own math package is still fused on arm64; PDFjet uses it for arcs, ellipses and SVG alone, where a last bit changes no line or page. The rule since 5 October 2026: in the Go port, write a float multiplication as float32(a*b), or float64(a*b).

Constants and fields in Go

Java, C# and Swift keep constants in classes; Go keeps them in packages: color.Blue for Color.blue, shape.Circle for Shape.CIRCLE, structelem.P for StructElem.P, border.Top, compliance.PDF_UA_1, direction, scriptposition, capstyle, joinstyle, pagelayout, pagemode, pathoperator, mark, errorcorrectionlevel and the font families (IBMPlexSans.Regular). The constants of a package have its type, as alignment.Alignment and pathoperator.PathOperator, so a plain int or string variable does not compile where one is expected. The core fonts are functions, corefont.Courier(), where the other ports have CoreFont.COURIER. The page sizes are functions too, letter.Portrait() for Letter.PORTRAIT, because a caller can assign to a Go package variable. In all four ports a page size is a PageSize that cannot be changed, with getWidth and getHeight; Go's is pagesize.PageSize, made with pagesize.NewPageSize for a size that has no package. Fields are private in every port; Paragraph and Title have the getters an example needs (getX1, getPrefix). Permissions prints through String() in Go and description in Swift where Java and C# have toString.

Two conventions hold in every port. A rotation is setRotation(degrees) and a positive angle turns clockwise, as on a screen, because y grows downward on a PDFjet page; a negative angle turns counterclockwise. That holds for shapes, images, pages, text (setTextRotation) and arcs (Arc.setStartAngle and setSweep) alike. A spacing setter that takes points is a gap, added to the space the text already takes (TextFrame.setParagraphGap), and one that takes a multiple of the line is a spacing (setLineSpacing, TextColumn.setParagraphSpacing).

About

Standards-compliant PDFs, at jet speed, in Java, C#, Go and Swift

Topics

Resources

Security policy

Stars

32 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages