Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ The features of OpenPDF include:
* Encryption: You can encrypt PDF documents for security purposes.
* Page Layout: OpenPDF allows you to set the page size, orientation, and other layout properties.
* PDF 2.0 support (ISO 32000-2).
* Brotli stream compression (`/BrotliDecode`) for creating and reading PDF streams compressed with
[Brotli](https://github.com/google/brotli) – see [Brotli compression](#brotli-compression).

[![Maven Central](https://img.shields.io/maven-central/v/com.github.librepdf/openpdf.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/com.github.librepdf/openpdf)
![CI](https://github.com/LibrePDF/OpenPDF/actions/workflows/maven.yml/badge.svg)
Expand Down Expand Up @@ -192,6 +194,11 @@ and use the class `org.librepdf.openpdf.fonts.Liberation`.
</dependency>
```

### Brotli4j

Brotli4j is a required dependency for Brotli stream compression support.
<https://github.com/hyperxpro/Brotli4j/>

### Supporting complex glyph substitution/ Ligature substitution

OpenPDF supports glyph substitution which is required for correct rendering of fonts ligature substitution requirements.
Expand All @@ -204,6 +211,38 @@ OpenPDF supports OpenType layout, glyph positioning, reordering and substitution
positioning of accents, the rendering of non-Latin and right-to-left scripts. OpenPDF supports DIN 91379.
See: [wiki](https://github.com/LibrePDF/OpenPDF/wiki/Accents,-DIN-91379,-non-Latin-scripts)

### Brotli compression

OpenPDF can read and write PDF streams compressed with
[Brotli](https://github.com/google/brotli) — exposed in the PDF as the
`/BrotliDecode` filter, which is being standardised for PDF 2.0 (ISO 32000-2)
through [ISO/TS 32001](https://www.iso.org/standard/45874.html). The codec is
backed by [brotli4j](https://github.com/hyperxpro/Brotli4j) (a required
dependency, with native binaries shipped for Linux / macOS / Windows on x86_64
and aarch64).

Enable Brotli for the page content streams produced by a writer:

```java
PdfWriter writer = PdfWriter.getInstance(document, out);
writer.setUseBrotliCompression(true); // /BrotliDecode instead of /FlateDecode
```

…or globally for every subsequently-created `PdfWriter`:

```java
Document.useBrotliCompression = true;
```

Reading is fully transparent: `PdfReader.getStreamBytes(...)` and
`PdfReader.getPageContent(...)` decode `/BrotliDecode` automatically, so OpenPDF
opens Brotli-compressed PDFs (e.g. those produced by AutoCAD's
`pdfplot11.hdi`) without any extra configuration. The raw codec is available as
`org.openpdf.text.pdf.codec.BrotliFilter` (`encode` / `decode`).

Default compression remains `/FlateDecode` for compatibility with older
PDF readers that do not yet implement ISO/TS 32001.

### Optional

- [BouncyCastle](https://www.bouncycastle.org/) (BouncyCastle is used to sign PDF files, so it's a recommended
Expand Down
5 changes: 5 additions & 0 deletions openpdf-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
<artifactId>fop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>brotli4j</artifactId>
<version>${brotli4j.version}</version>
</dependency>

<!-- Test Deps -->
<dependency>
Expand Down
8 changes: 8 additions & 0 deletions openpdf-core/src/main/java/org/openpdf/text/Document.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@
* Allows the pdf documents to be produced without compression for debugging purposes.
*/
public static boolean compress = true;
/**
* When {@code true}, page content streams written by OpenPDF are compressed with the
* Brotli algorithm and tagged with the {@code /BrotliDecode} filter instead of the
* default {@code /FlateDecode}. Brotli is being standardised as a PDF 2.0
* (ISO 32000-2) stream filter through ISO/TS 32001. This requires {@link #compress}
* to also be {@code true}. Default is {@code false}.
*/
public static boolean useBrotliCompression = false;

Check warning on line 121 in openpdf-core/src/main/java/org/openpdf/text/Document.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make useBrotliCompression a static final constant or non-public and provide accessors if needed.

See more on https://sonarcloud.io/project/issues?id=LibrePDF_OpenPDF&issues=AZ4Cq_ga9XQgJ0IQt0bR&open=AZ4Cq_ga9XQgJ0IQt0bR&pullRequest=1555

Check warning on line 121 in openpdf-core/src/main/java/org/openpdf/text/Document.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make this "public static useBrotliCompression" field final

See more on https://sonarcloud.io/project/issues?id=LibrePDF_OpenPDF&issues=AZ4Cq_ga9XQgJ0IQt0bS&open=AZ4Cq_ga9XQgJ0IQt0bS&pullRequest=1555
/**
* When true the file access is not done through a memory mapped file. Use it if the file is too big to be mapped in
* your address space.
Expand Down
27 changes: 19 additions & 8 deletions openpdf-core/src/main/java/org/openpdf/text/pdf/PdfContents.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.openpdf.text.DocWriter;
import org.openpdf.text.Document;
import org.openpdf.text.Rectangle;
import org.openpdf.text.pdf.codec.BrotliFilter;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.zip.Deflater;
Expand Down Expand Up @@ -86,17 +87,26 @@ class PdfContents extends PdfStream {
Rectangle page) throws BadPdfFormatException {
super();
try {
OutputStream out = null;
Deflater deflater = null;
PdfWriter writer = text.getPdfWriter();
boolean useBrotli = Document.compress
&& writer != null
&& writer.isUseBrotliCompression();
streamBytes = new ByteArrayOutputStream();
if (Document.compress) {

OutputStream out;
Deflater deflater = null;
if (useBrotli) {
compressed = true;
out = BrotliFilter.encoder(streamBytes);
} else if (Document.compress) {
compressed = true;
compressionLevel = text.getPdfWriter().getCompressionLevel();
compressionLevel = writer.getCompressionLevel();
deflater = new Deflater(compressionLevel);
out = new DeflaterOutputStream(streamBytes, deflater);
} else {
out = streamBytes;
}

int rotation = page.getRotation();
switch (rotation) {
case 90:
Expand Down Expand Up @@ -143,12 +153,13 @@ class PdfContents extends PdfStream {
if (deflater != null) {
deflater.end();
}

put(PdfName.LENGTH, new PdfNumber(streamBytes.size()));
if (compressed) {
put(PdfName.FILTER, useBrotli ? PdfName.BROTLIDECODE : PdfName.FLATEDECODE);
}
} catch (Exception e) {
throw new BadPdfFormatException(e.getMessage());
}
put(PdfName.LENGTH, new PdfNumber(streamBytes.size()));
if (compressed) {
put(PdfName.FILTER, PdfName.FLATEDECODE);
}
}
}
5 changes: 5 additions & 0 deletions openpdf-core/src/main/java/org/openpdf/text/pdf/PdfName.java
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,11 @@ public class PdfName extends PdfObject implements Comparable<PdfName> {
* A name
*/
public static final PdfName FLATEDECODE = new PdfName("FlateDecode");
/**
* The {@code /BrotliDecode} filter — being standardised as a PDF 2.0 stream filter
* through ISO/TS 32001.
*/
public static final PdfName BROTLIDECODE = new PdfName("BrotliDecode");
/**
* A name
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.openpdf.text.PageSize;
import org.openpdf.text.Rectangle;
import org.openpdf.text.error_messages.MessageLocalization;
import org.openpdf.text.pdf.codec.BrotliFilter;
import org.openpdf.text.exceptions.BadPasswordException;
import org.openpdf.text.exceptions.InvalidPdfException;
import org.openpdf.text.exceptions.UnsupportedPdfException;
Expand Down Expand Up @@ -872,6 +873,9 @@ public static byte[] getStreamBytes(PRStream stream,
}
case "/Crypt":
break;
case "/BrotliDecode":
b = BrotliFilter.decode(b);
break;
default:
throw new UnsupportedPdfException(
MessageLocalization.getComposedMessage(
Expand Down
29 changes: 29 additions & 0 deletions openpdf-core/src/main/java/org/openpdf/text/pdf/PdfWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,12 @@ public class PdfWriter extends DocWriter implements
* @since 2.1.3
*/
protected int compressionLevel = PdfStream.DEFAULT_COMPRESSION;
/**
* Whether streams written by this writer (notably page content streams) should be
* compressed using Brotli ({@code /BrotliDecode}) instead of Flate ({@code /FlateDecode}).
* Defaults to the value of {@link org.openpdf.text.Document#useBrotliCompression}.
*/
protected boolean useBrotliCompression = Document.useBrotliCompression;
/**
* The fonts of this document
*/
Expand Down Expand Up @@ -2050,6 +2056,29 @@ public void setCompressionLevel(int compressionLevel) {
}
}

/**
* Returns whether this writer compresses page content streams with Brotli
* ({@code /BrotliDecode}) instead of Flate ({@code /FlateDecode}).
*
* @return {@code true} if Brotli compression is enabled
*/
public boolean isUseBrotliCompression() {
return useBrotliCompression;
}

/**
* Enables or disables Brotli compression for page content streams produced by this
* writer. When enabled, the streams use the {@code /BrotliDecode} filter — being
* standardised as a PDF 2.0 (ISO 32000-2) stream filter through ISO/TS 32001 —
* instead of {@code /FlateDecode}. Has no effect if {@link Document#compress}
* is {@code false}.
*
* @param useBrotliCompression {@code true} to enable Brotli, {@code false} for Flate
*/
public void setUseBrotliCompression(boolean useBrotliCompression) {
this.useBrotliCompression = useBrotliCompression;
Comment thread
andreasrosdalw marked this conversation as resolved.
}

/**
* Adds a <CODE>BaseFont</CODE> to the document but not to the page resources. It is used for templates.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2026 OpenPDF
*
* Licensed under the LGPL 2.1 or MPL 2.0.
*/
package org.openpdf.text.pdf.codec;

import com.aayushatharva.brotli4j.Brotli4jLoader;
import com.aayushatharva.brotli4j.decoder.DecoderJNI;
import com.aayushatharva.brotli4j.decoder.DirectDecompress;
import com.aayushatharva.brotli4j.encoder.BrotliOutputStream;
import com.aayushatharva.brotli4j.encoder.Encoder;

import java.io.IOException;
import java.io.OutputStream;

/**
* Brotli compression / decompression helper used to support PDF streams that use the
* {@code /BrotliDecode} filter. Brotli is being standardised as a stream filter for
* PDF 2.0 (ISO 32000-2) through ISO/TS 32001.
* <p>
* Backed by <a href="https://github.com/hyperxpro/Brotli4j">brotli4j</a>. The native
* library is loaded lazily on first use; consumers that never touch Brotli pay no cost.
*/
public final class BrotliFilter {

private BrotliFilter() {
}

private static void ensureAvailable() throws IOException {
try {
Brotli4jLoader.ensureAvailability();
} catch (Exception e) {
throw new IOException("Brotli native library could not be loaded. "
+ "Add a brotli4j native dependency for your platform.", e);
}
}

/**
* Compresses the given data using Brotli (default quality).
*
* @param in the data to compress
* @return the Brotli-compressed bytes
* @throws IOException on compression error
*/
public static byte[] encode(byte[] in) throws IOException {
ensureAvailable();
return Encoder.compress(in);
}

/**
* Returns a new streaming Brotli encoder that writes its compressed output to
* {@code out}. Closing the returned stream flushes and finishes the Brotli frame.
*
* @param out the destination for the compressed bytes
* @return an {@link OutputStream} that Brotli-compresses everything written to it
* @throws IOException if the encoder cannot be created
*/
public static OutputStream encoder(OutputStream out) throws IOException {
ensureAvailable();
return new BrotliOutputStream(out);
}

/**
* Decompresses Brotli-encoded data.
*
* @param in the compressed data
* @return the decoded bytes
* @throws IOException if the data cannot be decoded
*/
public static byte[] decode(byte[] in) throws IOException {
ensureAvailable();
DirectDecompress result = DirectDecompress.decompress(in);
if (result.getResultStatus() != DecoderJNI.Status.DONE) {
throw new IOException("Brotli decoding failed: " + result.getResultStatus());
}
return result.getDecompressedData();
}
}

Loading
Loading