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.
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.
- 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 in many scripts |
![]() Tables |
![]() Charts |
![]() Barcodes |
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.7Or 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.WorldFrom 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@latestIf 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@latestpackage 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_nofontsThe 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()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 |
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 gccOn 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.
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 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.
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.
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.
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.
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.
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,nameandpost(without the names of the glyphs). - A font of CFF outlines,
/FontFile3with/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 itsOS/2table. 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.
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:
- The fonts PDFjet carries first, in
resources/fontsand 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. - 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
staticfolder of the download, one for each weight. A variable font, such asRoboto[wght].ttf, is drawn in its default weight only, so it gives Regular even for Bold, and a CFF2 variable font is refused. - 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.
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.reorderVisuallyputs 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, soBidi.reorderVisuallyputs 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.
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.
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 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.
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.
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.
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.
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
restoreGraphicsStatewithout itssaveGraphicsState, anaddEMCwithout itsaddBDCoraddArtifactBMC, 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, callingcomplete()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; setEncryptionorsetComplianceafter 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.
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.
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.
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 + paddingThe 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).
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).



