From f60954c37e90bfda2ccea2e9f899295cd505b1c9 Mon Sep 17 00:00:00 2001 From: Vlad Lipskiy Date: Wed, 3 Dec 2025 15:29:39 +0300 Subject: [PATCH 1/4] Add experimental support for reading BrotliDecode With this commit all modified streams are written with `BrotliDecode`. --- pom.xml | 7 ++++++- src/main/java/com/itextpdf/rups/model/PdfFile.java | 14 +++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c6d05034..bb8175c0 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,7 @@ 2.2.0 3.6.2 76.1 - 9.4.0 + 9.5.0-SNAPSHOT 2.20.1 1.5.21 @@ -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/model/PdfFile.java b/src/main/java/com/itextpdf/rups/model/PdfFile.java index 400a30c3..214e84c8 100644 --- a/src/main/java/com/itextpdf/rups/model/PdfFile.java +++ b/src/main/java/com/itextpdf/rups/model/PdfFile.java @@ -42,11 +42,14 @@ This file is part of the iText (R) project. */ package com.itextpdf.rups.model; +import com.itextpdf.brotlicompressor.BrotliStreamCompressionStrategy; import com.itextpdf.kernel.exceptions.BadPasswordException; +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.rups.view.Language; import java.io.ByteArrayInputStream; @@ -252,7 +255,7 @@ private boolean openDocumentReadWrite(byte[] password) throws IOException { ); final ByteArrayOutputStream tempWriterOutputStream = new ByteArrayOutputStream(); final PdfWriter writer = new PdfWriter(tempWriterOutputStream); - document = new PdfDocument(reader, writer); + document = new PdfDocument(reader, writer, createStampingProps()); writerOutputStream = tempWriterOutputStream; return true; } catch (BadPasswordException e) { @@ -293,4 +296,13 @@ private boolean openDocumentReadOnly(byte[] password) throws IOException { return false; } } + + private static StampingProperties createStampingProps() { + final StampingProperties props = new StampingProperties(); + props.registerDependency( + IStreamCompressionStrategy.class, + new BrotliStreamCompressionStrategy() + ); + return props; + } } From e2ada8537cad7d7f327fcdaf0126799f145f35f6 Mon Sep 17 00:00:00 2001 From: Vlad Lipskiy Date: Wed, 10 Dec 2025 15:24:52 +0300 Subject: [PATCH 2/4] Add ability to manipulate stream filters When you open a document as owner, you can now manipulate stream filters via the context menu. The available options at the moment are the following: 1. Remove all filters from a stream. I.e. save the decoded stream into the file. 2. Add a `FlateDecode` filter with the default compression level. 3. Add a `BrotliDecode` filter with the default compression level. When you add a filter, they are layered on top, not replaced. So you could test some of the more unorthodox configurations. Additionally, when you save a stream now, it is saved without any filters applied by default. And now the `Length` value is correctly updated on the stream. --- .../rups/controller/PdfReaderController.java | 15 +- .../controller/RupsInstanceController.java | 2 +- .../com/itextpdf/rups/util/PdfStreamUtil.java | 195 ++++++++++++++++++ .../java/com/itextpdf/rups/view/Language.java | 3 + .../contextmenu/AbstractPdfStreamAction.java | 87 ++++++++ .../view/contextmenu/ApplyFilterAction.java | 89 ++++++++ .../contextmenu/IPdfContextMenuTarget.java | 7 + .../view/contextmenu/PdfTreeContextMenu.java | 89 ++++++-- .../contextmenu/RemoveAllFiltersAction.java | 67 ++++++ .../com/itextpdf/rups/view/itext/PdfTree.java | 16 ++ .../itext/SyntaxHighlightedStreamPane.java | 38 ++-- .../itext/treenodes/PdfObjectTreeNode.java | 8 + .../treenodes/asn1/AbstractAsn1TreeNode.java | 8 + .../resources/bundles/rups-lang.properties | 8 +- .../bundles/rups-lang_en_US.properties | 25 +++ 15 files changed, 605 insertions(+), 52 deletions(-) create mode 100644 src/main/java/com/itextpdf/rups/util/PdfStreamUtil.java create mode 100644 src/main/java/com/itextpdf/rups/view/contextmenu/AbstractPdfStreamAction.java create mode 100644 src/main/java/com/itextpdf/rups/view/contextmenu/ApplyFilterAction.java create mode 100644 src/main/java/com/itextpdf/rups/view/contextmenu/RemoveAllFiltersAction.java diff --git a/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java b/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java index aab74cc0..7ac0a3e1 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 aa52a64f..45ec0b15 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/util/PdfStreamUtil.java b/src/main/java/com/itextpdf/rups/util/PdfStreamUtil.java new file mode 100644 index 00000000..1bb890f1 --- /dev/null +++ b/src/main/java/com/itextpdf/rups/util/PdfStreamUtil.java @@ -0,0 +1,195 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2025 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 setDataWithDefaultCompression(PdfStream stream, byte[] data) + throws IOException { + // By default, we will be saving the stream as-is without compression + stream.setData(data); + stream.setCompressionLevel(CompressionConstants.NO_COMPRESSION); + stream.put(PdfName.Length, new PdfNumber(data.length)); + stream.remove(PdfName.Filter); + stream.remove(PdfName.DecodeParms); + } + + 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 1aa8b93f..0767f627 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, @@ -212,6 +214,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/contextmenu/AbstractPdfStreamAction.java b/src/main/java/com/itextpdf/rups/view/contextmenu/AbstractPdfStreamAction.java new file mode 100644 index 00000000..fa758f54 --- /dev/null +++ b/src/main/java/com/itextpdf/rups/view/contextmenu/AbstractPdfStreamAction.java @@ -0,0 +1,87 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2025 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; +import javax.swing.tree.TreeSelectionModel; + +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 TreeSelectionModel selectionModel = tree.getSelectionModel(); + final TreePath[] paths = selectionModel.getSelectionPaths(); + if (paths == null || paths.length == 0) { + return null; + } + final Object node = paths[0].getLastPathComponent(); + 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) { + controller.deleteAllTreeChildren(root); + controller.selectNode(root); + } + } +} 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..76d39216 --- /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-2025 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 cc7768bd..edabe6f5 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 a39c94e9..5640f209 100644 --- a/src/main/java/com/itextpdf/rups/view/contextmenu/PdfTreeContextMenu.java +++ b/src/main/java/com/itextpdf/rups/view/contextmenu/PdfTreeContextMenu.java @@ -42,12 +42,18 @@ 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.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. @@ -55,38 +61,77 @@ This file is part of the iText (R) project. * @author Michael Demey */ 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 applyBrotliDecodeMenu = createJMenuItem(new ApplyFilterAction( + PdfName.BrotliDecode.getValue(), + parentTree, + controller, + BrotliStreamCompressionStrategy::new + )); + final JMenuItem applyFlateDecodeMenu = createJMenuItem(new ApplyFilterAction( + PdfName.FlateDecode.getValue(), + parentTree, + controller, + FlateCompressionStrategy::new + )); + + filterSectionSeparator = new JPopupMenu.Separator(); + + applyFilterSubMenu = new JMenu(Language.APPLY_FILTER.getString()); + applyFilterSubMenu.add(applyBrotliDecodeMenu); + applyFilterSubMenu.add(applyFlateDecodeMenu); - 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..5f340617 --- /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-2025 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 ce5548c1..756ade62 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 6427fc41..5ce262cc 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,15 @@ 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.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 +81,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 +209,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 +220,18 @@ public void saveToTarget() { } catch (IOException e) { LoggerHelper.error(Language.ERROR_UNEXPECTED_EXCEPTION.getString(), e, getClass()); } - ((PdfStream) target.getPdfObject()).setData(baos.toByteArray()); + try { + PdfStreamUtil.setDataWithDefaultCompression(targetStream, baos.toByteArray()); + } 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 4ecd9360..1a287576 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 @@ -433,6 +433,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 4fe239ee..f488661d 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..6d0f9d01 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. @@ -179,6 +182,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..6d0f9d01 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 @@ -158,7 +171,18 @@ 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_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 +191,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 From da93df8f58e5dd44224c4faf1fb80b97f792441a Mon Sep 17 00:00:00 2001 From: Vlad Lipskiy Date: Wed, 10 Dec 2025 16:09:40 +0300 Subject: [PATCH 3/4] Add ability to select default stream filter Now you can change this in the preferences window. This will affect the streams created automatically by iText itself and the default filter used, when saving stream. You can still remove all filters afterward, if required. --- .../com/itextpdf/rups/RupsConfiguration.java | 50 ++++++++++++ .../itextpdf/rups/conf/StreamFilterId.java | 79 +++++++++++++++++++ .../java/com/itextpdf/rups/model/PdfFile.java | 23 ++++-- .../com/itextpdf/rups/util/PdfStreamUtil.java | 27 +++++-- .../java/com/itextpdf/rups/view/Language.java | 2 + .../itextpdf/rups/view/PreferencesWindow.java | 22 ++++++ .../itext/SyntaxHighlightedStreamPane.java | 7 +- .../resources/bundles/rups-lang.properties | 2 + .../bundles/rups-lang_en_US.properties | 2 + src/main/resources/config/default.properties | 1 + 10 files changed, 202 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/itextpdf/rups/conf/StreamFilterId.java diff --git a/src/main/java/com/itextpdf/rups/RupsConfiguration.java b/src/main/java/com/itextpdf/rups/RupsConfiguration.java index 0a3171da..222d922d 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..11cd0c9d --- /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-2025 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/model/PdfFile.java b/src/main/java/com/itextpdf/rups/model/PdfFile.java index 214e84c8..24e75c85 100644 --- a/src/main/java/com/itextpdf/rups/model/PdfFile.java +++ b/src/main/java/com/itextpdf/rups/model/PdfFile.java @@ -42,14 +42,16 @@ This file is part of the iText (R) project. */ package com.itextpdf.rups.model; -import com.itextpdf.brotlicompressor.BrotliStreamCompressionStrategy; 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; @@ -254,7 +256,7 @@ private boolean openDocumentReadWrite(byte[] password) throws IOException { readerProperties ); final ByteArrayOutputStream tempWriterOutputStream = new ByteArrayOutputStream(); - final PdfWriter writer = new PdfWriter(tempWriterOutputStream); + final PdfWriter writer = new PdfWriter(tempWriterOutputStream, createWriterProperties()); document = new PdfDocument(reader, writer, createStampingProps()); writerOutputStream = tempWriterOutputStream; return true; @@ -297,12 +299,21 @@ private boolean openDocumentReadOnly(byte[] password) throws IOException { } } + 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(); - props.registerDependency( - IStreamCompressionStrategy.class, - new BrotliStreamCompressionStrategy() - ); + 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 index 1bb890f1..908b478f 100644 --- a/src/main/java/com/itextpdf/rups/util/PdfStreamUtil.java +++ b/src/main/java/com/itextpdf/rups/util/PdfStreamUtil.java @@ -84,14 +84,29 @@ public static void applyFilter(PdfStream stream, IStreamCompressionStrategy stra } } - public static void setDataWithDefaultCompression(PdfStream stream, byte[] data) + public static void setDataWithFilter(PdfStream stream, byte[] data, IStreamCompressionStrategy strategy) throws IOException { - // By default, we will be saving the stream as-is without compression - stream.setData(data); + 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(data.length)); - stream.remove(PdfName.Filter); - stream.remove(PdfName.DecodeParms); + 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) { diff --git a/src/main/java/com/itextpdf/rups/view/Language.java b/src/main/java/com/itextpdf/rups/view/Language.java index 0767f627..cbc6005a 100644 --- a/src/main/java/com/itextpdf/rups/view/Language.java +++ b/src/main/java/com/itextpdf/rups/view/Language.java @@ -186,6 +186,7 @@ public enum Language { MENU_BAR_VERSION, MESSAGE_ABOUT, + NONE, NO_SELECTED_FILE, NULL_AS_TEXT, @@ -205,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, diff --git a/src/main/java/com/itextpdf/rups/view/PreferencesWindow.java b/src/main/java/com/itextpdf/rups/view/PreferencesWindow.java index d38b1130..5b7dc804 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/itext/SyntaxHighlightedStreamPane.java b/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java index 5ce262cc..a5473cd7 100644 --- a/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java +++ b/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java @@ -47,6 +47,7 @@ This file is part of the iText (R) project. 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; @@ -221,7 +222,11 @@ public void saveToTarget() { LoggerHelper.error(Language.ERROR_UNEXPECTED_EXCEPTION.getString(), e, getClass()); } try { - PdfStreamUtil.setDataWithDefaultCompression(targetStream, baos.toByteArray()); + PdfStreamUtil.setDataWithFilter( + targetStream, + baos.toByteArray(), + RupsConfiguration.INSTANCE.getDefaultFilterStrategy() + ); } catch (IOException e) { final String errorMessage = Language.ERROR_APPLYING_FILTER.getString(); LoggerHelper.error(errorMessage, e, getClass()); diff --git a/src/main/resources/bundles/rups-lang.properties b/src/main/resources/bundles/rups-lang.properties index 6d0f9d01..7e4da7cb 100644 --- a/src/main/resources/bundles/rups-lang.properties +++ b/src/main/resources/bundles/rups-lang.properties @@ -148,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 @@ -173,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 diff --git a/src/main/resources/bundles/rups-lang_en_US.properties b/src/main/resources/bundles/rups-lang_en_US.properties index 6d0f9d01..7e4da7cb 100644 --- a/src/main/resources/bundles/rups-lang_en_US.properties +++ b/src/main/resources/bundles/rups-lang_en_US.properties @@ -148,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 @@ -173,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 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 From c84e37c2830c20c9f76e2355aef4a8e9d5b22592 Mon Sep 17 00:00:00 2001 From: Vlad Lipskiy Date: Wed, 10 Dec 2025 22:24:59 +0300 Subject: [PATCH 4/4] Add additional stream filters to apply * ASCII85Decode * ASCIIHexDecode * RunLengthDecode --- .../encoders/ASCII85CompressionStrategy.java | 67 ++++++++ .../rups/io/encoders/ASCII85OutputStream.java | 138 +++++++++++++++++ .../encoders/ASCIIHexCompressionStrategy.java | 67 ++++++++ .../io/encoders/ASCIIHexOutputStream.java | 92 +++++++++++ .../RunLengthCompressionStrategy.java | 67 ++++++++ .../io/encoders/RunLengthOutputStream.java | 146 ++++++++++++++++++ .../view/contextmenu/PdfTreeContextMenu.java | 24 +++ 7 files changed, 601 insertions(+) create mode 100644 src/main/java/com/itextpdf/rups/io/encoders/ASCII85CompressionStrategy.java create mode 100644 src/main/java/com/itextpdf/rups/io/encoders/ASCII85OutputStream.java create mode 100644 src/main/java/com/itextpdf/rups/io/encoders/ASCIIHexCompressionStrategy.java create mode 100644 src/main/java/com/itextpdf/rups/io/encoders/ASCIIHexOutputStream.java create mode 100644 src/main/java/com/itextpdf/rups/io/encoders/RunLengthCompressionStrategy.java create mode 100644 src/main/java/com/itextpdf/rups/io/encoders/RunLengthOutputStream.java 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..8c64b6f9 --- /dev/null +++ b/src/main/java/com/itextpdf/rups/io/encoders/ASCII85CompressionStrategy.java @@ -0,0 +1,67 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2025 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; + +public class ASCII85CompressionStrategy implements IStreamCompressionStrategy { + @Override + public PdfName getFilterName() { + return PdfName.ASCII85Decode; + } + + @Override + public PdfObject getDecodeParams() { + return null; + } + + @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..e714bba7 --- /dev/null +++ b/src/main/java/com/itextpdf/rups/io/encoders/ASCII85OutputStream.java @@ -0,0 +1,138 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2025 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; + +public class ASCII85OutputStream extends FilterOutputStream implements IFinishable { + private static final int OFFSET = 33; + private static final int BASE = 85; + private static final int INPUT_LENGTH = 4; + private static final int OUTPUT_LENGTH = 5; + private static final byte ALL_ZEROS_MARKER = 'z'; + private static final byte[] EOD = new byte[] {'~', '>'}; + + private final byte[] buffer = new byte[OUTPUT_LENGTH]; + private int inputOr = 0; + private int inputCursor = 0; + + private final AtomicBoolean finished = new AtomicBoolean(false); + + public ASCII85OutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int b) throws IOException { + final int value = b & 0xFF; + buffer[inputCursor] = (byte) value; + inputOr |= value; + ++inputCursor; + writeBufferIfFull(); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (int i = off; i < off + len; ++i) { + write(b[i]); + } + } + + @Override + public void close() throws IOException { + finish(); + super.close(); + } + + @Override + public void finish() throws IOException { + if (finished.getAndSet(true)) { + return; + } + // Writing the remainder + if (inputCursor > 0) { + 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; + } + // Special case, if all zeros + if (inputOr == 0) { + 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..1b277d74 --- /dev/null +++ b/src/main/java/com/itextpdf/rups/io/encoders/ASCIIHexCompressionStrategy.java @@ -0,0 +1,67 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2025 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; + +public class ASCIIHexCompressionStrategy implements IStreamCompressionStrategy { + @Override + public PdfName getFilterName() { + return PdfName.ASCIIHexDecode; + } + + @Override + public PdfObject getDecodeParams() { + return null; + } + + @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..626eba9d --- /dev/null +++ b/src/main/java/com/itextpdf/rups/io/encoders/ASCIIHexOutputStream.java @@ -0,0 +1,92 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2025 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; + +public class ASCIIHexOutputStream extends FilterOutputStream implements IFinishable { + private static final byte EOD = '>'; + private static final byte[] CHAR_MAP = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + private final AtomicBoolean finished = new AtomicBoolean(false); + + public ASCIIHexOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int b) throws IOException { + final int value = (b & 0xFF); + out.write(CHAR_MAP[value >> 4]); + out.write(CHAR_MAP[value & 0x0F]); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (int i = off; i < off + len; ++i) { + write(b[i]); + } + } + + @Override + public void close() throws IOException { + finish(); + super.close(); + } + + @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..023389b9 --- /dev/null +++ b/src/main/java/com/itextpdf/rups/io/encoders/RunLengthCompressionStrategy.java @@ -0,0 +1,67 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2025 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; + +public class RunLengthCompressionStrategy implements IStreamCompressionStrategy { + @Override + public PdfName getFilterName() { + return PdfName.RunLengthDecode; + } + + @Override + public PdfObject getDecodeParams() { + return null; + } + + @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..3f4520f0 --- /dev/null +++ b/src/main/java/com/itextpdf/rups/io/encoders/RunLengthOutputStream.java @@ -0,0 +1,146 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2025 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; + +public class RunLengthOutputStream extends FilterOutputStream implements IFinishable { + private static final int MAX_LENGTH = 128; + private static final byte EOD = (byte) 128; + + private final byte[] buffer = new byte[MAX_LENGTH]; + private int repeatValue = -1; + private int currentLength = 0; + + private final AtomicBoolean finished = new AtomicBoolean(false); + + public RunLengthOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int b) throws IOException { + final int value = b & 0xFF; + // If continuing old streak + if (value == repeatValue) { + ++currentLength; + if (currentLength == MAX_LENGTH) { + writePending(); + } + return; + } + // If there was a streak, but it is now gone + if (repeatValue != -1) { + writePending(); + buffer[currentLength] = (byte) value; + ++currentLength; + return; + } + // If a streak started + if (currentLength >= 2 + && buffer[currentLength - 1] == (byte) value + && buffer[currentLength - 2] == (byte) value) { + currentLength -= 2; + writePending(); + repeatValue = value; + currentLength = 3; + return; + } + // No streak, just adding another byte + buffer[currentLength] = (byte) value; + ++currentLength; + if (currentLength == MAX_LENGTH) { + writePending(); + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (int i = off; i < off + len; ++i) { + write(b[i]); + } + } + + @Override + public void close() throws IOException { + finish(); + super.close(); + } + + @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; + } + assert currentLength <= MAX_LENGTH; + if (repeatValue < 0) { + // Writing regular bytes + out.write(currentLength - 1); + out.write(buffer, 0, currentLength); + } else { + // Writing repeats + out.write(257 - currentLength); + out.write(repeatValue); + } + resetPending(); + } + + private void resetPending() { + repeatValue = -1; + currentLength = 0; + } +} 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 5640f209..fd6b33b7 100644 --- a/src/main/java/com/itextpdf/rups/view/contextmenu/PdfTreeContextMenu.java +++ b/src/main/java/com/itextpdf/rups/view/contextmenu/PdfTreeContextMenu.java @@ -46,6 +46,9 @@ This file is part of the iText (R) project. 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.view.Language; import com.itextpdf.rups.view.itext.PdfTree; @@ -92,6 +95,18 @@ public PdfTreeContextMenu(PdfTree parentTree, PdfReaderController controller) { 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, @@ -104,12 +119,21 @@ public PdfTreeContextMenu(PdfTree parentTree, PdfReaderController controller) { 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(inspectObjectMenu); add(saveRawBytesToFileMenu);