diff --git a/pom.xml b/pom.xml index ee1f4382..08b482d6 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,7 @@ 2.2.0 3.7 76.1 - 9.4.0 + 9.5.0-SNAPSHOT 2.20.1 1.5.22 @@ -117,6 +117,11 @@ bouncy-castle-adapter ${itext.version} + + com.itextpdf + brotli-compressor + ${itext.version} + org.dom4j dom4j 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 encodeTestArguments() { + // These inputs were generated randomly just in case + return Arrays.asList( + new Object[]{ + new byte[0], + "~>", + "Empty input", + }, + new Object[]{ + new byte[]{0x0C}, + "$i~>", + "Single 1-byte block", + }, + new Object[]{ + new byte[]{0x39, 0x14}, + "3>;~>", + "Single 2-byte block", + }, + new Object[]{ + new byte[]{0x21, (byte) 0xD8, 0x6C}, + "+kUT~>", + "Single 3-byte block", + }, + new Object[]{ + new byte[]{0x7F, 0x1D, (byte) 0xEA, 0x50}, + "Ii[fN~>", + "Single full block", + }, + new Object[]{ + new byte[]{0x2D, 0x1E, (byte) 0xB7, 0x7D, + 0x1F}, + "/KVBL*r~>", + "Single full block + single 1-byte block", + }, + new Object[]{ + new byte[]{0x54, (byte) 0x8E, 0x0A, (byte) 0xAD, + 0x20, 0x3F}, + "", + "Single full block + single 2-byte block", + }, + new Object[]{ + new byte[]{(byte) 0xA1, 0x72, 0x6A, 0x6C, + (byte) 0xAC, 0x56, 0x09}, + "TlOmaXB#W~>", + "Single full block + single 3-byte block", + }, + new Object[]{ + new byte[]{(byte) 0x99, 0x0D, (byte) 0xCA, 0x53, + (byte) 0xFF, (byte) 0x94, (byte) 0xC4, 0x26}, + "R17;;s-1GK~>", + "Two full blocks", + }, + // Single full block + single partial zero block variations + new Object[]{ + new byte[]{(byte) 0xA9, 0x1A, (byte) 0x9D, 0x59, + 0x00}, + "W>_=1!!~>", + "Single full block + single zeroed 1-byte block", + }, + new Object[]{ + new byte[]{(byte) 0xFC, 0x17, 0x09, (byte) 0x8A, + 0x00, 0x00}, + "r\"fZs!!!~>", + "Single full block + single zeroed 2-byte block", + }, + new Object[]{ + new byte[]{0x65, (byte) 0x87, 0x11, (byte) 0xFF, + 0x00, 0x00, 0x00}, + "AVUlt!!!!~>", + "Single full block + single zeroed 3-byte block", + }, + new Object[]{ + new byte[]{0x45, 0x35, (byte) 0xD6, (byte) 0xCE, + 0x00, 0x00, 0x00, 0x00}, + "75`ZAz~>", + "Single full block + single zeroed full block", + } + ); + } + + @ParameterizedTest(name = "{2}") + @MethodSource("encodeTestArguments") + public void encodeTest(byte[] input, String output, String name) throws IOException { + CloseableByteArrayOutputStream baos = new CloseableByteArrayOutputStream(); + try (ASCII85OutputStream encoder = new ASCII85OutputStream(baos)) { + encoder.write(input); + } + Assertions.assertEquals(output, toString(baos)); + } + + @Test + public void finishableImplTest() throws IOException { + CloseableByteArrayOutputStream baos = new CloseableByteArrayOutputStream(); + ASCII85OutputStream encoder = new ASCII85OutputStream(baos); + + encoder.write(new byte[]{(byte) 0xBF, 0x1B, 0x25, 0x03, (byte) 0x94}); + encoder.flush(); + Assertions.assertEquals("^DeI$", toString(baos)); + + // Should add encoded partial block and EOD + encoder.finish(); + Assertions.assertFalse(baos.isClosed()); + Assertions.assertEquals("^DeI$PQ~>", toString(baos)); + + // Should be noop, since idempotent + encoder.finish(); + Assertions.assertFalse(baos.isClosed()); + Assertions.assertEquals("^DeI$PQ~>", toString(baos)); + + // Should not append data, since finished + encoder.close(); + Assertions.assertTrue(baos.isClosed()); + Assertions.assertEquals("^DeI$PQ~>", toString(baos)); + } + + private static String toString(java.io.ByteArrayOutputStream baos) { + return baos.toString(StandardCharsets.US_ASCII); + } +} diff --git a/src/test/java/com/itextpdf/rups/io/encoders/ASCIIHexOutputStreamTest.java b/src/test/java/com/itextpdf/rups/io/encoders/ASCIIHexOutputStreamTest.java new file mode 100644 index 00000000..1fcb596c --- /dev/null +++ b/src/test/java/com/itextpdf/rups/io/encoders/ASCIIHexOutputStreamTest.java @@ -0,0 +1,119 @@ +/* + 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 org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("UnitTest") +public class ASCIIHexOutputStreamTest { + @Test + public void encodeTest() throws IOException { + byte[] input = new byte[256]; + for (int i = 0; i < input.length; ++i) { + input[i] = (byte) i; + } + String expected = "000102030405060708090a0b0c0d0e0f101112131415161718" + + "191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435" + + "363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152" + + "535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f" + + "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c" + + "8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9" + + "aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6" + + "c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3" + + "e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff>"; + + CloseableByteArrayOutputStream baos = new CloseableByteArrayOutputStream(); + try (ASCIIHexOutputStream encoder = new ASCIIHexOutputStream(baos)) { + encoder.write(input); + } + Assertions.assertEquals(expected, toString(baos)); + } + + @Test + public void emptyStreamTest() throws IOException { + CloseableByteArrayOutputStream baos = new CloseableByteArrayOutputStream(); + ASCIIHexOutputStream encoder = new ASCIIHexOutputStream(baos); + Assertions.assertEquals("", toString(baos)); + + encoder.finish(); + Assertions.assertEquals(">", toString(baos)); + + encoder.close(); + Assertions.assertEquals(">", toString(baos)); + } + + @Test + public void finishableImplTest() throws IOException { + CloseableByteArrayOutputStream baos = new CloseableByteArrayOutputStream(); + ASCIIHexOutputStream encoder = new ASCIIHexOutputStream(baos); + + encoder.write(new byte[]{0x1F, 0x3A, 0x7F, 0x59}); + encoder.flush(); + Assertions.assertEquals("1f3a7f59", toString(baos)); + + // Should add EOD + encoder.finish(); + Assertions.assertFalse(baos.isClosed()); + Assertions.assertEquals("1f3a7f59>", toString(baos)); + + // Should be noop, since idempotent + encoder.finish(); + Assertions.assertFalse(baos.isClosed()); + Assertions.assertEquals("1f3a7f59>", toString(baos)); + + // Should not append data, since finished + encoder.close(); + Assertions.assertTrue(baos.isClosed()); + Assertions.assertEquals("1f3a7f59>", toString(baos)); + } + + private static String toString(java.io.ByteArrayOutputStream baos) { + return baos.toString(StandardCharsets.US_ASCII); + } +} diff --git a/src/test/java/com/itextpdf/rups/io/encoders/RunLengthOutputStreamTest.java b/src/test/java/com/itextpdf/rups/io/encoders/RunLengthOutputStreamTest.java new file mode 100644 index 00000000..c9710799 --- /dev/null +++ b/src/test/java/com/itextpdf/rups/io/encoders/RunLengthOutputStreamTest.java @@ -0,0 +1,198 @@ +/* + 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.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 RunLengthOutputStreamTest { + public static Iterable encodeTestArguments() { + return Arrays.asList( + new Object[]{ + new byte[0], + new byte[]{(byte) 0x80}, + "Empty input" + }, + new Object[]{ + // We do not collapse 2 elem repeating runs + concat(uniqueRun(1), repeatingRun(2, 0x42), uniqueRun(2), + repeatingRun(3, 0x43), + uniqueRun(3), + repeatingRun(4, 0x44), + uniqueRun(4)), + concat( 4, uniqueRun(1), repeatingRun(2, 0x42), uniqueRun(2), + 254, 0x43, + 2, uniqueRun(3), + 253, 0x44, + 3, uniqueRun(4), + 0x80), + "Variable run types" + }, + new Object[]{ + uniqueRun(300, 0x00), + concat(127, uniqueRun(128, 0x00), + 127, uniqueRun(128, 0x80), + 43, uniqueRun( 44, 0x00), + 0x80), + "Long unique run" + }, + new Object[]{ + repeatingRun(300, 0xAD), + concat(129, 0xAD, 129, 0xAD, 213, 0xAD, 0x80), + "Long repeating run" + }, + new Object[]{ + concat(uniqueRun(128, 0x40), repeatingRun(128, 0x60)), + concat(127, uniqueRun(128, 0x40), 129, 0x60, 0x80), + "128 unique run + 128 repeating run" + }, + new Object[]{ + concat(repeatingRun(128, 0x40), uniqueRun(128, 0x60)), + concat(129, 0x40, 127, uniqueRun(128, 0x60), 0x80), + "128 repeating run + 128 unique run" + } + ); + } + + @ParameterizedTest(name = "{2}") + @MethodSource("encodeTestArguments") + public void encodeTest(byte[] input, byte[] output, String name) throws IOException { + CloseableByteArrayOutputStream baos = new CloseableByteArrayOutputStream(); + try (RunLengthOutputStream encoder = new RunLengthOutputStream(baos)) { + encoder.write(input); + } + Assertions.assertArrayEquals(output, baos.toByteArray()); + } + + @Test + public void finishableImplTest() throws IOException { + CloseableByteArrayOutputStream baos = new CloseableByteArrayOutputStream(); + RunLengthOutputStream encoder = new RunLengthOutputStream(baos); + + encoder.write(uniqueRun(3, 0x00)); + encoder.write(repeatingRun(3, 0x00)); + encoder.write(repeatingRun(3, 0xFF)); + encoder.write(uniqueRun(3, 0x81)); + encoder.flush(); + Assertions.assertArrayEquals( + concat(2, uniqueRun(3, 0x00), 254, 0x00, 254, 0xFF), + baos.toByteArray() + ); + + // Should add encoded pending block and EOD + encoder.finish(); + Assertions.assertFalse(baos.isClosed()); + Assertions.assertArrayEquals( + concat(2, uniqueRun(3, 0x00), 254, 0x00, 254, 0xFF, 2, uniqueRun(3, 0x81), 0x80), + baos.toByteArray() + ); + + // Should be noop, since idempotent + encoder.finish(); + Assertions.assertFalse(baos.isClosed()); + Assertions.assertArrayEquals( + concat(2, uniqueRun(3, 0x00), 254, 0x00, 254, 0xFF, 2, uniqueRun(3, 0x81), 0x80), + baos.toByteArray() + ); + + // Should not append data, since finished + encoder.close(); + Assertions.assertTrue(baos.isClosed()); + Assertions.assertArrayEquals( + concat(2, uniqueRun(3, 0x00), 254, 0x00, 254, 0xFF, 2, uniqueRun(3, 0x81), 0x80), + baos.toByteArray() + ); + } + + private static byte[] concat(Object... values) { + int size = 0; + for (int i = 0; i < values.length; ++i) { + if (values[i] instanceof Integer) { + ++size; + } else if (values[i] instanceof byte[]) { + size += ((byte[]) values[i]).length; + } else { + throw new IllegalArgumentException("unexpected type"); + } + } + byte[] result = new byte[size]; + int offset = 0; + for (int i = 0; i < values.length; ++i) { + if (values[i] instanceof Integer) { + result[offset] = (byte) (((Integer) values[i]) & 0xFF); + ++offset; + } else { + byte[] arr = (byte[]) values[i]; + System.arraycopy(arr, 0, result, offset, arr.length); + offset += arr.length; + } + } + return result; + } + + private static byte[] uniqueRun(int length) { + return uniqueRun(length, 0); + } + + private static byte[] uniqueRun(int length, int offset) { + byte[] run = new byte[length]; + for (int i = 0; i < length; ++i) { + run[i] = (byte) ((offset + i) & 0xFF); + } + return run; + } + + private static byte[] repeatingRun(int length, int value) { + byte[] run = new byte[length]; + Arrays.fill(run, (byte) (value & 0xFF)); + return run; + } +} diff --git a/src/test/java/com/itextpdf/rups/io/util/CloseableByteArrayOutputStream.java b/src/test/java/com/itextpdf/rups/io/util/CloseableByteArrayOutputStream.java new file mode 100644 index 00000000..a4dc0d21 --- /dev/null +++ b/src/test/java/com/itextpdf/rups/io/util/CloseableByteArrayOutputStream.java @@ -0,0 +1,83 @@ +/* + 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.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * A version of {@link ByteArrayOutputStream}, which cannot be written to + * after calling {@link #close()}. + */ +public class CloseableByteArrayOutputStream extends java.io.ByteArrayOutputStream { + private boolean closed = false; + + public CloseableByteArrayOutputStream() { + super(); + } + + public boolean isClosed() { + return closed; + } + + @Override + public synchronized void write(int b) { + if (closed) { + throw new RuntimeException("Stream is closed"); + } + super.write(b); + } + + @Override + public synchronized void write(byte[] b, int off, int len) { + if (closed) { + throw new RuntimeException("Stream is closed"); + } + super.write(b, off, len); + } + + @Override + public synchronized void close() throws IOException { + closed = true; + } +} diff --git a/src/test/java/com/itextpdf/rups/util/PdfStreamUtilTest.java b/src/test/java/com/itextpdf/rups/util/PdfStreamUtilTest.java new file mode 100644 index 00000000..7b993f31 --- /dev/null +++ b/src/test/java/com/itextpdf/rups/util/PdfStreamUtilTest.java @@ -0,0 +1,380 @@ +/* + 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.kernel.pdf.CompressionConstants; +import com.itextpdf.kernel.pdf.PdfArray; +import com.itextpdf.kernel.pdf.PdfDictionary; +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 com.itextpdf.rups.io.encoders.ASCII85CompressionStrategy; +import com.itextpdf.rups.io.encoders.ASCIIHexCompressionStrategy; + +import java.io.IOException; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import static java.nio.charset.StandardCharsets.US_ASCII; + +@Tag("UnitTest") +final class PdfStreamUtilTest { + private static final byte[] TEST_DATA = "ABC123".getBytes(US_ASCII); + private static final byte[] TEST_DATA_85 = "5sdpn1,A~>".getBytes(US_ASCII); + private static final byte[] TEST_DATA_HEX = "414243313233>".getBytes(US_ASCII); + private static final byte[] TEST_DATA_HEX_85 = "1bggB1c$pB1GUaB4o~>".getBytes(US_ASCII); + private static final byte[] TEST_DATA_HEX_85_HEX = "316267674231632470423147556142346f7e3e>" + .getBytes(US_ASCII); + + @Test + void applyFilter_hexOverPlain() throws IOException { + final PdfStream stream = new PdfStream(TEST_DATA); + stream.put(PdfName.Length, new PdfNumber(TEST_DATA.length)); + stream.remove(PdfName.Filter); + stream.remove(PdfName.DecodeParms); + + PdfStreamUtil.applyFilter(stream, new ASCIIHexCompressionStrategy()); + + Assertions.assertArrayEquals(TEST_DATA_HEX, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA_HEX.length), stream.get(PdfName.Length)); + Assertions.assertEquals(PdfName.ASCIIHexDecode, stream.get(PdfName.Filter)); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + } + + @Test + void applyFilter_hexOverPlainWithNulls() throws IOException { + final PdfStream stream = new PdfStream(TEST_DATA); + stream.put(PdfName.Length, new PdfNumber(TEST_DATA.length)); + stream.put(PdfName.Filter, PdfNull.PDF_NULL); + stream.put(PdfName.DecodeParms, PdfNull.PDF_NULL); + + PdfStreamUtil.applyFilter(stream, new ASCIIHexCompressionStrategy()); + + Assertions.assertArrayEquals(TEST_DATA_HEX, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA_HEX.length), stream.get(PdfName.Length)); + Assertions.assertEquals(PdfName.ASCIIHexDecode, stream.get(PdfName.Filter)); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + } + + @Test + void applyFilter_hexWithParamsOverPlain() throws IOException { + final PdfStream stream = new PdfStream(TEST_DATA); + stream.put(PdfName.Length, new PdfNumber(TEST_DATA.length)); + stream.remove(PdfName.Filter); + stream.remove(PdfName.DecodeParms); + + PdfStreamUtil.applyFilter(stream, new ASCIIHexCompressionStrategy() { + @Override + public PdfObject getDecodeParams() { + return new PdfDictionary(); + } + }); + + Assertions.assertArrayEquals(TEST_DATA_HEX, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA_HEX.length), stream.get(PdfName.Length)); + Assertions.assertEquals(PdfName.ASCIIHexDecode, stream.get(PdfName.Filter)); + Assertions.assertTrue(stream.get(PdfName.DecodeParms).isDictionary()); + } + + @Test + void applyFilter_85OverHex() throws IOException { + final PdfStream stream = new PdfStream(TEST_DATA_HEX); + stream.put(PdfName.Length, new PdfNumber(TEST_DATA_HEX.length)); + stream.put(PdfName.Filter, PdfName.ASCIIHexDecode); + stream.remove(PdfName.DecodeParms); + + PdfStreamUtil.applyFilter(stream, new ASCII85CompressionStrategy()); + + Assertions.assertArrayEquals(TEST_DATA_HEX_85, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA_HEX_85.length), stream.get(PdfName.Length)); + Assertions.assertTrue(stream.get(PdfName.Filter).isArray()); + Assertions.assertEquals( + List.of(PdfName.ASCII85Decode, PdfName.ASCIIHexDecode), + ((PdfArray) stream.get(PdfName.Filter)).toList() + ); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + } + + @Test + void applyFilter_85WithParamsOverHex() throws IOException { + final PdfStream stream = new PdfStream(TEST_DATA_HEX); + stream.put(PdfName.Length, new PdfNumber(TEST_DATA_HEX.length)); + stream.put(PdfName.Filter, PdfName.ASCIIHexDecode); + stream.remove(PdfName.DecodeParms); + + final PdfDictionary newParamsDict = new PdfDictionary(); + PdfStreamUtil.applyFilter(stream, new ASCII85CompressionStrategy() { + @Override + public PdfObject getDecodeParams() { + return newParamsDict; + } + }); + + Assertions.assertArrayEquals(TEST_DATA_HEX_85, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA_HEX_85.length), stream.get(PdfName.Length)); + Assertions.assertTrue(stream.get(PdfName.Filter).isArray()); + Assertions.assertEquals( + List.of(PdfName.ASCII85Decode, PdfName.ASCIIHexDecode), + ((PdfArray) stream.get(PdfName.Filter)).toList() + ); + Assertions.assertTrue(stream.get(PdfName.DecodeParms).isArray()); + Assertions.assertEquals( + List.of(newParamsDict, PdfNull.PDF_NULL), + ((PdfArray) stream.get(PdfName.DecodeParms)).toList() + ); + } + + @Test + void applyFilter_85OverHexWithDictParams() throws IOException { + final PdfStream stream = new PdfStream(TEST_DATA_HEX); + stream.put(PdfName.Length, new PdfNumber(TEST_DATA_HEX.length)); + stream.put(PdfName.Filter, PdfName.ASCIIHexDecode); + final PdfDictionary oldParamsDict = new PdfDictionary(); + stream.put(PdfName.DecodeParms, oldParamsDict); + + PdfStreamUtil.applyFilter(stream, new ASCII85CompressionStrategy()); + + Assertions.assertArrayEquals(TEST_DATA_HEX_85, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA_HEX_85.length), stream.get(PdfName.Length)); + Assertions.assertTrue(stream.get(PdfName.Filter).isArray()); + Assertions.assertEquals( + List.of(PdfName.ASCII85Decode, PdfName.ASCIIHexDecode), + ((PdfArray) stream.get(PdfName.Filter)).toList() + ); + Assertions.assertTrue(stream.get(PdfName.DecodeParms).isArray()); + Assertions.assertEquals( + List.of(PdfNull.PDF_NULL, oldParamsDict), + ((PdfArray) stream.get(PdfName.DecodeParms)).toList() + ); + } + + @Test + void applyFilter_85OverHexWithFilterAndParamArrays() throws IOException { + final PdfStream stream = new PdfStream(TEST_DATA_HEX); + stream.put(PdfName.Length, new PdfNumber(TEST_DATA_HEX.length)); + stream.put(PdfName.Filter, new PdfArray(PdfName.ASCIIHexDecode)); + final PdfDictionary oldParamsDict = new PdfDictionary(); + stream.put(PdfName.DecodeParms, new PdfArray(oldParamsDict)); + + PdfStreamUtil.applyFilter(stream, new ASCII85CompressionStrategy()); + + Assertions.assertArrayEquals(TEST_DATA_HEX_85, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA_HEX_85.length), stream.get(PdfName.Length)); + Assertions.assertTrue(stream.get(PdfName.Filter).isArray()); + Assertions.assertEquals( + List.of(PdfName.ASCII85Decode, PdfName.ASCIIHexDecode), + ((PdfArray) stream.get(PdfName.Filter)).toList() + ); + Assertions.assertTrue(stream.get(PdfName.DecodeParms).isArray()); + Assertions.assertEquals( + List.of(PdfNull.PDF_NULL, oldParamsDict), + ((PdfArray) stream.get(PdfName.DecodeParms)).toList() + ); + } + + @Test + void applyFilter_85WithParamsOverHexWithEmptyParamArray() throws IOException { + final PdfStream stream = new PdfStream(TEST_DATA_HEX); + stream.put(PdfName.Length, new PdfNumber(TEST_DATA_HEX.length)); + stream.put(PdfName.Filter, new PdfArray(PdfName.ASCIIHexDecode)); + stream.put(PdfName.DecodeParms, new PdfArray()); + + final PdfDictionary newParamsDict = new PdfDictionary(); + PdfStreamUtil.applyFilter(stream, new ASCII85CompressionStrategy() { + @Override + public PdfObject getDecodeParams() { + return newParamsDict; + } + }); + + Assertions.assertArrayEquals(TEST_DATA_HEX_85, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA_HEX_85.length), stream.get(PdfName.Length)); + Assertions.assertTrue(stream.get(PdfName.Filter).isArray()); + Assertions.assertEquals( + List.of(PdfName.ASCII85Decode, PdfName.ASCIIHexDecode), + ((PdfArray) stream.get(PdfName.Filter)).toList() + ); + Assertions.assertTrue(stream.get(PdfName.DecodeParms).isArray()); + Assertions.assertEquals( + List.of(newParamsDict, PdfNull.PDF_NULL), + ((PdfArray) stream.get(PdfName.DecodeParms)).toList() + ); + } + + @Test + void applyFilter_HexOver85OverHexWithInvalidParamArray() throws IOException { + final PdfStream stream = new PdfStream(TEST_DATA_HEX_85); + stream.put(PdfName.Length, new PdfNumber(TEST_DATA_HEX_85.length)); + stream.put(PdfName.Filter, new PdfArray(List.of(PdfName.ASCII85Decode, PdfName.ASCIIHexDecode))); + final PdfDictionary oldParamsDict = new PdfDictionary(); + stream.put(PdfName.DecodeParms, oldParamsDict); + + PdfStreamUtil.applyFilter(stream, new ASCIIHexCompressionStrategy()); + + Assertions.assertArrayEquals(TEST_DATA_HEX_85_HEX, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals( + new PdfNumber(TEST_DATA_HEX_85_HEX.length), + stream.get(PdfName.Length) + ); + Assertions.assertTrue(stream.get(PdfName.Filter).isArray()); + Assertions.assertEquals( + List.of(PdfName.ASCIIHexDecode, PdfName.ASCII85Decode, PdfName.ASCIIHexDecode), + ((PdfArray) stream.get(PdfName.Filter)).toList() + ); + Assertions.assertTrue(stream.get(PdfName.DecodeParms).isArray()); + Assertions.assertEquals( + List.of(PdfNull.PDF_NULL, oldParamsDict, PdfNull.PDF_NULL), + ((PdfArray) stream.get(PdfName.DecodeParms)).toList() + ); + } + + @Test + void setDataWithFilter_ascii85Strategy() throws IOException { + final PdfStream stream = createHexStream(); + + PdfStreamUtil.setDataWithFilter(stream, TEST_DATA, new ASCII85CompressionStrategy()); + + Assertions.assertArrayEquals(TEST_DATA_85, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA_85.length), stream.get(PdfName.Length)); + Assertions.assertEquals(PdfName.ASCII85Decode, stream.get(PdfName.Filter)); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + } + + @Test + void setDataWithFilter_nullStrategy() throws IOException { + final PdfStream stream = createHexStream(); + final byte[] newData = {0x01, 0x02, 0x03}; + + PdfStreamUtil.setDataWithFilter(stream, newData, null); + + Assertions.assertArrayEquals(newData, stream.getBytes(false)); + Assertions.assertArrayEquals(newData, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(newData.length), stream.get(PdfName.Length)); + Assertions.assertFalse(stream.containsKey(PdfName.Filter)); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + } + + @Test + void setDataWithFilter_withDictDecodeParams() throws IOException { + final PdfStream stream = createHexStream(); + + PdfStreamUtil.setDataWithFilter(stream, TEST_DATA, new ASCII85CompressionStrategy() { + @Override + public PdfObject getDecodeParams() { + return new PdfDictionary(); + } + }); + + Assertions.assertArrayEquals(TEST_DATA_85, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA_85.length), stream.get(PdfName.Length)); + Assertions.assertEquals(PdfName.ASCII85Decode, stream.get(PdfName.Filter)); + Assertions.assertTrue(stream.get(PdfName.DecodeParms).isDictionary()); + } + + @Test + void setDataWithFilter_withInvalidDecodeParams() throws IOException { + final PdfStream stream = createHexStream(); + + PdfStreamUtil.setDataWithFilter(stream, TEST_DATA, new ASCII85CompressionStrategy() { + @Override + public PdfObject getDecodeParams() { + return new PdfNumber(1); + } + }); + + Assertions.assertArrayEquals(TEST_DATA_85, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA_85.length), stream.get(PdfName.Length)); + Assertions.assertEquals(PdfName.ASCII85Decode, stream.get(PdfName.Filter)); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + } + + @Test + void removeAllFilters() { + final PdfStream stream = createHexStream(); + + PdfStreamUtil.removeAllFilters(stream); + + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(false)); + Assertions.assertArrayEquals(TEST_DATA, stream.getBytes(true)); + Assertions.assertEquals(CompressionConstants.NO_COMPRESSION, stream.getCompressionLevel()); + Assertions.assertEquals(new PdfNumber(TEST_DATA.length), stream.get(PdfName.Length)); + Assertions.assertFalse(stream.containsKey(PdfName.Filter)); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + } + + private static PdfStream createHexStream() { + final PdfStream stream = new PdfStream(TEST_DATA_HEX); + stream.put(PdfName.Length, new PdfNumber(TEST_DATA_HEX.length)); + stream.put(PdfName.Filter, PdfName.ASCIIHexDecode); + stream.put(PdfName.DecodeParms, PdfNull.PDF_NULL); + return stream; + } +} diff --git a/src/test/java/com/itextpdf/rups/view/contextmenu/ApplyFilterActionTest.java b/src/test/java/com/itextpdf/rups/view/contextmenu/ApplyFilterActionTest.java new file mode 100644 index 00000000..0cc13256 --- /dev/null +++ b/src/test/java/com/itextpdf/rups/view/contextmenu/ApplyFilterActionTest.java @@ -0,0 +1,220 @@ +/* + 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.PdfDocument; +import com.itextpdf.kernel.pdf.PdfName; +import com.itextpdf.kernel.pdf.PdfNumber; +import com.itextpdf.kernel.pdf.PdfStream; +import com.itextpdf.rups.controller.PdfReaderController; +import com.itextpdf.rups.io.encoders.ASCII85CompressionStrategy; +import com.itextpdf.rups.mock.NoopProgressDialog; +import com.itextpdf.rups.model.IRupsEventListener; +import com.itextpdf.rups.model.ObjectLoader; +import com.itextpdf.rups.model.PdfFile; +import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode; + +import java.io.File; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("UnitTest") +final class ApplyFilterActionTest { + private static final String TEST_PDF_PATH = "./src/test/resources/com/itextpdf/rups" + + "/controller/hello_world.pdf"; + private static final int TEST_PDF_NON_STREAM_OBJ_ID = 1; + private static final int TEST_PDF_STREAM_OBJ_ID = 5; + + private PdfReaderController controller; + private PdfFile pdfFile; + + @BeforeEach + void beforeEach() throws Exception { + controller = new PdfReaderController(null, null); + pdfFile = PdfFile.openAsOwner(new File(TEST_PDF_PATH)); + // Using a noop listener here to prevent threading issues + final ObjectLoader loader = new ObjectLoader( + new IRupsEventListener() {}, pdfFile, "Test loader", new NoopProgressDialog() + ); + loader.execute(); + loader.get(); + controller.handleOpenDocument(loader); + } + + @AfterEach + void afterEach() { + if (controller != null) { + controller.handleCloseDocument(); + controller = null; + } + if (pdfFile != null) { + final PdfDocument doc = pdfFile.getPdfDocument(); + if (doc != null) { + doc.close(); + } + pdfFile = null; + } + } + + @Test + void actionPerformed_success() { + final PdfObjectTreeNode node = selectTestStreamNode(); + final byte[] originalData = getStream(node).getBytes(); + final ApplyFilterAction action = new ApplyFilterAction( + "", + controller.getPdfTree(), + controller, + ASCII85CompressionStrategy::new + ); + action.actionPerformed(null); + // Data should get re-encoded and the dictionary should be updated + final PdfStream stream = getStream(node); + Assertions.assertArrayEquals(originalData, stream.getBytes()); + Assertions.assertEquals( + new PdfNumber(stream.getBytes(false).length), + stream.get(PdfName.Length) + ); + Assertions.assertEquals(PdfName.ASCII85Decode, stream.get(PdfName.Filter)); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + // Since controller was passed, children should be wiped: they will be + // recreated later via handlePdfTreeNodeClicked + Assertions.assertEquals(0, node.getChildCount()); + } + + @Test + void actionPerformed_withoutController() { + final PdfObjectTreeNode node = selectTestStreamNode(); + final byte[] originalData = getStream(node).getBytes(); + final ApplyFilterAction action = new ApplyFilterAction( + "", + controller.getPdfTree(), + null, + ASCII85CompressionStrategy::new + ); + action.actionPerformed(null); + // Data should get re-encoded and the dictionary should be updated + final PdfStream stream = getStream(node); + Assertions.assertArrayEquals(originalData, stream.getBytes()); + Assertions.assertEquals( + new PdfNumber(stream.getBytes(false).length), + stream.get(PdfName.Length) + ); + Assertions.assertEquals(PdfName.ASCII85Decode, stream.get(PdfName.Filter)); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + // The length child should remain, as action has no access to the + // controller to wipe them + Assertions.assertEquals(1, node.getChildCount()); + } + + @Test + void actionPerformed_failure() { + final PdfObjectTreeNode node = selectTestStreamNode(); + final byte[] originalData = getStream(node).getBytes(); + final ApplyFilterAction action = new ApplyFilterAction( + "", + controller.getPdfTree(), + controller, + () -> null + ); + action.actionPerformed(null); + // Data should remain as-is + final PdfStream stream = getStream(node); + Assertions.assertArrayEquals(originalData, stream.getBytes(true)); + Assertions.assertArrayEquals(originalData, stream.getBytes(false)); + Assertions.assertEquals(new PdfNumber(originalData.length), stream.get(PdfName.Length)); + Assertions.assertFalse(stream.containsKey(PdfName.Filter)); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + // The length child should remain, as the tree wasn't rebuilt because + // of the failure + Assertions.assertEquals(1, node.getChildCount()); + } + + @Test + void actionPerformed_noSelection() { + controller.getPdfTree().clearSelection(); + final ApplyFilterAction action = new ApplyFilterAction( + "", + controller.getPdfTree(), + controller, + () -> { + Assertions.fail("Strategy should not have been called"); + return null; + } + ); + action.actionPerformed(null); + } + + @Test + void actionPerformed_nonStreamSelected() { + controller.selectNode(TEST_PDF_NON_STREAM_OBJ_ID); + final ApplyFilterAction action = new ApplyFilterAction( + "", + controller.getPdfTree(), + controller, + () -> { + Assertions.fail("Strategy should not have been called"); + return null; + } + ); + action.actionPerformed(null); + } + + private PdfObjectTreeNode selectTestStreamNode() { + controller.selectNode(TEST_PDF_STREAM_OBJ_ID); + final PdfObjectTreeNode node = (PdfObjectTreeNode) controller.getPdfTree() + .getLastSelectedPathComponent(); + Assertions.assertNotNull(node); + Assertions.assertTrue(node.isStream()); + // This should create the /Length child + controller.handlePdfTreeNodeClicked(node); + Assertions.assertEquals(1, node.getChildCount()); + return node; + } + + private static PdfStream getStream(PdfObjectTreeNode node) { + return (PdfStream) node.getPdfObject(); + } +} diff --git a/src/test/java/com/itextpdf/rups/view/contextmenu/RemoveAllFiltersActionTest.java b/src/test/java/com/itextpdf/rups/view/contextmenu/RemoveAllFiltersActionTest.java new file mode 100644 index 00000000..1f8b38aa --- /dev/null +++ b/src/test/java/com/itextpdf/rups/view/contextmenu/RemoveAllFiltersActionTest.java @@ -0,0 +1,175 @@ +/* + 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.PdfDocument; +import com.itextpdf.kernel.pdf.PdfName; +import com.itextpdf.kernel.pdf.PdfNumber; +import com.itextpdf.kernel.pdf.PdfStream; +import com.itextpdf.rups.controller.PdfReaderController; +import com.itextpdf.rups.mock.NoopProgressDialog; +import com.itextpdf.rups.model.IRupsEventListener; +import com.itextpdf.rups.model.ObjectLoader; +import com.itextpdf.rups.model.PdfFile; +import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode; + +import java.io.File; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("UnitTest") +final class RemoveAllFiltersActionTest { + private static final String TEST_PDF_PATH = "./src/test/resources/com/itextpdf/rups" + + "/view/itext/cmp_purchase_order_filled.pdf"; + private static final int TEST_PDF_STREAM_OBJ_ID = 1762; + + private PdfReaderController controller; + private PdfFile pdfFile; + + @BeforeEach + void beforeEach() throws Exception { + controller = new PdfReaderController(null, null); + pdfFile = PdfFile.openAsOwner(new File(TEST_PDF_PATH)); + // Using a noop listener here to prevent threading issues + final ObjectLoader loader = new ObjectLoader( + new IRupsEventListener() {}, pdfFile, "Test loader", new NoopProgressDialog() + ); + loader.execute(); + loader.get(); + controller.handleOpenDocument(loader); + } + + @AfterEach + void afterEach() { + if (controller != null) { + controller.handleCloseDocument(); + controller = null; + } + if (pdfFile != null) { + final PdfDocument doc = pdfFile.getPdfDocument(); + if (doc != null) { + doc.close(); + } + pdfFile = null; + } + } + + @Test + void actionPerformed_success() { + final PdfObjectTreeNode node = selectTestStreamNode(); + final byte[] originalData = getStream(node).getBytes(); + final RemoveAllFiltersAction action = new RemoveAllFiltersAction( + "", + controller.getPdfTree(), + controller + ); + action.actionPerformed(null); + // Data should get re-encoded and the dictionary should be updated + final PdfStream stream = getStream(node); + Assertions.assertArrayEquals(originalData, stream.getBytes()); + Assertions.assertEquals( + new PdfNumber(stream.getBytes(false).length), + stream.get(PdfName.Length) + ); + Assertions.assertFalse(stream.containsKey(PdfName.Filter)); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + // Since controller was passed, children should be wiped: they will be + // recreated later via handlePdfTreeNodeClicked + Assertions.assertEquals(0, node.getChildCount()); + } + + @Test + void actionPerformed_withoutController() { + final PdfObjectTreeNode node = selectTestStreamNode(); + final byte[] originalData = getStream(node).getBytes(); + final RemoveAllFiltersAction action = new RemoveAllFiltersAction( + "", + controller.getPdfTree(), + null + ); + action.actionPerformed(null); + // Data should get re-encoded and the dictionary should be updated + final PdfStream stream = getStream(node); + Assertions.assertArrayEquals(originalData, stream.getBytes()); + Assertions.assertEquals( + new PdfNumber(stream.getBytes(false).length), + stream.get(PdfName.Length) + ); + Assertions.assertFalse(stream.containsKey(PdfName.Filter)); + Assertions.assertFalse(stream.containsKey(PdfName.DecodeParms)); + // The length child should remain, as action has no access to the + // controller to wipe them + Assertions.assertEquals(2, node.getChildCount()); + } + + @Test + void actionPerformed_noSelection() { + controller.getPdfTree().clearSelection(); + final RemoveAllFiltersAction action = new RemoveAllFiltersAction( + "", + controller.getPdfTree(), + controller + ); + // Should just be a noop without selection + Assertions.assertDoesNotThrow(() -> action.actionPerformed(null)); + } + + private PdfObjectTreeNode selectTestStreamNode() { + controller.selectNode(TEST_PDF_STREAM_OBJ_ID); + final PdfObjectTreeNode node = (PdfObjectTreeNode) controller.getPdfTree() + .getLastSelectedPathComponent(); + Assertions.assertNotNull(node); + Assertions.assertTrue(node.isStream()); + // This should create the /Length and /Filter child + controller.handlePdfTreeNodeClicked(node); + Assertions.assertEquals(2, node.getChildCount()); + return node; + } + + private static PdfStream getStream(PdfObjectTreeNode node) { + return (PdfStream) node.getPdfObject(); + } +}