diff --git a/pom.xml b/pom.xml
index ee1f4382..08b482d6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -78,7 +78,7 @@
2.2.03.776.1
- 9.4.0
+ 9.5.0-SNAPSHOT2.20.11.5.22
@@ -117,6 +117,11 @@
bouncy-castle-adapter${itext.version}
+
+ com.itextpdf
+ brotli-compressor
+ ${itext.version}
+ org.dom4jdom4j
diff --git a/src/main/java/com/itextpdf/rups/RupsConfiguration.java b/src/main/java/com/itextpdf/rups/RupsConfiguration.java
index 2358e766..42b87e6c 100644
--- a/src/main/java/com/itextpdf/rups/RupsConfiguration.java
+++ b/src/main/java/com/itextpdf/rups/RupsConfiguration.java
@@ -42,6 +42,10 @@ This file is part of the iText (R) project.
*/
package com.itextpdf.rups;
+import com.itextpdf.brotlicompressor.BrotliStreamCompressionStrategy;
+import com.itextpdf.kernel.pdf.FlateCompressionStrategy;
+import com.itextpdf.kernel.pdf.IStreamCompressionStrategy;
+import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.rups.conf.LookAndFeelId;
import com.itextpdf.rups.model.LoggerHelper;
import com.itextpdf.rups.model.MruListHandler;
@@ -94,6 +98,7 @@ public enum RupsConfiguration {
private static final String DEFAULT_HOME_VALUE = "home";
private static final String CLOSE_OPERATION_KEY = "ui.closeoperation";
private static final String DUPLICATE_OPEN_FILES_KEY = "rups.duplicatefiles";
+ private static final String DEFAULT_FILTER_KEY = "rups.defaultfilter";
private static final String HOME_FOLDER_KEY = "user.home";
private static final String LOCALE_KEY = "user.locale";
private static final String LOOK_AND_FEEL_KEY = "ui.lookandfeel";
@@ -132,6 +137,39 @@ public boolean canOpenDuplicateFiles() {
return Boolean.parseBoolean(value);
}
+ /**
+ * Returns which default compression filter RUPS should use for streams.
+ *
+ * @return PdfName of the compression filter, or {@code null} if none.
+ */
+ public PdfName getDefaultFilter() {
+ final String value = getValueFromSystemPreferences(DEFAULT_FILTER_KEY);
+ if (PdfName.FlateDecode.getValue().equals(value)) {
+ return PdfName.FlateDecode;
+ }
+ if (PdfName.BrotliDecode.getValue().equals(value)) {
+ return PdfName.BrotliDecode;
+ }
+ return null;
+ }
+
+ /**
+ * Returns which default compression filter strategy RUPS should use for streams.
+ *
+ * @return compression strategy for the default filter or {@code null} if
+ * no compression required.
+ */
+ public IStreamCompressionStrategy getDefaultFilterStrategy() {
+ final String value = getValueFromSystemPreferences(DEFAULT_FILTER_KEY);
+ if (PdfName.FlateDecode.getValue().equals(value)) {
+ return new FlateCompressionStrategy();
+ }
+ if (PdfName.BrotliDecode.getValue().equals(value)) {
+ return new BrotliStreamCompressionStrategy();
+ }
+ return null;
+ }
+
/**
* Returns the closing operation for the RUPS instance. Default it is returning EXIT_ON_CLOSE, but
* another value could be useful when embedding RUPS or calling it from a Java process.
@@ -218,6 +256,18 @@ public void setOpenDuplicateFiles(boolean value) {
this.temporaryProperties.setProperty(DUPLICATE_OPEN_FILES_KEY, Boolean.toString(value));
}
+ /**
+ * Sets which default compression filter RUPS should use for streams.
+ *
+ * @param value PdfName of the compression filter, or {@code null} if none.
+ */
+ public void setDefaultFilter(PdfName value) {
+ this.temporaryProperties.setProperty(
+ DEFAULT_FILTER_KEY,
+ value != null ? value.getValue() : "null"
+ );
+ }
+
/**
* Sets the default folder to use in JFileChoosers.
*
diff --git a/src/main/java/com/itextpdf/rups/conf/StreamFilterId.java b/src/main/java/com/itextpdf/rups/conf/StreamFilterId.java
new file mode 100644
index 00000000..5b0489f9
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/conf/StreamFilterId.java
@@ -0,0 +1,79 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.conf;
+
+import com.itextpdf.kernel.pdf.PdfName;
+import com.itextpdf.rups.view.Language;
+
+import java.util.Objects;
+
+public class StreamFilterId {
+ private final PdfName value;
+
+ public StreamFilterId(PdfName value) {
+ this.value = value;
+ }
+
+ public PdfName getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final StreamFilterId that = (StreamFilterId) o;
+ return Objects.equals(value, that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+
+ @Override
+ public String toString() {
+ return value != null ? value.getValue() : Language.NONE.getString();
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java b/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java
index 286a609a..3fcf68ca 100644
--- a/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java
+++ b/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java
@@ -164,7 +164,7 @@ public PdfReaderController(TreeSelectionListener treeSelectionListener,
pdfTree = new PdfTree();
pdfTree.addTreeSelectionListener(treeSelectionListener);
- final PdfTreeContextMenu menu = new PdfTreeContextMenu(pdfTree);
+ final PdfTreeContextMenu menu = new PdfTreeContextMenu(pdfTree, this);
pdfTree.setComponentPopupMenu(menu);
pdfTree.addMouseListener(new PdfTreeContextMenuMouseListener(menu, pdfTree));
@@ -386,15 +386,20 @@ public int addTreeNodeArrayChild(PdfObjectTreeNode parent, int index) {
public int deleteTreeChild(PdfObjectTreeNode parent, int index) {
parent.remove(index);
- ((DefaultTreeModel) pdfTree.getModel()).reload(parent);
+ getTreeModel().reload(parent);
return index;
}
+ public void deleteAllTreeChildren(PdfObjectTreeNode parent) {
+ parent.removeAllChildren();
+ getTreeModel().reload(parent);
+ }
+
//Returns index of the added child
public int addTreeNodeChild(PdfObjectTreeNode parent, PdfObjectTreeNode child, int index) {
parent.insert(child, index);
nodes.expandNode(child);
- ((DefaultTreeModel) pdfTree.getModel()).reload(parent);
+ getTreeModel().reload(parent);
return index;
}
@@ -477,4 +482,8 @@ private void forAllComponents(Consumer func) {
func.accept(objectPanel);
func.accept(streamPane);
}
+
+ private DefaultTreeModel getTreeModel() {
+ return (DefaultTreeModel) pdfTree.getModel();
+ }
}
diff --git a/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java b/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java
index 39e066c5..cace0e9d 100644
--- a/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java
+++ b/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java
@@ -339,7 +339,7 @@ public void valueChanged(TreeSelectionEvent evt) {
*/
final JPopupMenu menu = tree.getComponentPopupMenu();
if ((menu instanceof PdfTreeContextMenu) && (selectedNode instanceof IPdfContextMenuTarget)) {
- ((PdfTreeContextMenu) menu).setEnabledForNode((IPdfContextMenuTarget) selectedNode);
+ ((PdfTreeContextMenu) menu).prepareForNode((IPdfContextMenuTarget) selectedNode);
}
if (selectedNode instanceof PdfTrailerTreeNode) {
diff --git a/src/main/java/com/itextpdf/rups/io/encoders/ASCII85CompressionStrategy.java b/src/main/java/com/itextpdf/rups/io/encoders/ASCII85CompressionStrategy.java
new file mode 100644
index 00000000..d30afa35
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/io/encoders/ASCII85CompressionStrategy.java
@@ -0,0 +1,112 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.io.encoders;
+
+import com.itextpdf.kernel.pdf.IStreamCompressionStrategy;
+import com.itextpdf.kernel.pdf.PdfName;
+import com.itextpdf.kernel.pdf.PdfObject;
+import com.itextpdf.kernel.pdf.PdfStream;
+
+import java.io.OutputStream;
+
+/**
+ * A compression strategy that uses the {@code ASCII85Decode} filter for PDF
+ * streams.
+ *
+ *
+ * This strategy implements the {@link IStreamCompressionStrategy} interface
+ * and provides {@code ASCII85Decode} encoding.
+ *
+ *
+ * The strategy ensures, that streams are saved using just 7-bit ASCII
+ * characters, but it typically increases sizes of streams by 25% compared to
+ * just saving them as-is. So calling this a "compression strategy" is a
+ * misnomer.
+ */
+public class ASCII85CompressionStrategy implements IStreamCompressionStrategy {
+ /**
+ * Constructs a new {@link ASCII85CompressionStrategy} instance.
+ */
+ public ASCII85CompressionStrategy() {
+ // empty constructor
+ }
+
+ /**
+ * Returns the name of the compression filter.
+ *
+ * @return {@link PdfName#ASCII85Decode} representing the {@code ASCII85Decode} filter
+ */
+ @Override
+ public PdfName getFilterName() {
+ return PdfName.ASCII85Decode;
+ }
+
+ /**
+ * Returns the decode parameters for the {@code ASCII85Decode} filter.
+ *
+ * This implementation returns {@code null} as no special decode parameters
+ * are required for standard ASCII85 compression.
+ *
+ * @return {@code null} as no decode parameters are needed
+ */
+ @Override
+ public PdfObject getDecodeParams() {
+ return null;
+ }
+
+ /**
+ * Creates a new output stream with ASCII85 compression applied.
+ *
+ * This method wraps the original output stream in a {@link ASCII85OutputStream}
+ * that applies ASCII85 compression.
+ *
+ * @param original the original output stream to wrap
+ * @param stream the PDF stream containing compression configuration
+ *
+ * @return a new {@link ASCII85OutputStream} that compresses data using the ASCII85 algorithm
+ */
+ @Override
+ public OutputStream createNewOutputStream(OutputStream original, PdfStream stream) {
+ return new ASCII85OutputStream(original);
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/io/encoders/ASCII85OutputStream.java b/src/main/java/com/itextpdf/rups/io/encoders/ASCII85OutputStream.java
new file mode 100644
index 00000000..63b556ba
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/io/encoders/ASCII85OutputStream.java
@@ -0,0 +1,184 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.io.encoders;
+
+import com.itextpdf.io.source.IFinishable;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * An output stream that encodes data according to the {@code ASCII85Decode}
+ * filter from the PDF specification.
+ */
+public class ASCII85OutputStream extends FilterOutputStream implements IFinishable {
+ private static final int BASE = 85;
+ /**
+ * Offset to the first base-85 output char.
+ */
+ private static final int OFFSET = 33;
+ /**
+ * Size of the encoding block. After this amount of bytes data is converted
+ * and flush to the backing stream.
+ */
+ private static final int INPUT_LENGTH = 4;
+ /**
+ * Amount of bytes produced from a block of input bytes.
+ */
+ private static final int OUTPUT_LENGTH = 5;
+ /**
+ * Marker written, when all input bytes are zero. Not used for partial
+ * blocks.
+ */
+ private static final byte ALL_ZEROS_MARKER = 'z';
+ /**
+ * End Of Data marker.
+ */
+ private static final byte[] EOD = new byte[]{'~', '>'};
+
+ /**
+ * Encoding block buffer. Reused for encoding output, when flushing.
+ */
+ private final byte[] buffer = new byte[OUTPUT_LENGTH];
+ /**
+ * Bitwise OR of all bytes within the encoding block. Used to quickly
+ * check, whether the encoding block contains only zeros.
+ */
+ private int inputOr = 0;
+ /**
+ * Input bytes cursor within the buffer.
+ */
+ private int inputCursor = 0;
+
+ /**
+ * Flag for detecting, whether {@link #finish} has been called.
+ */
+ private final AtomicBoolean finished = new AtomicBoolean(false);
+
+ /**
+ * Creates a new {@code ASCIIHexDecode} encoding stream.
+ *
+ * @param out the output stream to write encoded data to
+ */
+ public ASCII85OutputStream(OutputStream out) {
+ super(out);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void write(int b) throws IOException {
+ int value = b & 0xFF;
+ buffer[inputCursor] = (byte) value;
+ inputOr |= value;
+ ++inputCursor;
+ writeBufferIfFull();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() throws IOException {
+ finish();
+ super.close();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void finish() throws IOException {
+ if (finished.getAndSet(true)) {
+ return;
+ }
+ // Writing the remainder
+ if (inputCursor > 0) {
+ if (inputOr == 0) {
+ // If all zeros, output is just n + 1 exclamation points
+ Arrays.fill(buffer, 0, inputCursor + 1, (byte) '!');
+ } else {
+ Arrays.fill(buffer, inputCursor, INPUT_LENGTH, (byte) 0);
+ convertBuffer();
+ }
+ out.write(buffer, 0, inputCursor + 1);
+ resetBuffer();
+ }
+ out.write(EOD);
+ flush();
+ }
+
+ private void writeBufferIfFull() throws IOException {
+ if (inputCursor < INPUT_LENGTH) {
+ return;
+ }
+ if (inputOr == 0) {
+ // Special case, if all zeros
+ out.write(ALL_ZEROS_MARKER);
+ } else {
+ convertBuffer();
+ out.write(buffer);
+ }
+ resetBuffer();
+ }
+
+ private void resetBuffer() {
+ inputOr = 0;
+ inputCursor = 0;
+ }
+
+ private void convertBuffer() {
+ long num = ((buffer[0] & 0xFFL) << 24)
+ | ((buffer[1] & 0xFFL) << 16)
+ | ((buffer[2] & 0xFFL) << 8)
+ | (buffer[3] & 0xFFL);
+ for (int i = OUTPUT_LENGTH - 1; i >= 0; --i) {
+ buffer[i] = (byte) (OFFSET + (num % BASE));
+ num /= BASE;
+ }
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/io/encoders/ASCIIHexCompressionStrategy.java b/src/main/java/com/itextpdf/rups/io/encoders/ASCIIHexCompressionStrategy.java
new file mode 100644
index 00000000..8a1cb822
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/io/encoders/ASCIIHexCompressionStrategy.java
@@ -0,0 +1,111 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.io.encoders;
+
+import com.itextpdf.kernel.pdf.IStreamCompressionStrategy;
+import com.itextpdf.kernel.pdf.PdfName;
+import com.itextpdf.kernel.pdf.PdfObject;
+import com.itextpdf.kernel.pdf.PdfStream;
+
+import java.io.OutputStream;
+
+/**
+ * A compression strategy that uses the {@code ASCIIHexDecode} filter for PDF
+ * streams.
+ *
+ *
+ * This strategy implements the {@link IStreamCompressionStrategy} interface
+ * and provides {@code ASCIIHexDecode} encoding.
+ *
+ *
+ * The strategy ensures, that streams are saved using just 7-bit ASCII
+ * characters, but it doubles the sizes of streams compared to just saving
+ * them as-is. So calling this a "compression strategy" is a misnomer.
+ */
+public class ASCIIHexCompressionStrategy implements IStreamCompressionStrategy {
+ /**
+ * Constructs a new {@link ASCIIHexCompressionStrategy} instance.
+ */
+ public ASCIIHexCompressionStrategy() {
+ // empty constructor
+ }
+
+ /**
+ * Returns the name of the compression filter.
+ *
+ * @return {@link PdfName#ASCIIHexDecode} representing the {@code ASCIIHexDecode} filter
+ */
+ @Override
+ public PdfName getFilterName() {
+ return PdfName.ASCIIHexDecode;
+ }
+
+ /**
+ * Returns the decode parameters for the {@code ASCIIHexDecode} filter.
+ *
+ * This implementation returns {@code null} as no special decode parameters
+ * are required for standard ASCIIHex compression.
+ *
+ * @return {@code null} as no decode parameters are needed
+ */
+ @Override
+ public PdfObject getDecodeParams() {
+ return null;
+ }
+
+ /**
+ * Creates a new output stream with ASCIIHex compression applied.
+ *
+ * This method wraps the original output stream in a {@link ASCIIHexOutputStream}
+ * that applies ASCIIHex compression.
+ *
+ * @param original the original output stream to wrap
+ * @param stream the PDF stream containing compression configuration
+ *
+ * @return a new {@link ASCIIHexOutputStream} that compresses data using the ASCIIHex algorithm
+ */
+ @Override
+ public OutputStream createNewOutputStream(OutputStream original, PdfStream stream) {
+ return new ASCIIHexOutputStream(original);
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/io/encoders/ASCIIHexOutputStream.java b/src/main/java/com/itextpdf/rups/io/encoders/ASCIIHexOutputStream.java
new file mode 100644
index 00000000..bf9838df
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/io/encoders/ASCIIHexOutputStream.java
@@ -0,0 +1,121 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.io.encoders;
+
+import com.itextpdf.io.source.IFinishable;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * An output stream that encodes data according to the {@code ASCIIHexDecode}
+ * filter from the PDF specification.
+ */
+public class ASCIIHexOutputStream extends FilterOutputStream implements IFinishable {
+ /**
+ * End Of Data marker.
+ */
+ private static final byte EOD = '>';
+ /**
+ * Array for mapping nibble values to the corresponding lowercase
+ * hexadecimal characters.
+ */
+ private static final byte[] CHAR_MAP = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+ };
+
+ /**
+ * Buffer for storing the output hex char pair.
+ */
+ private final byte[] buffer = new byte[2];
+
+ /**
+ * Flag for detecting, whether {@link #finish} has been called.
+ */
+ private final AtomicBoolean finished = new AtomicBoolean(false);
+
+ /**
+ * Creates a new {@code ASCIIHexDecode} encoding stream.
+ *
+ * @param out the output stream to write encoded data to
+ */
+ public ASCIIHexOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void write(int b) throws IOException {
+ int value = (b & 0xFF);
+ // Writing via a 2-elem buffer, in case `write(byte[])` on the
+ // underlying stream is more performant
+ buffer[0] = CHAR_MAP[value >> 4];
+ buffer[1] = CHAR_MAP[value & 0x0F];
+ out.write(buffer);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() throws IOException {
+ finish();
+ super.close();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void finish() throws IOException {
+ if (finished.getAndSet(true)) {
+ return;
+ }
+ out.write(EOD);
+ flush();
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/io/encoders/RunLengthCompressionStrategy.java b/src/main/java/com/itextpdf/rups/io/encoders/RunLengthCompressionStrategy.java
new file mode 100644
index 00000000..a31e6e2d
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/io/encoders/RunLengthCompressionStrategy.java
@@ -0,0 +1,111 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.io.encoders;
+
+import com.itextpdf.kernel.pdf.IStreamCompressionStrategy;
+import com.itextpdf.kernel.pdf.PdfName;
+import com.itextpdf.kernel.pdf.PdfObject;
+import com.itextpdf.kernel.pdf.PdfStream;
+
+import java.io.OutputStream;
+
+/**
+ * A compression strategy that uses the {@code RunLengthDecode} filter for PDF
+ * streams.
+ *
+ *
+ * This strategy implements the {@link IStreamCompressionStrategy} interface
+ * and provides {@code RunLengthDecode} encoding.
+ *
+ *
+ * Run-length encoding works best, when input data has long sequences of
+ * identical bytes. This is, usually, not the case for PDF content streams, so
+ * using this strategy will often cause a marginal size increase instead.
+ */
+public class RunLengthCompressionStrategy implements IStreamCompressionStrategy {
+ /**
+ * Constructs a new {@link RunLengthCompressionStrategy} instance.
+ */
+ public RunLengthCompressionStrategy() {
+ // empty constructor
+ }
+
+ /**
+ * Returns the name of the compression filter.
+ *
+ * @return {@link PdfName#RunLengthDecode} representing the {@code RunLengthDecode} filter
+ */
+ @Override
+ public PdfName getFilterName() {
+ return PdfName.RunLengthDecode;
+ }
+
+ /**
+ * Returns the decode parameters for the {@code RunLengthDecode} filter.
+ *
+ * This implementation returns {@code null} as no special decode parameters
+ * are required for standard run length compression.
+ *
+ * @return {@code null} as no decode parameters are needed
+ */
+ @Override
+ public PdfObject getDecodeParams() {
+ return null;
+ }
+
+ /**
+ * Creates a new output stream with run length compression applied.
+ *
+ * This method wraps the original output stream in a {@link RunLengthOutputStream}
+ * that applies run length compression.
+ *
+ * @param original the original output stream to wrap
+ * @param stream the PDF stream containing compression configuration
+ *
+ * @return a new {@link RunLengthOutputStream} that compresses data using the run length algorithm
+ */
+ @Override
+ public OutputStream createNewOutputStream(OutputStream original, PdfStream stream) {
+ return new RunLengthOutputStream(original);
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/io/encoders/RunLengthOutputStream.java b/src/main/java/com/itextpdf/rups/io/encoders/RunLengthOutputStream.java
new file mode 100644
index 00000000..ae3b19f0
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/io/encoders/RunLengthOutputStream.java
@@ -0,0 +1,188 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.io.encoders;
+
+import com.itextpdf.io.source.IFinishable;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * An output stream that encodes data according to the {@code RunLengthDecode}
+ * filter from the PDF specification.
+ */
+public class RunLengthOutputStream extends FilterOutputStream implements IFinishable {
+ /**
+ * Maximum length of a run. Applies to both "unique" and repeating ones.
+ */
+ private static final int MAX_LENGTH = 128;
+ /**
+ * End Of Data marker.
+ */
+ private static final byte EOD = (byte) 128;
+
+ /**
+ * Buffer for storing the pending run.
+ */
+ private final byte[] buffer = new byte[MAX_LENGTH];
+ /**
+ * Value, that repeats in a repeating run. Set to {@code -1}, when the
+ * pending run is a "unique" one.
+ */
+ private int repeatValue = -1;
+ /**
+ * Current length of the pending run.
+ */
+ private int currentLength = 0;
+
+ /**
+ * Flag for detecting, whether {@link #finish} has been called.
+ */
+ private final AtomicBoolean finished = new AtomicBoolean(false);
+
+ /**
+ * Creates a new {@code RunLengthDecode} encoding stream.
+ *
+ * @param out the output stream to write encoded data to
+ */
+ public RunLengthOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void write(int b) throws IOException {
+ int value = b & 0xFF;
+ // Case for continuing a repeating run
+ if (value == repeatValue) {
+ ++currentLength;
+ if (currentLength == MAX_LENGTH) {
+ writePending();
+ }
+ return;
+ }
+ /*
+ * If there was a repeating run, but we got a different value, then we
+ * need to write the current repeating run we had and start a new
+ * "unique" run.
+ */
+ if (repeatValue != -1) {
+ writePending();
+ buffer[currentLength] = (byte) value;
+ ++currentLength;
+ return;
+ }
+ /*
+ * As soon as we detect a sequence of 3 or more bytes, which are the
+ * same, we need to switch to a repeating run. For this we will write
+ * the values before the repeated one as a "unique" run and start a
+ * new repeating run at length 3.
+ *
+ * Technically speaking we can switch to a repeating run at 2 bytes,
+ * but in the vast majority of cases this will make the compression
+ * ratio worse.
+ */
+ if (currentLength >= 2
+ && buffer[currentLength - 1] == (byte) value
+ && buffer[currentLength - 2] == (byte) value) {
+ currentLength -= 2;
+ writePending();
+ repeatValue = value;
+ currentLength = 3;
+ return;
+ }
+ // Just continuing (or starting) a "unique" run
+ buffer[currentLength] = (byte) value;
+ ++currentLength;
+ if (currentLength == MAX_LENGTH) {
+ writePending();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() throws IOException {
+ finish();
+ super.close();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void finish() throws IOException {
+ if (finished.getAndSet(true)) {
+ return;
+ }
+ writePending();
+ out.write(EOD);
+ flush();
+ }
+
+ private void writePending() throws IOException {
+ if (currentLength <= 0) {
+ return;
+ }
+ if (repeatValue < 0) {
+ // Writing "unique" run
+ out.write(currentLength - 1);
+ out.write(buffer, 0, currentLength);
+ } else {
+ // Writing repeating run
+ out.write(257 - currentLength);
+ out.write(repeatValue);
+ }
+ resetPending();
+ }
+
+ private void resetPending() {
+ repeatValue = -1;
+ currentLength = 0;
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/model/PdfFile.java b/src/main/java/com/itextpdf/rups/model/PdfFile.java
index f41d2253..1e519775 100644
--- a/src/main/java/com/itextpdf/rups/model/PdfFile.java
+++ b/src/main/java/com/itextpdf/rups/model/PdfFile.java
@@ -43,10 +43,15 @@ This file is part of the iText (R) project.
package com.itextpdf.rups.model;
import com.itextpdf.kernel.exceptions.BadPasswordException;
+import com.itextpdf.kernel.pdf.CompressionConstants;
+import com.itextpdf.kernel.pdf.IStreamCompressionStrategy;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.ReaderProperties;
+import com.itextpdf.kernel.pdf.StampingProperties;
+import com.itextpdf.kernel.pdf.WriterProperties;
+import com.itextpdf.rups.RupsConfiguration;
import com.itextpdf.rups.view.Language;
import java.io.ByteArrayInputStream;
@@ -251,8 +256,8 @@ private boolean openDocumentReadWrite(byte[] password) throws IOException {
readerProperties
);
final ByteArrayOutputStream tempWriterOutputStream = new ByteArrayOutputStream();
- final PdfWriter writer = new PdfWriter(tempWriterOutputStream);
- document = new PdfDocument(reader, writer);
+ final PdfWriter writer = new PdfWriter(tempWriterOutputStream, createWriterProperties());
+ document = new PdfDocument(reader, writer, createStampingProps());
writerOutputStream = tempWriterOutputStream;
return true;
} catch (BadPasswordException e) {
@@ -293,4 +298,22 @@ private boolean openDocumentReadOnly(byte[] password) throws IOException {
return false;
}
}
+
+ private static WriterProperties createWriterProperties() {
+ final WriterProperties props = new WriterProperties();
+ if (RupsConfiguration.INSTANCE.getDefaultFilter() == null) {
+ props.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
+ }
+ return props;
+ }
+
+ private static StampingProperties createStampingProps() {
+ final StampingProperties props = new StampingProperties();
+ final IStreamCompressionStrategy filterStrategy =
+ RupsConfiguration.INSTANCE.getDefaultFilterStrategy();
+ if (filterStrategy != null) {
+ props.registerDependency(IStreamCompressionStrategy.class, filterStrategy);
+ }
+ return props;
+ }
}
diff --git a/src/main/java/com/itextpdf/rups/util/PdfStreamUtil.java b/src/main/java/com/itextpdf/rups/util/PdfStreamUtil.java
new file mode 100644
index 00000000..e5b5b1cd
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/util/PdfStreamUtil.java
@@ -0,0 +1,210 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.util;
+
+import com.itextpdf.io.source.IFinishable;
+import com.itextpdf.kernel.pdf.CompressionConstants;
+import com.itextpdf.kernel.pdf.IStreamCompressionStrategy;
+import com.itextpdf.kernel.pdf.PdfArray;
+import com.itextpdf.kernel.pdf.PdfName;
+import com.itextpdf.kernel.pdf.PdfNull;
+import com.itextpdf.kernel.pdf.PdfNumber;
+import com.itextpdf.kernel.pdf.PdfObject;
+import com.itextpdf.kernel.pdf.PdfStream;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public final class PdfStreamUtil {
+ private PdfStreamUtil() {
+ // Static class
+ }
+
+ public static void applyFilter(PdfStream stream, IStreamCompressionStrategy strategy)
+ throws IOException {
+ // Creating all the data first, so that the stream is still untouched
+ // in case of an exception
+ final byte[] encodedBytes = encode(stream.getBytes(false), strategy);
+ final PdfObject oldFilterValue = stream.get(PdfName.Filter);
+ final int oldFilterCount = getFilterCount(oldFilterValue);
+ final PdfObject newFilterValue = createNewFilterValue(oldFilterValue, strategy);
+ final PdfObject newDecodeParamsValue = createNewDecodeParamsValue(
+ stream.get(PdfName.DecodeParms), oldFilterCount, strategy
+ );
+ // Now applying everything to the stream itself
+ stream.setData(encodedBytes);
+ stream.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
+ stream.put(PdfName.Length, new PdfNumber(encodedBytes.length));
+ stream.put(PdfName.Filter, newFilterValue);
+ if (newDecodeParamsValue.isNull()) {
+ stream.remove(PdfName.DecodeParms);
+ } else {
+ stream.put(PdfName.DecodeParms, newDecodeParamsValue);
+ }
+ }
+
+ public static void setDataWithFilter(PdfStream stream, byte[] data, IStreamCompressionStrategy strategy)
+ throws IOException {
+ byte[] encodedBytes = data;
+ PdfObject filterValue = PdfNull.PDF_NULL;
+ PdfObject decodeParamsValue = PdfNull.PDF_NULL;
+ if (strategy != null) {
+ encodedBytes = encode(data, strategy);
+ filterValue = strategy.getFilterName();
+ decodeParamsValue = getDecodeParams(strategy);
+ }
+ stream.setData(encodedBytes);
+ stream.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
+ stream.put(PdfName.Length, new PdfNumber(encodedBytes.length));
+ if (filterValue.isNull()) {
+ stream.remove(PdfName.Filter);
+ } else {
+ stream.put(PdfName.Filter, filterValue);
+ }
+ if (decodeParamsValue.isNull()) {
+ stream.remove(PdfName.DecodeParms);
+ } else {
+ stream.put(PdfName.DecodeParms, decodeParamsValue);
+ }
+ }
+
+ public static void removeAllFilters(PdfStream stream) {
+ final byte[] decodedBytes = stream.getBytes();
+ stream.setData(decodedBytes);
+ stream.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
+ stream.put(PdfName.Length, new PdfNumber(decodedBytes.length));
+ stream.remove(PdfName.Filter);
+ stream.remove(PdfName.DecodeParms);
+ }
+
+ private static byte[] encode(byte[] original, IStreamCompressionStrategy strategy)
+ throws IOException {
+ final ByteArrayOutputStream target = new ByteArrayOutputStream(original.length);
+ // At the moment we need to pass a stream, but the only information
+ // used is the compression level, so we will make a dummy
+ final PdfStream dummyStream = new PdfStream();
+ dummyStream.setCompressionLevel(CompressionConstants.DEFAULT_COMPRESSION);
+ try (final OutputStream compressor = strategy.createNewOutputStream(target, dummyStream)) {
+ compressor.write(original);
+ ((IFinishable) compressor).finish();
+ }
+ return target.toByteArray();
+ }
+
+ private static PdfObject createNewFilterValue(PdfObject oldValue, IStreamCompressionStrategy strategy) {
+ if (oldValue == null) {
+ return strategy.getFilterName();
+ }
+ if (oldValue.isArray()) {
+ final PdfArray newValue = new PdfArray(strategy.getFilterName());
+ newValue.addAll((PdfArray) oldValue);
+ return newValue;
+ }
+ if (oldValue.isName()) {
+ final PdfArray newValue = new PdfArray(strategy.getFilterName());
+ newValue.add(oldValue);
+ return newValue;
+ }
+ return strategy.getFilterName();
+ }
+
+ private static PdfObject createNewDecodeParamsValue(
+ PdfObject oldValue,
+ int oldFilterCount,
+ IStreamCompressionStrategy strategy
+ ) {
+ final PdfObject prependDecodeParams = getDecodeParams(strategy);
+ // Assuming current DecodeParams are valid...
+ if (oldValue != null && oldValue.isArray()) {
+ final PdfArray oldValueArray = (PdfArray) oldValue;
+ final PdfArray newValue = new PdfArray(prependDecodeParams);
+ // We will also handle cases, when there was a size mismatch already
+ for (int i = 0; i < Math.min(oldFilterCount, oldValueArray.size()); ++i) {
+ newValue.add(oldValueArray.get(i, false));
+ }
+ for (int i = Math.min(oldFilterCount, oldValueArray.size()); i < oldFilterCount; ++i) {
+ newValue.add(PdfNull.PDF_NULL);
+ }
+ return newValue;
+ }
+ if (oldValue != null && oldValue.isDictionary()) {
+ final PdfArray newValue = new PdfArray(prependDecodeParams);
+ newValue.add(oldValue);
+ // We will also handle cases, when there was a size mismatch already
+ for (int i = 1; i < oldFilterCount; ++i) {
+ newValue.add(PdfNull.PDF_NULL);
+ }
+ return newValue;
+ }
+ if (!prependDecodeParams.isNull() && oldFilterCount > 0) {
+ final PdfArray newValue = new PdfArray(prependDecodeParams);
+ for (int i = 0; i < oldFilterCount; ++i) {
+ newValue.add(PdfNull.PDF_NULL);
+ }
+ return newValue;
+ }
+ return prependDecodeParams;
+ }
+
+ private static int getFilterCount(PdfObject filterValue) {
+ if (filterValue == null) {
+ return 0;
+ }
+ if (filterValue.isArray()) {
+ return ((PdfArray) filterValue).size();
+ }
+ if (filterValue.isName()) {
+ return 1;
+ }
+ return 0;
+ }
+
+ private static PdfObject getDecodeParams(IStreamCompressionStrategy strategy) {
+ final PdfObject decodeParams = strategy.getDecodeParams();
+ if (decodeParams == null || !decodeParams.isDictionary()) {
+ return PdfNull.PDF_NULL;
+ }
+ return decodeParams;
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/view/Language.java b/src/main/java/com/itextpdf/rups/view/Language.java
index 15cd2dc4..cca4809e 100644
--- a/src/main/java/com/itextpdf/rups/view/Language.java
+++ b/src/main/java/com/itextpdf/rups/view/Language.java
@@ -55,6 +55,7 @@ This file is part of the iText (R) project.
* in the resource bundles.
*/
public enum Language {
+ APPLY_FILTER,
ARRAY,
ARRAY_CHOOSE_INDEX,
@@ -92,6 +93,7 @@ public enum Language {
ENTER_OWNER_PASSWORD,
ERROR,
+ ERROR_APPLYING_FILTER,
ERROR_BUILDING_CONTENT_STREAM,
ERROR_CANNOT_CHECK_NULL_FOR_INPUT_STREAM,
ERROR_CANNOT_FIND_FILE,
@@ -184,6 +186,7 @@ public enum Language {
MENU_BAR_VERSION,
MESSAGE_ABOUT,
+ NONE,
NO_SELECTED_FILE,
NULL_AS_TEXT,
@@ -203,6 +206,7 @@ public enum Language {
PLAINTEXT_DESCRIPTION,
PREFERENCES,
PREFERENCES_ALLOW_DUPLICATE_FILES,
+ PREFERENCES_DEFAULT_STREAM_FILTER,
PREFERENCES_NEED_RESTART,
PREFERENCES_OPEN_FOLDER,
PREFERENCES_RESET_TO_DEFAULTS,
@@ -212,6 +216,7 @@ public enum Language {
PREFERENCES_VISUAL_SETTINGS,
RAW_BYTES,
+ REMOVE_ALL_FILTERS,
SAVE,
SAVE_IMAGE,
diff --git a/src/main/java/com/itextpdf/rups/view/PreferencesWindow.java b/src/main/java/com/itextpdf/rups/view/PreferencesWindow.java
index b99f7534..3a1ea51b 100644
--- a/src/main/java/com/itextpdf/rups/view/PreferencesWindow.java
+++ b/src/main/java/com/itextpdf/rups/view/PreferencesWindow.java
@@ -42,8 +42,10 @@ This file is part of the iText (R) project.
*/
package com.itextpdf.rups.view;
+import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.rups.RupsConfiguration;
import com.itextpdf.rups.conf.LookAndFeelId;
+import com.itextpdf.rups.conf.StreamFilterId;
import com.itextpdf.rups.view.icons.FrameIconUtil;
import java.awt.BorderLayout;
@@ -85,6 +87,7 @@ public final class PreferencesWindow {
// Fields to reset
private JCheckBox openDuplicateFiles;
+ private JComboBox defaultFilter;
private JTextField pathField;
private JLabel restartLabel;
private JComboBox localeBox;
@@ -167,6 +170,21 @@ private void createGeneralSettingsTab() {
JLabel openDuplicateFilesLabel = new JLabel(Language.PREFERENCES_ALLOW_DUPLICATE_FILES.getString());
openDuplicateFilesLabel.setLabelFor(this.openDuplicateFiles);
+ this.defaultFilter = new JComboBox<>();
+ this.defaultFilter.addItem(new StreamFilterId(null));
+ this.defaultFilter.addItem(new StreamFilterId(PdfName.BrotliDecode));
+ this.defaultFilter.addItem(new StreamFilterId(PdfName.FlateDecode));
+ this.defaultFilter.setSelectedItem(new StreamFilterId(RupsConfiguration.INSTANCE.getDefaultFilter()));
+ this.defaultFilter.addItemListener((ItemEvent e) -> {
+ if (e.getStateChange() == ItemEvent.SELECTED) {
+ RupsConfiguration.INSTANCE.setDefaultFilter(((StreamFilterId) e.getItem()).getValue());
+ }
+ });
+ final JLabel defaultFilterLabel = new JLabel(
+ Language.PREFERENCES_DEFAULT_STREAM_FILTER.getString()
+ );
+ defaultFilterLabel.setLabelFor(this.defaultFilter);
+
JPanel generalSettingsPanel = new JPanel();
generalSettingsPanel.setLayout(this.gridBagLayout);
@@ -176,6 +194,9 @@ private void createGeneralSettingsTab() {
generalSettingsPanel.add(openDuplicateFilesLabel, this.left);
generalSettingsPanel.add(this.openDuplicateFiles, this.right);
+ generalSettingsPanel.add(defaultFilterLabel, this.left);
+ generalSettingsPanel.add(this.defaultFilter, this.right);
+
this.generalSettingsScrollPane = new JScrollPane(generalSettingsPanel);
}
@@ -270,6 +291,7 @@ private void completeJDialogCreation() {
private void resetView() {
this.pathField.setText(RupsConfiguration.INSTANCE.getHomeFolder().getPath());
this.openDuplicateFiles.setSelected(RupsConfiguration.INSTANCE.canOpenDuplicateFiles());
+ this.defaultFilter.setSelectedItem(new StreamFilterId(RupsConfiguration.INSTANCE.getDefaultFilter()));
this.lookAndFeelBox.setSelectedItem(RupsConfiguration.INSTANCE.getLookAndFeel());
this.localeBox.setSelectedItem(RupsConfiguration.INSTANCE.getUserLocale().toLanguageTag());
this.restartLabel.setText(" ");
diff --git a/src/main/java/com/itextpdf/rups/view/contextmenu/AbstractPdfStreamAction.java b/src/main/java/com/itextpdf/rups/view/contextmenu/AbstractPdfStreamAction.java
new file mode 100644
index 00000000..7b32cfe7
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/view/contextmenu/AbstractPdfStreamAction.java
@@ -0,0 +1,88 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.view.contextmenu;
+
+import com.itextpdf.rups.controller.PdfReaderController;
+import com.itextpdf.rups.view.itext.PdfTree;
+import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode;
+
+import javax.swing.tree.TreePath;
+
+public abstract class AbstractPdfStreamAction extends AbstractRupsAction {
+ protected final transient PdfReaderController controller;
+
+ protected AbstractPdfStreamAction(String name, PdfTree invoker, PdfReaderController controller) {
+ super(name, invoker);
+ this.controller = controller;
+ }
+
+ protected PdfObjectTreeNode getTargetPdfStreamNode() {
+ final PdfTree tree = (PdfTree) invoker;
+ final Object node = tree.getLastSelectedPathComponent();
+ if (!(node instanceof PdfObjectTreeNode)) {
+ return null;
+ }
+ final PdfObjectTreeNode objectNode = (PdfObjectTreeNode) node;
+ if (!objectNode.isPdfStreamNode()) {
+ return null;
+ }
+ return objectNode;
+ }
+
+ protected void forceTreeRebuild(PdfObjectTreeNode root) {
+ // We need to delete all children from the tree node to force them to
+ // be regenerated after the update. Presumably there should be a
+ // better way to do this, but this works fine for now
+ if (controller != null) {
+ final TreePath path = new TreePath(root.getPath());
+ boolean wasExpanded = controller.getPdfTree().isExpanded(path);
+ controller.deleteAllTreeChildren(root);
+ controller.selectNode(root);
+ if (wasExpanded) {
+ controller.getPdfTree().expandPath(path);
+ } else {
+ controller.getPdfTree().collapsePath(path);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/view/contextmenu/ApplyFilterAction.java b/src/main/java/com/itextpdf/rups/view/contextmenu/ApplyFilterAction.java
new file mode 100644
index 00000000..3577317e
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/view/contextmenu/ApplyFilterAction.java
@@ -0,0 +1,89 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.view.contextmenu;
+
+import com.itextpdf.kernel.pdf.IStreamCompressionStrategy;
+import com.itextpdf.kernel.pdf.PdfStream;
+import com.itextpdf.rups.Rups;
+import com.itextpdf.rups.controller.PdfReaderController;
+import com.itextpdf.rups.model.LoggerHelper;
+import com.itextpdf.rups.util.PdfStreamUtil;
+import com.itextpdf.rups.view.Language;
+import com.itextpdf.rups.view.itext.PdfTree;
+import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode;
+
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+public class ApplyFilterAction extends AbstractPdfStreamAction {
+ private final transient Supplier encodingStrategySupplier;
+
+ public ApplyFilterAction(
+ String name,
+ PdfTree invoker,
+ PdfReaderController controller,
+ Supplier encodingStrategySupplier
+ ) {
+ super(name, invoker, controller);
+ this.encodingStrategySupplier = Objects.requireNonNull(encodingStrategySupplier);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ final PdfObjectTreeNode target = getTargetPdfStreamNode();
+ if (target == null) {
+ return;
+ }
+ try {
+ PdfStreamUtil.applyFilter((PdfStream) target.getPdfObject(), encodingStrategySupplier.get());
+ } catch (IOException | RuntimeException ex) {
+ final String errorMessage = Language.ERROR_APPLYING_FILTER.getString();
+ LoggerHelper.error(errorMessage, ex, getClass());
+ Rups.showBriefMessage(errorMessage);
+ return;
+ }
+ forceTreeRebuild(target);
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/view/contextmenu/IPdfContextMenuTarget.java b/src/main/java/com/itextpdf/rups/view/contextmenu/IPdfContextMenuTarget.java
index fd6fc39e..64392721 100644
--- a/src/main/java/com/itextpdf/rups/view/contextmenu/IPdfContextMenuTarget.java
+++ b/src/main/java/com/itextpdf/rups/view/contextmenu/IPdfContextMenuTarget.java
@@ -46,6 +46,13 @@ This file is part of the iText (R) project.
* Interface for tree nodes, which can spawn {@link com.itextpdf.rups.view.contextmenu.PdfTreeContextMenu}.
*/
public interface IPdfContextMenuTarget {
+ /**
+ * Returns true, if the tree node is a PDF stream node.
+ *
+ * @return true, if the tree node is a PDF stream node.
+ */
+ boolean isPdfStreamNode();
+
/**
* Returns true, if the tree node supports the "Inspect Object" operation.
*
diff --git a/src/main/java/com/itextpdf/rups/view/contextmenu/PdfTreeContextMenu.java b/src/main/java/com/itextpdf/rups/view/contextmenu/PdfTreeContextMenu.java
index d94f850f..5c40c490 100644
--- a/src/main/java/com/itextpdf/rups/view/contextmenu/PdfTreeContextMenu.java
+++ b/src/main/java/com/itextpdf/rups/view/contextmenu/PdfTreeContextMenu.java
@@ -42,51 +42,123 @@ This file is part of the iText (R) project.
*/
package com.itextpdf.rups.view.contextmenu;
+import com.itextpdf.brotlicompressor.BrotliStreamCompressionStrategy;
+import com.itextpdf.kernel.pdf.FlateCompressionStrategy;
+import com.itextpdf.kernel.pdf.PdfName;
+import com.itextpdf.rups.controller.PdfReaderController;
+import com.itextpdf.rups.io.encoders.ASCII85CompressionStrategy;
+import com.itextpdf.rups.io.encoders.ASCIIHexCompressionStrategy;
+import com.itextpdf.rups.io.encoders.RunLengthCompressionStrategy;
+import com.itextpdf.rups.util.ExcludeFromGeneratedJacocoReport;
import com.itextpdf.rups.view.Language;
+import com.itextpdf.rups.view.itext.PdfTree;
import javax.swing.Action;
+import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
-import java.awt.Component;
+import javax.swing.JSeparator;
/**
* Convenience class for the popup menu for the PdfTree panel.
*
* @author Michael Demey
*/
+// Excluding from coverage, as this is just logic for creating a UI pop-up menu
+@ExcludeFromGeneratedJacocoReport
public final class PdfTreeContextMenu extends JPopupMenu {
- private final InspectObjectAction inspectObjectAction;
- private final SaveToFilePdfTreeAction saveRawBytesToFileAction;
- private final SaveToFilePdfTreeAction saveToFileAction;
+ private final PdfTree parentTree;
- public PdfTreeContextMenu(Component component) {
- inspectObjectAction = new InspectObjectAction(
+ private final JMenuItem inspectObjectMenu;
+ private final JMenuItem saveRawBytesToFileMenu;
+ private final JMenuItem saveToFileMenu;
+ private final JSeparator filterSectionSeparator;
+ private final JMenu applyFilterSubMenu;
+ private final JMenuItem removeAllFiltersMenu;
+
+ public PdfTreeContextMenu(PdfTree parentTree, PdfReaderController controller) {
+ this.parentTree = parentTree;
+
+ inspectObjectMenu = createJMenuItem(new InspectObjectAction(
Language.INSPECT_OBJECT.getString(),
- component
- );
- saveRawBytesToFileAction = new SaveToFilePdfTreeAction(
+ parentTree
+ ));
+ saveRawBytesToFileMenu = createJMenuItem(new SaveToFilePdfTreeAction(
Language.SAVE_RAW_BYTES_TO_FILE.getString(),
- component,
+ parentTree,
true
- );
- saveToFileAction = new SaveToFilePdfTreeAction(
+ ));
+ saveToFileMenu = createJMenuItem(new SaveToFilePdfTreeAction(
Language.SAVE_TO_FILE.getString(),
- component,
+ parentTree,
false
- );
+ ));
+ removeAllFiltersMenu = createJMenuItem(new RemoveAllFiltersAction(
+ Language.REMOVE_ALL_FILTERS.getString(),
+ parentTree,
+ controller
+ ));
+ final JMenuItem applyAscii85DecodeMenu = createJMenuItem(new ApplyFilterAction(
+ PdfName.ASCII85Decode.getValue(),
+ parentTree,
+ controller,
+ ASCII85CompressionStrategy::new
+ ));
+ final JMenuItem applyAsciiHexDecodeMenu = createJMenuItem(new ApplyFilterAction(
+ PdfName.ASCIIHexDecode.getValue(),
+ parentTree,
+ controller,
+ ASCIIHexCompressionStrategy::new
+ ));
+ final JMenuItem applyBrotliDecodeMenu = createJMenuItem(new ApplyFilterAction(
+ PdfName.BrotliDecode.getValue(),
+ parentTree,
+ controller,
+ BrotliStreamCompressionStrategy::new
+ ));
+ final JMenuItem applyFlateDecodeMenu = createJMenuItem(new ApplyFilterAction(
+ PdfName.FlateDecode.getValue(),
+ parentTree,
+ controller,
+ FlateCompressionStrategy::new
+ ));
+ final JMenuItem applyRunLengthDecodeMenu = createJMenuItem(new ApplyFilterAction(
+ PdfName.RunLengthDecode.getValue(),
+ parentTree,
+ controller,
+ RunLengthCompressionStrategy::new
+ ));
+
+ filterSectionSeparator = new JPopupMenu.Separator();
+
+ applyFilterSubMenu = new JMenu(Language.APPLY_FILTER.getString());
+ applyFilterSubMenu.add(applyAscii85DecodeMenu);
+ applyFilterSubMenu.add(applyAsciiHexDecodeMenu);
+ applyFilterSubMenu.add(applyBrotliDecodeMenu);
+ applyFilterSubMenu.add(applyFlateDecodeMenu);
+ applyFilterSubMenu.add(applyRunLengthDecodeMenu);
- add(getJMenuItem(inspectObjectAction));
- add(getJMenuItem(saveRawBytesToFileAction));
- add(getJMenuItem(saveToFileAction));
+ add(inspectObjectMenu);
+ add(saveRawBytesToFileMenu);
+ add(saveToFileMenu);
+ add(filterSectionSeparator);
+ add(applyFilterSubMenu);
+ add(removeAllFiltersMenu);
}
- public void setEnabledForNode(IPdfContextMenuTarget node) {
- inspectObjectAction.setEnabled(node.supportsInspectObject());
- saveRawBytesToFileAction.setEnabled(node.supportsSave());
- saveToFileAction.setEnabled(node.supportsSave());
+ public void prepareForNode(IPdfContextMenuTarget node) {
+ inspectObjectMenu.setEnabled(node.supportsInspectObject());
+ saveRawBytesToFileMenu.setEnabled(node.supportsSave());
+ saveToFileMenu.setEnabled(node.supportsSave());
+ filterSectionSeparator.setVisible(node.isPdfStreamNode());
+ filterSectionSeparator.setEnabled(parentTree.isMutable());
+ applyFilterSubMenu.setVisible(node.isPdfStreamNode());
+ applyFilterSubMenu.setEnabled(parentTree.isMutable());
+ removeAllFiltersMenu.setVisible(node.isPdfStreamNode());
+ removeAllFiltersMenu.setEnabled(parentTree.isMutable());
}
- private static JMenuItem getJMenuItem(AbstractRupsAction rupsAction) {
+ private static JMenuItem createJMenuItem(AbstractRupsAction rupsAction) {
final JMenuItem jMenuItem = new JMenuItem();
jMenuItem.setText((String) rupsAction.getValue(Action.NAME));
jMenuItem.setAction(rupsAction);
diff --git a/src/main/java/com/itextpdf/rups/view/contextmenu/RemoveAllFiltersAction.java b/src/main/java/com/itextpdf/rups/view/contextmenu/RemoveAllFiltersAction.java
new file mode 100644
index 00000000..d82e4d57
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/view/contextmenu/RemoveAllFiltersAction.java
@@ -0,0 +1,67 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.view.contextmenu;
+
+import com.itextpdf.kernel.pdf.PdfStream;
+import com.itextpdf.rups.controller.PdfReaderController;
+import com.itextpdf.rups.util.PdfStreamUtil;
+import com.itextpdf.rups.view.itext.PdfTree;
+import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode;
+
+import java.awt.event.ActionEvent;
+
+public class RemoveAllFiltersAction extends AbstractPdfStreamAction {
+ public RemoveAllFiltersAction(String name, PdfTree invoker, PdfReaderController controller) {
+ super(name, invoker, controller);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ final PdfObjectTreeNode target = getTargetPdfStreamNode();
+ if (target == null) {
+ return;
+ }
+ PdfStreamUtil.removeAllFilters((PdfStream) target.getPdfObject());
+ forceTreeRebuild(target);
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/view/itext/PdfTree.java b/src/main/java/com/itextpdf/rups/view/itext/PdfTree.java
index e7d72290..a0dc41c8 100644
--- a/src/main/java/com/itextpdf/rups/view/itext/PdfTree.java
+++ b/src/main/java/com/itextpdf/rups/view/itext/PdfTree.java
@@ -65,6 +65,11 @@ public final class PdfTree extends JTree implements IRupsEventListener {
*/
private PdfTrailerTreeNode root;
+ /**
+ * Whether the backing PDF file is mutable or not.
+ */
+ private boolean isMutable = false;
+
/**
* Constructs a PDF tree.
*/
@@ -87,6 +92,15 @@ public PdfTrailerTreeNode getRoot() {
return root;
}
+ /**
+ * Returns whether the backing PDF file is mutable or not.
+ *
+ * @return whether the backing PDF file is mutable or not.
+ */
+ public boolean isMutable() {
+ return isMutable;
+ }
+
/**
* Select a specific node in the tree.
* Typically this method will be called from a different tree,
@@ -105,6 +119,7 @@ public void selectNode(DefaultMutableTreeNode node) {
@Override
public void handleCloseDocument() {
reset();
+ isMutable = false;
}
@Override
@@ -113,6 +128,7 @@ public void handleOpenDocument(ObjectLoader loader) {
root.setUserObject(String.format(Language.PDF_OBJECT_TREE.getString(), loader.getLoaderName()));
loader.getNodes().expandNode(root);
setModel(new DefaultTreeModel(root));
+ isMutable = loader.getFile().isOpenedAsOwner();
}
private void reset() {
diff --git a/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java b/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java
index 88ba39d0..b805e6e3 100644
--- a/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java
+++ b/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java
@@ -43,14 +43,16 @@ This file is part of the iText (R) project.
package com.itextpdf.rups.view.itext;
import com.itextpdf.kernel.exceptions.PdfException;
-import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.xobject.PdfImageXObject;
+import com.itextpdf.rups.Rups;
+import com.itextpdf.rups.RupsConfiguration;
import com.itextpdf.rups.controller.PdfReaderController;
import com.itextpdf.rups.model.LoggerHelper;
import com.itextpdf.rups.model.ObjectLoader;
import com.itextpdf.rups.model.IRupsEventListener;
+import com.itextpdf.rups.util.PdfStreamUtil;
import com.itextpdf.rups.view.Language;
import com.itextpdf.rups.view.contextmenu.ContextMenuMouseListener;
import com.itextpdf.rups.view.contextmenu.SaveImageAction;
@@ -80,7 +82,6 @@ This file is part of the iText (R) project.
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
-import javax.swing.tree.TreeNode;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
@@ -209,30 +210,10 @@ public void saveToTarget() {
/*
* FIXME: With indirect objects with multiple references, this will
* change the tree only in one of them.
- * FIXME: This doesn't change Length...
*/
+ final PdfStream targetStream = (PdfStream) target.getPdfObject();
manager.discardAllEdits();
manager.setLimit(0);
- if (controller != null && ((PdfDictionary) target.getPdfObject()).containsKey(PdfName.Filter)) {
- controller.deleteTreeNodeDictChild(target, PdfName.Filter);
- }
- /*
- * In the current state, stream node could contain ASN1. data, which
- * is parsed and added as tree nodes. After editing, it won't be valid,
- * so we must remove them.
- */
- if (controller != null) {
- int i = 0;
- while (i < target.getChildCount()) {
- final TreeNode child = target.getChildAt(i);
- if (child instanceof PdfObjectTreeNode) {
- ++i;
- } else {
- controller.deleteTreeChild(target, i);
- // Will assume it being just a shift...
- }
- }
- }
final int sizeEst = text.getText().length();
final ByteArrayOutputStream baos = new ByteArrayOutputStream(sizeEst);
try {
@@ -240,8 +221,22 @@ public void saveToTarget() {
} catch (IOException e) {
LoggerHelper.error(Language.ERROR_UNEXPECTED_EXCEPTION.getString(), e, getClass());
}
- ((PdfStream) target.getPdfObject()).setData(baos.toByteArray());
+ try {
+ PdfStreamUtil.setDataWithFilter(
+ targetStream,
+ baos.toByteArray(),
+ RupsConfiguration.INSTANCE.getDefaultFilterStrategy()
+ );
+ } catch (IOException e) {
+ final String errorMessage = Language.ERROR_APPLYING_FILTER.getString();
+ LoggerHelper.error(errorMessage, e, getClass());
+ Rups.showBriefMessage(errorMessage);
+ }
if (controller != null) {
+ // We need to delete all children from the tree node to force them to
+ // be regenerated after the update. Presumably there should be a
+ // better way to do this, but this works fine for now
+ controller.deleteAllTreeChildren(target);
controller.selectNode(target);
}
manager.setLimit(MAX_NUMBER_OF_EDITS);
diff --git a/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java b/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java
index 82fb1a95..b78aa249 100644
--- a/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java
+++ b/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java
@@ -442,6 +442,14 @@ public PdfName getPdfDictionaryType() {
return null;
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isPdfStreamNode() {
+ return object.isStream();
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/src/main/java/com/itextpdf/rups/view/itext/treenodes/asn1/AbstractAsn1TreeNode.java b/src/main/java/com/itextpdf/rups/view/itext/treenodes/asn1/AbstractAsn1TreeNode.java
index df279ff4..913b5cb3 100644
--- a/src/main/java/com/itextpdf/rups/view/itext/treenodes/asn1/AbstractAsn1TreeNode.java
+++ b/src/main/java/com/itextpdf/rups/view/itext/treenodes/asn1/AbstractAsn1TreeNode.java
@@ -207,6 +207,14 @@ public String toString() {
return sb.toString();
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isPdfStreamNode() {
+ return false;
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/src/main/resources/bundles/rups-lang.properties b/src/main/resources/bundles/rups-lang.properties
index ca1cf20f..7e4da7cb 100644
--- a/src/main/resources/bundles/rups-lang.properties
+++ b/src/main/resources/bundles/rups-lang.properties
@@ -1,3 +1,5 @@
+APPLY_FILTER=Apply Filter
+
ARRAY=Array
ARRAY_CHOOSE_INDEX=Choose array index
@@ -39,9 +41,10 @@ DUPLICATE_FILES_OFF=Opening duplicate files has been turned off. Please turn thi
EDITOR_CONSOLE=Console
EDITOR_CONSOLE_TOOLTIP=Console window (System.out/System.err)
ENTER_ANY_PASSWORD=Enter the password to open the document
-ENTER_OWNER_PASSWORD=Enter the Owner password to open the document
+ENTER_OWNER_PASSWORD=Enter the Owner password of this PDF file
ERROR=Error
+ERROR_APPLYING_FILTER=Unable to apply filter to stream.
ERROR_BUILDING_CONTENT_STREAM=Error building content stream representation.
ERROR_CANNOT_CHECK_NULL_FOR_INPUT_STREAM=Cannot check for null inputStream from PdfStream.
ERROR_CANNOT_FIND_FILE=Can't find file: %s
@@ -71,7 +74,7 @@ ERROR_NO_OPEN_DOCUMENT=There is no open document.
ERROR_NO_OPEN_DOCUMENT_COMPARE=There is no open document. Nothing to compare with.
ERROR_ONLY_OPEN_ONE_FILE=You can only open one file!
ERROR_OPENING_FILE=Error opening file: %s
-ERROR_PARENT_NULL=Parent node is null for
+ERROR_PARENT_NULL=Parent node is null.
ERROR_PARSING_IMAGE=Error while parsing Image.
ERROR_PARSING_PDF_OBJECT=Error while parsing PDF syntax.
ERROR_PARSING_PDF_STREAM=Error while parsing PdfStream.
@@ -145,6 +148,7 @@ MENU_BAR_VERSION=Version
MESSAGE_ABOUT=RUPS is a tool by iText Group NV.\nIt uses iText, a Free Java-PDF Library.\nVisit http://www.itextpdf.com/ for more info.
+NONE=None
NO_SELECTED_FILE=No file selected.
NULL_AS_TEXT=null
@@ -170,6 +174,7 @@ PLAINTEXT_DESCRIPTION=Plain text representation of the PDF
PREFERENCES=Preferences
PREFERENCES_ALLOW_DUPLICATE_FILES=Allow duplicate files in viewer
+PREFERENCES_DEFAULT_STREAM_FILTER=Default stream filter
PREFERENCES_NEED_RESTART=RUPS needs to be restarted when changing this value.
PREFERENCES_OPEN_FOLDER=Default Open File Folder
PREFERENCES_RESET_TO_DEFAULTS=Reset to Defaults
@@ -179,6 +184,7 @@ PREFERENCES_SELECT_NEW_DEFAULT_FOLDER=Select new default folder
PREFERENCES_VISUAL_SETTINGS=Visual Settings
RAW_BYTES= raw bytes
+REMOVE_ALL_FILTERS=Remove All Filters
SAVE=Save
SAVE_IMAGE=Save Image
diff --git a/src/main/resources/bundles/rups-lang_en_US.properties b/src/main/resources/bundles/rups-lang_en_US.properties
index 4e2b051f..7e4da7cb 100644
--- a/src/main/resources/bundles/rups-lang_en_US.properties
+++ b/src/main/resources/bundles/rups-lang_en_US.properties
@@ -1,3 +1,5 @@
+APPLY_FILTER=Apply Filter
+
ARRAY=Array
ARRAY_CHOOSE_INDEX=Choose array index
@@ -8,9 +10,11 @@ CLEAR=Clear
COMPARE_EQUAL=Documents are equal
COMPARE_WITH=Compare with...
+CONSOLE=Console
CONSOLE_BACKUP=Backup
CONSOLE_ERROR=Error
CONSOLE_INFO=Info
+CONSOLE_TOOL_TIP=Console window (System.out/System.err)
COPY=Copy
COPY_TO_CLIPBOARD=Copy to Clipboard
@@ -32,12 +36,15 @@ DICTIONARY_KEY=Key
DICTIONARY_OF_TYPE=Dictionary of type: %s
DICTIONARY_VALUE=Value
+DUPLICATE_FILES_OFF=Opening duplicate files has been turned off. Please turn this on in the Preferences to enable duplicate files.
+
EDITOR_CONSOLE=Console
EDITOR_CONSOLE_TOOLTIP=Console window (System.out/System.err)
ENTER_ANY_PASSWORD=Enter the password to open the document
ENTER_OWNER_PASSWORD=Enter the Owner password of this PDF file
ERROR=Error
+ERROR_APPLYING_FILTER=Unable to apply filter to stream.
ERROR_BUILDING_CONTENT_STREAM=Error building content stream representation.
ERROR_CANNOT_CHECK_NULL_FOR_INPUT_STREAM=Cannot check for null inputStream from PdfStream.
ERROR_CANNOT_FIND_FILE=Can't find file: %s
@@ -45,18 +52,23 @@ ERROR_CLOSING_STREAM=Can't close stream.
ERROR_COMPARE_DOCUMENT_CREATION=Can't open document for comparison
ERROR_COMPARED_DOCUMENT_CLOSED=Compared document is closed.
ERROR_COMPARED_DOCUMENT_NULL=Compared document is null.
+ERROR_DRAG_AND_DROP=Error while opening through drag and drop: %s
ERROR_DUPLICATE_KEY=This key already exist in dictionary. Please edit existing entry.
ERROR_EDITING_UNSPECIFIED_DOCUMENT=Trying to edit references when no document was specified.
ERROR_EMPTY_FIELD=Don't leave fields empty.
+ERROR_FILE_COULD_NOT_BE_VIEWED=File couldn't be opened using the system viewer.
ERROR_ILLEGAL_CHUNK= - the chunk of this type not allowed here.
ERROR_INCORRECT_ARRAY_BRACKETS=Incorrect sequence of array brackets.
ERROR_INCORRECT_DICTIONARY_BRACKETS=Incorrect sequence of dictionary brackets.
ERROR_INDEX_NOT_IN_RANGE=The typed index is not in range.
ERROR_INDEX_NOT_INTEGER=The typed index isn't integer.
+ERROR_INITIALIZING_SETTINGS=Error initializing settings.
ERROR_KEY_IS_NOT_NAME=Key value isn't value Name object.
+ERROR_LOADING_DEFAULT_SETTINGS=Error loading default settings.
ERROR_LOADING_IMAGE=Image can't be loaded.
ERROR_LOADING_MAVEN_SETTINGS=Failed to load Maven settings.
ERROR_LOADING_XFA=Can't load XFA.
+ERROR_LOOK_AND_FEEL=Error setting the look and feel.
ERROR_MISSING_PASSWORD=The required password for this document was not provided.
ERROR_NO_OPEN_DOCUMENT=There is no open document.
ERROR_NO_OPEN_DOCUMENT_COMPARE=There is no open document. Nothing to compare with.
@@ -114,6 +126,7 @@ LAF_FLATLAFMACOSLIGHT=FlatLaf macOS Light
LAF_FLATLAFMACOSDARK=FlatLaf macOS Dark
LOADING=Loading...
+LOCALE=Locale
LOG_TREE_NODE_CREATED=Tree node was successfully created for new indirect object
LOOK_AND_FEEL=Theme
@@ -135,6 +148,7 @@ MENU_BAR_VERSION=Version
MESSAGE_ABOUT=RUPS is a tool by iText Group NV.\nIt uses iText, a Free Java-PDF Library.\nVisit http://www.itextpdf.com/ for more info.
+NONE=None
NO_SELECTED_FILE=No file selected.
NULL_AS_TEXT=null
@@ -158,7 +172,19 @@ PDF_OBJECT_TREE=PDF Object Tree (%s)
PLAINTEXT=Plain Text
PLAINTEXT_DESCRIPTION=Plain text representation of the PDF
+PREFERENCES=Preferences
+PREFERENCES_ALLOW_DUPLICATE_FILES=Allow duplicate files in viewer
+PREFERENCES_DEFAULT_STREAM_FILTER=Default stream filter
+PREFERENCES_NEED_RESTART=RUPS needs to be restarted when changing this value.
+PREFERENCES_OPEN_FOLDER=Default Open File Folder
+PREFERENCES_RESET_TO_DEFAULTS=Reset to Defaults
+PREFERENCES_RESET_TO_DEFAULTS_CONFIRM=Do you want to reset all settings?
+PREFERENCES_RUPS_SETTINGS=General Settings
+PREFERENCES_SELECT_NEW_DEFAULT_FOLDER=Select new default folder
+PREFERENCES_VISUAL_SETTINGS=Visual Settings
+
RAW_BYTES= raw bytes
+REMOVE_ALL_FILTERS=Remove All Filters
SAVE=Save
SAVE_IMAGE=Save Image
@@ -167,6 +193,7 @@ SAVE_RAW_BYTES_TO_FILE=Save Raw Bytes to File
SAVE_SUCCESS=File Saved
SAVE_TO_FILE=Save to File
SAVE_TO_STREAM=Save to Stream
+SAVE_UNSAVED_CHANGES=You have unchanged changes! Are you sure you want to discard them?
SELECT_ALL=Select All
diff --git a/src/main/resources/config/default.properties b/src/main/resources/config/default.properties
index f2aabef7..2beddb87 100644
--- a/src/main/resources/config/default.properties
+++ b/src/main/resources/config/default.properties
@@ -1,4 +1,5 @@
rups.duplicatefiles=false
+rups.defaultfilter=FlateDecode
ui.closeoperation=exit
ui.lookandfeel=flatlaflight
diff --git a/src/test/java/com/itextpdf/rups/RupsConfigurationTest.java b/src/test/java/com/itextpdf/rups/RupsConfigurationTest.java
index b7f249c7..b82b205a 100644
--- a/src/test/java/com/itextpdf/rups/RupsConfigurationTest.java
+++ b/src/test/java/com/itextpdf/rups/RupsConfigurationTest.java
@@ -42,6 +42,10 @@ This file is part of the iText (R) project.
*/
package com.itextpdf.rups;
+import com.itextpdf.brotlicompressor.BrotliStreamCompressionStrategy;
+import com.itextpdf.kernel.pdf.FlateCompressionStrategy;
+import com.itextpdf.kernel.pdf.PdfName;
+
import java.util.Set;
import org.junit.jupiter.api.*;
@@ -134,6 +138,35 @@ public void closingOperationsPossibleValuesTest() {
Assertions.assertTrue(VALID_CLOSE_OPERATION_VALUES.contains(closeOperation));
}
+ @Test
+ void setDefaultFilterTest() {
+ // Default is /FlateDecode
+ Assertions.assertSame(
+ PdfName.FlateDecode,
+ RupsConfiguration.INSTANCE.getDefaultFilter()
+ );
+ Assertions.assertInstanceOf(
+ FlateCompressionStrategy.class,
+ RupsConfiguration.INSTANCE.getDefaultFilterStrategy()
+ );
+ // Changing to /BrotliDecode
+ RupsConfiguration.INSTANCE.setDefaultFilter(PdfName.BrotliDecode);
+ RupsConfiguration.INSTANCE.saveConfiguration();
+ Assertions.assertSame(
+ PdfName.BrotliDecode,
+ RupsConfiguration.INSTANCE.getDefaultFilter()
+ );
+ Assertions.assertInstanceOf(
+ BrotliStreamCompressionStrategy.class,
+ RupsConfiguration.INSTANCE.getDefaultFilterStrategy()
+ );
+ // Changing to an unsupported value, should return null
+ RupsConfiguration.INSTANCE.setDefaultFilter(PdfName.ASCIIHexDecode);
+ RupsConfiguration.INSTANCE.saveConfiguration();
+ Assertions.assertNull(RupsConfiguration.INSTANCE.getDefaultFilter());
+ Assertions.assertNull(RupsConfiguration.INSTANCE.getDefaultFilterStrategy());
+ }
+
@AfterAll
public static void afterClass() {
RupsConfiguration.INSTANCE.restore(copy);
diff --git a/src/test/java/com/itextpdf/rups/io/encoders/ASCII85OutputStreamTest.java b/src/test/java/com/itextpdf/rups/io/encoders/ASCII85OutputStreamTest.java
new file mode 100644
index 00000000..b9c223be
--- /dev/null
+++ b/src/test/java/com/itextpdf/rups/io/encoders/ASCII85OutputStreamTest.java
@@ -0,0 +1,176 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2026 Apryse Group NV
+ Authors: Apryse Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ 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 http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.io.encoders;
+
+import com.itextpdf.rups.io.util.CloseableByteArrayOutputStream;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import org.junit.jupiter.api.Assertions;
+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;
+
+@Tag("UnitTest")
+public class ASCII85OutputStreamTest {
+ public static Iterable