CompressionStrategyTest.java
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2026 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itextpdf.kernel.pdf;
import com.itextpdf.commons.utils.SystemUtil;
import com.itextpdf.io.source.ByteArrayOutputStream;
import com.itextpdf.io.source.IFinishable;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.TestUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@Tag("IntegrationTest")
public class CompressionStrategyTest extends ExtendedITextTest {
private static final String SOURCE_FOLDER = "./src/test/resources/com/itextpdf/kernel/pdf/CompressionStrategyTest/";
private static final String DESTINATION_FOLDER = TestUtil.getOutputPath() + "/kernel/pdf/CompressionStrategyTest/";
@BeforeAll
public static void beforeClass() {
createDestinationFolder(DESTINATION_FOLDER);
}
private static final String testString = "Some test string for testing compression strategy should be big enough";
private static final byte[] TEST_CONTENT_STREAM_DATA = ("q\n"
+ "0 0 0 RG\n"
+ "10 w\n"
+ "1 j\n"
+ "2 J\n"
+ "100 100 m\n"
+ "300 100 l\n"
+ "200 300 l\n"
+ "s\n"
+ "1 0 0 RG\n"
+ "200 50 m\n"
+ "200 350 l\n"
+ "S\n"
+ "Q\n"
).getBytes(StandardCharsets.US_ASCII);
public static Iterable<Object[]> compressionStrategiesArguments() {
return Arrays.asList(
new Object[]{
new ASCII85CompressionStrategy(),
"ASCII85"
},
new Object[]{
new ASCIIHexCompressionStrategy(),
"ASCIIHex"
},
new Object[]{
new RunLengthCompressionStrategy(),
"RunLength"
}
);
}
public static Iterable<Object[]> twoCompressionStrategies() {
return Arrays.asList(
new Object[]{
new RunLengthCompressionStrategy(),
new ASCII85CompressionStrategy(),
"Run length on ASCII85"
},
new Object[]{
new ASCII85CompressionStrategy(),
new ASCIIHexCompressionStrategy(),
"ASCII85 on ASCIIHex"
},
new Object[]{
new ASCIIHexCompressionStrategy(),
new RunLengthCompressionStrategy(),
"ASCIIHex on RunLength"
}
);
}
@Test
public void ascii85DecodeTest() throws IOException {
doStrategyTest(new ASCII85CompressionStrategy());
}
@Test
public void asciiHexDecodeTest() throws IOException {
doStrategyTest(new ASCIIHexCompressionStrategy());
}
@Test
public void flateDecodeTest() throws IOException {
doStrategyTest(new FlateCompressionStrategy());
}
@Test
public void runLengthDecodeTest() throws IOException {
doStrategyTest(new RunLengthCompressionStrategy());
}
@ParameterizedTest(name = "{1}")
@MethodSource("compressionStrategiesArguments")
public void addStreamCompressionStampingModeTest(IStreamCompressionStrategy strategy, String compressionName) throws IOException {
String resultPath = DESTINATION_FOLDER + "stamped" + compressionName + "Streams.pdf";
StampingProperties props = new StampingProperties();
props.registerDependency(IStreamCompressionStrategy.class, strategy);
int streamCount = 3;
try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(SOURCE_FOLDER + compressionName + "ContentStream.pdf"),
new PdfWriter(resultPath), props)) {
PdfPage page = pdfDoc.addNewPage();
PdfStream stream = new PdfStream(testString.getBytes(StandardCharsets.UTF_8), CompressionConstants.BEST_COMPRESSION);
stream.makeIndirect(pdfDoc);
PdfArray contents = new PdfArray();
contents.add(page.getPdfObject().get(PdfName.Contents));
for (int i = 0; i < streamCount; i++) {
contents.add(stream.getIndirectReference());
}
page.getPdfObject().put(PdfName.Contents, contents);
}
try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(resultPath), props)) {
PdfPage page = pdfDoc.getPage(2);
Assertions.assertEquals(streamCount + 1, page.getContentStreamCount());
for (int i = 1; i < page.getContentStreamCount(); i++) {
PdfObject filterObject = page.getContentStream(i).get(PdfName.Filter);
Assertions.assertEquals(strategy.getFilterName(), filterObject);
Assertions.assertArrayEquals(page.getContentStream(i).getBytes(),
testString.getBytes(StandardCharsets.UTF_8));
}
}
}
@ParameterizedTest(name = "{2}")
@MethodSource("twoCompressionStrategies")
public void twoFiltersInSingleStreamTest(IStreamCompressionStrategy firstStrategy, IStreamCompressionStrategy secondStrategy, String testName) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
StampingProperties props = new StampingProperties();
props.registerDependency(IStreamCompressionStrategy.class, firstStrategy);
try (PdfDocument pdfDoc = new PdfDocument(
new PdfWriter(baos), props)) {
PdfPage page = pdfDoc.addNewPage();
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(50, 700);
canvas.showText(testString);
canvas.endText();
canvas.release();
PdfStream contentStream = page.getContentStream(0);
contentStream.setCompressionLevel(CompressionConstants.DEFAULT_COMPRESSION);
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
OutputStream zip = secondStrategy.createNewOutputStream(byteArrayStream, contentStream);
((ByteArrayOutputStream) contentStream.getOutputStream().getOutputStream()).writeTo(zip);
((IFinishable) zip).finish();
contentStream.setData(byteArrayStream.toByteArray());
PdfArray filters = new PdfArray();
filters.add(secondStrategy.getFilterName());
contentStream.put(PdfName.Filter, filters);
}
try (PdfDocument readDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(baos.toByteArray())))) {
PdfStream stream = readDoc.getFirstPage().getContentStream(0);
PdfArray filters = stream.getAsArray(PdfName.Filter);
Assertions.assertEquals(firstStrategy.getFilterName(), filters.getAsName(0));
Assertions.assertEquals(secondStrategy.getFilterName(), filters.getAsName(1));
byte[] decoded = stream.getBytes();
String decodedText = new String(decoded, StandardCharsets.UTF_8);
Assertions.assertTrue(decodedText.contains(testString));
}
}
private static void doStrategyTest(IStreamCompressionStrategy strategy) throws IOException {
long writePlainTime = SystemUtil.currentTimeMillis();
ByteArrayOutputStream plainPdfBytes = new ByteArrayOutputStream();
writeTestDocument(plainPdfBytes, null, CompressionConstants.NO_COMPRESSION);
byte[] bytes = plainPdfBytes.toByteArray();
long plainSize = bytes.length;
writePlainTime = SystemUtil.currentTimeMillis() - writePlainTime;
System.out.println(
"Generated PDF size without compression: "
+ plainSize + " bytes in " + writePlainTime + "ms"
);
long writeCompressedTime = SystemUtil.currentTimeMillis();
ByteArrayOutputStream compressedPdfBytes = new ByteArrayOutputStream();
writeTestDocument(compressedPdfBytes, strategy, CompressionConstants.DEFAULT_COMPRESSION);
byte[] compressedBytes = compressedPdfBytes.toByteArray();
long compressedSize = compressedBytes.length;
writeCompressedTime = SystemUtil.currentTimeMillis() - writeCompressedTime;
System.out.println(
"Generated PDF with `" + strategy.getFilterName() + "` compression: "
+ compressedSize + " bytes in " + writeCompressedTime + "ms"
);
System.out.println("Compression ratio: " + ((double) compressedSize / plainSize));
PdfDocument plainDoc = new PdfDocument(new PdfReader(new ByteArrayInputStream(compressedBytes)));
PdfDocument compressedDoc = new PdfDocument(new PdfReader(new ByteArrayInputStream(bytes)));
int numberOfPdfObjects = plainDoc.getNumberOfPdfObjects();
Assertions.assertEquals(
numberOfPdfObjects, compressedDoc.getNumberOfPdfObjects(),
"Number of PDF objects should be the same in both documents"
);
for (int objNum = 1; objNum <= numberOfPdfObjects; ++objNum) {
PdfObject plainObj = plainDoc.getPdfObject(objNum);
PdfObject compressedObj = compressedDoc.getPdfObject(objNum);
Assertions.assertEquals(
getType(plainObj), getType(compressedObj),
"PDF object type should be identical for object number " + objNum
);
if ((plainObj instanceof PdfStream) && (compressedObj instanceof PdfStream)) {
byte[] plainStreamBytes = ((PdfStream) plainObj).getBytes();
byte[] compressedStreamBytes = ((PdfStream) compressedObj).getBytes();
Assertions.assertArrayEquals(
plainStreamBytes, compressedStreamBytes,
"PDF stream bytes should be identical for object number " + objNum
);
}
}
}
private static void writeTestDocument(
ByteArrayOutputStream os,
IStreamCompressionStrategy strategy,
int compressionLevel
) {
DocumentProperties docProps = new DocumentProperties();
if (strategy != null) {
docProps.registerDependency(IStreamCompressionStrategy.class, strategy);
}
WriterProperties writerProps = new WriterProperties()
.setCompressionLevel(compressionLevel);
try (PdfDocument doc = new PdfDocument(new PdfWriter(os, writerProps), docProps)) {
PdfPage page = doc.addNewPage();
page.getFirstContentStream().setData(TEST_CONTENT_STREAM_DATA);
}
}
private static byte getType(PdfObject obj) {
if (obj == null) {
return (byte) 0;
}
return obj.getType();
}
}