diff --git a/pom.xml b/pom.xml
index ff06e5f3..67b0725e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -53,6 +53,7 @@
5.9.2
2.0.1
1.2.11
+ 0.0.1-SNAPSHOT
UTF-8
RUPS
11
@@ -93,6 +94,11 @@
dom4j
${dom4j.version}
+
+ be.ysebie
+ diff
+ ${diff.version}
+
com.itextpdf
pdftest
diff --git a/src/main/java/com/itextpdf/rups/controller/IRupsController.java b/src/main/java/com/itextpdf/rups/controller/IRupsController.java
index 15b812c3..a8317c2b 100644
--- a/src/main/java/com/itextpdf/rups/controller/IRupsController.java
+++ b/src/main/java/com/itextpdf/rups/controller/IRupsController.java
@@ -91,4 +91,6 @@ public interface IRupsController {
* Closes the current file and tries to open it as an owner again.
*/
void reopenAsOwner();
+
+ void openDiffViewer(File fileA, File fileB);
}
diff --git a/src/main/java/com/itextpdf/rups/controller/RupsController.java b/src/main/java/com/itextpdf/rups/controller/RupsController.java
index 092db429..8af1d9ea 100644
--- a/src/main/java/com/itextpdf/rups/controller/RupsController.java
+++ b/src/main/java/com/itextpdf/rups/controller/RupsController.java
@@ -150,6 +150,11 @@ public void reopenAsOwner() {
}
}
+ @Override
+ public void openDiffViewer(File fileA, File fileB) {
+ this.rupsTabbedPane.openDiffViewer(fileA, fileB);
+ }
+
@Override
public final IPdfFile getCurrentFile() {
return this.rupsTabbedPane.getCurrentFile();
diff --git a/src/main/java/com/itextpdf/rups/view/RupsMenuBar.java b/src/main/java/com/itextpdf/rups/view/RupsMenuBar.java
index e7ca26b3..3b3ef07b 100644
--- a/src/main/java/com/itextpdf/rups/view/RupsMenuBar.java
+++ b/src/main/java/com/itextpdf/rups/view/RupsMenuBar.java
@@ -56,6 +56,7 @@ This file is part of the iText (R) project.
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
+import java.io.File;
import java.util.HashMap;
import java.util.Observable;
import java.util.Observer;
@@ -123,7 +124,13 @@ public RupsMenuBar(RupsController controller) {
}
);
add(edit);
-
+ final JMenu diffViewer = new JMenu("DiffViewer");
+ addItem(diffViewer, "DiffViewer", e -> {
+ File fileA = new File("src/main/resources/simpleParagraphTest.pdf");
+ File fileB = new File("src/main/resources/cmp_simpleParagraphTest.pdf");
+ controller.openDiffViewer(fileB,fileA);
+ } );
+ add(diffViewer);
add(Box.createGlue());
final JMenu help = new JMenu(Language.MENU_BAR_HELP.getString());
diff --git a/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java b/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java
index 3915d31c..ca54ae3d 100644
--- a/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java
+++ b/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java
@@ -45,15 +45,13 @@ This file is part of the iText (R) project.
import com.itextpdf.rups.Rups;
import com.itextpdf.rups.controller.RupsInstanceController;
import com.itextpdf.rups.model.IPdfFile;
+import com.itextpdf.rups.view.diff.DiffViewer;
-import java.awt.Component;
-import java.awt.Dimension;
-import java.io.File;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JTabbedPane;
+import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.io.File;
/**
* The class holding the JTabbedPane that holds the Rups tabs. This class is responsible for loading, closing, and
@@ -170,4 +168,10 @@ private void showReadOnlyWarning() {
Snackbar.make(Rups.getMainFrame(), Language.WARNING_OPENED_IN_READ_ONLY_MODE.getString()).show();
}
}
+
+ public void openDiffViewer(File fileA, File fileB) {
+ DiffViewer diffViewer = new DiffViewer(fileA,fileB);
+ this.jTabbedPane.addTab("DiffView", null, diffViewer);
+ this.jTabbedPane.setSelectedComponent(diffViewer);
+ }
}
diff --git a/src/main/java/com/itextpdf/rups/view/diff/DiffGenerationStrategy.java b/src/main/java/com/itextpdf/rups/view/diff/DiffGenerationStrategy.java
new file mode 100644
index 00000000..351c9b53
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/view/diff/DiffGenerationStrategy.java
@@ -0,0 +1,72 @@
+package com.itextpdf.rups.view.diff;
+
+import be.ysebie.diff.lib.DiffStrategy;
+import com.itextpdf.io.source.ByteArrayOutputStream;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.zip.DataFormatException;
+
+public class DiffGenerationStrategy implements DiffStrategy {
+
+ private final ViewerOptions options;
+
+ private ByteBuffer[] tokenizedDataA;
+ private ByteBuffer[] tokenizedDataB;
+
+ DiffGenerationStrategy(ViewerOptions options) throws IOException, DataFormatException {
+ this.options = options;
+ setupDiffData();
+ }
+
+ private void setupDiffData() throws IOException, DataFormatException {
+ if (options.fileA == null || options.fileB == null) {
+ throw new IOException("Files not set");
+ }
+ tokenizedDataA = generateData(parsePdfDocument(Files.readAllBytes(options.fileA.toPath())));
+ tokenizedDataB = generateData(parsePdfDocument(Files.readAllBytes(options.fileB.toPath())));
+ }
+
+ @Override
+ public ByteBuffer[] getDiffDataA() {
+ return tokenizedDataA;
+ }
+
+ @Override
+ public ByteBuffer[] getDiffDataB() {
+ return tokenizedDataB;
+ }
+
+ private ByteBuffer[] generateData(byte[] dataA) {
+ // if the byte is a new line, split the array
+ int start = 0;
+ int end = 0;
+ List result = new ArrayList<>();
+ for (int i = 0; i < dataA.length; i++) {
+ if (dataA[i] == '\n') {
+ end = i;
+ byte[] tmp = Arrays.copyOfRange(dataA, start, end);
+ start = end + 1;
+ result.add(ByteBuffer.wrap(tmp));
+ }
+ }
+ if (start < dataA.length) {
+ result.add(ByteBuffer.wrap(Arrays.copyOfRange(dataA, start, dataA.length)));
+ }
+ return result.toArray(new ByteBuffer[0]);
+ }
+
+ private byte[] parsePdfDocument(byte[] data) throws IOException, DataFormatException {
+ if (!options.decompress) {
+ return data;
+ }
+ PdfDiffModifier diffViewerTokenizer = new PdfDiffModifier(data);
+ ByteArrayOutputStream baos = diffViewerTokenizer.next();
+ return baos.toByteArray();
+ }
+}
+
diff --git a/src/main/java/com/itextpdf/rups/view/diff/DiffViewer.java b/src/main/java/com/itextpdf/rups/view/diff/DiffViewer.java
new file mode 100644
index 00000000..5090f4dd
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/view/diff/DiffViewer.java
@@ -0,0 +1,232 @@
+package com.itextpdf.rups.view.diff;
+
+import be.ysebie.diff.lib.Delta;
+import be.ysebie.diff.lib.Diff;
+import be.ysebie.diff.lib.deltaimpl.ChangeDelta;
+import be.ysebie.diff.lib.deltaimpl.DeleteDelta;
+import be.ysebie.diff.lib.deltaimpl.EqualDelta;
+import be.ysebie.diff.lib.deltaimpl.InsertDelta;
+
+import javax.swing.*;
+import javax.swing.text.SimpleAttributeSet;
+import javax.swing.text.StyleConstants;
+import javax.swing.text.StyledDocument;
+import java.awt.*;
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+public class DiffViewer extends JPanel {
+
+ private final ViewerOptions options;
+ private final JFileChooser filePicker = new JFileChooser();
+ private final JCheckBox cbDecompress = new JCheckBox("Decompress");
+ private final JPanel optionsBox = new JPanel();
+ JPanel mainPanel = new JPanel();
+
+ public DiffViewer(File a, File b) {
+ options = new ViewerOptions();
+ options.fileA = a;
+ options.fileB = b;
+ BorderLayout layout = new BorderLayout();
+ layout.setHgap(1);
+ layout.setVgap(1);
+ setLayout(layout);
+ GridLayout layout2 = new GridLayout(1, 2);
+ mainPanel.setLayout(layout2);
+
+ add(mainPanel, BorderLayout.CENTER);
+ setupOptionSelector();
+ setupMainView();
+ }
+
+ private static String convertByteBufferToString(ByteBuffer byteBuffer) {
+ return new String(byteBuffer.array());
+ }
+
+ private void setupOptionSelector() {
+ cbDecompress.setSelected(options.decompress);
+ cbDecompress.addActionListener((l) -> {
+ options.decompress = cbDecompress.isSelected();
+ rerenderDiff();
+ });
+ optionsBox.add(cbDecompress);
+ add(optionsBox, BorderLayout.NORTH);
+ }
+
+ private void rerenderDiff() {
+ SwingUtilities.invokeLater(this::setupMainView);
+
+ }
+
+ private void setupMainView() {
+ mainPanel.removeAll();
+
+ JPanel panelA = new JPanel();
+ JPanel panelB = new JPanel();
+ Box vboxA = Box.createVerticalBox();
+ Box vboxB = Box.createVerticalBox();
+
+
+ vboxA.setSize(100, 100);
+ vboxB.setSize(100, 100);
+
+ panelA.setLayout(new BoxLayout(panelA, BoxLayout.Y_AXIS));
+ panelB.setLayout(new BoxLayout(panelB, BoxLayout.Y_AXIS));
+
+ vboxA.setVisible(true);
+ vboxB.setVisible(true);
+
+ JButton btnFileA = new JButton("File A");
+ btnFileA.addActionListener((l) -> {
+ int returnVal = filePicker.showOpenDialog(this);
+ if (returnVal == JFileChooser.APPROVE_OPTION) {
+ options.fileA = filePicker.getSelectedFile();
+ rerenderDiff();
+ }
+ });
+
+ JButton btnFileB = new JButton("File B");
+ btnFileB.addActionListener((l) -> {
+ int returnVal = filePicker.showOpenDialog(this);
+ if (returnVal == JFileChooser.APPROVE_OPTION) {
+ options.fileB = filePicker.getSelectedFile();
+ rerenderDiff();
+ }
+ });
+
+ JLabel labelA = new JLabel("File A: " + (options.fileA != null ? options.fileA.getName() : "None"));
+ JLabel labelB = new JLabel("File B: " + (options.fileB != null ? options.fileB.getName() : "None"));
+
+ labelA.setHorizontalAlignment(SwingConstants.CENTER);
+ labelB.setHorizontalAlignment(SwingConstants.CENTER);
+
+ labelA.setSize(100, 100);
+ labelB.setSize(100, 100);
+
+ vboxA.add(labelA);
+ vboxA.add(btnFileA);
+
+ vboxB.add(labelB);
+ vboxB.add(btnFileB);
+
+ JTextPane textPaneA = new JTextPane();
+ textPaneA.setEditable(false);
+ JTextPane textPaneB = new JTextPane();
+ textPaneB.setEditable(false);
+
+
+ JScrollPane scrollPaneA = new JScrollPane(textPaneA);
+ JScrollPane scrollPaneB = new JScrollPane(textPaneB);
+
+ scrollPaneA.getVerticalScrollBar().addAdjustmentListener((l) -> {
+ scrollPaneB.getVerticalScrollBar().setValue(scrollPaneA.getVerticalScrollBar().getValue());
+ });
+ scrollPaneA.getHorizontalScrollBar().addAdjustmentListener((l) -> {
+ scrollPaneB.getHorizontalScrollBar().setValue(scrollPaneA.getHorizontalScrollBar().getValue());
+ });
+
+ scrollPaneB.getVerticalScrollBar().addAdjustmentListener((l) -> {
+ scrollPaneA.getVerticalScrollBar().setValue(scrollPaneB.getVerticalScrollBar().getValue());
+ });
+
+ scrollPaneB.getHorizontalScrollBar().addAdjustmentListener((l) -> {
+ scrollPaneA.getHorizontalScrollBar().setValue(scrollPaneB.getHorizontalScrollBar().getValue());
+ });
+
+ StyledDocument docA = textPaneA.getStyledDocument();
+ StyledDocument docB = textPaneB.getStyledDocument();
+
+ vboxA.add(scrollPaneA);
+ vboxB.add(scrollPaneB);
+
+ if (options.fileA != null && options.fileB != null) {
+ System.out.println("Generating diff");
+ try {
+ DiffGenerationStrategy diffGenerationStrategy = new DiffGenerationStrategy(options);
+ Diff diff = new Diff(diffGenerationStrategy);
+ List> deltas = diff.generateDeltas();
+ for (Delta delta : deltas) {
+ switch (delta.getType()) {
+ case Change:
+ System.out.println("Change");
+ ChangeDelta changeDelta = (ChangeDelta) delta;
+ StringBuilder sbChangeA = new StringBuilder();
+ StringBuilder sbChangeB = new StringBuilder();
+ for (ByteBuffer data : changeDelta.getDeletedData()) {
+ sbChangeA.append(convertByteBufferToString(data)).append("\n");
+ }
+ for (ByteBuffer data : changeDelta.getInsertedData()) {
+ sbChangeB.append(convertByteBufferToString(data)).append("\n");
+ }
+ SimpleAttributeSet keyWord = new SimpleAttributeSet();
+ StyleConstants.setForeground(keyWord, Color.BLACK);
+ StyleConstants.setBackground(keyWord, Color.YELLOW);
+ StyleConstants.setBold(keyWord, true);
+
+ docA.insertString(docA.getLength(), sbChangeA.toString(), keyWord);
+ docB.insertString(docB.getLength(), sbChangeB.toString(), keyWord);
+ break;
+ case Delete:
+ DeleteDelta deleteDelta = (DeleteDelta) delta;
+ StringBuilder sbADelete = new StringBuilder();
+ StringBuilder sbBDelete = new StringBuilder();
+ for (ByteBuffer data : deleteDelta.getDeletedData()) {
+ sbADelete.append(convertByteBufferToString(data)).append("\n");
+ sbBDelete.append("\n");
+ }
+ SimpleAttributeSet keyWordDelete = new SimpleAttributeSet();
+ StyleConstants.setForeground(keyWordDelete, Color.BLACK);
+ StyleConstants.setBackground(keyWordDelete, Color.RED);
+
+ docA.insertString(docA.getLength(), sbADelete.toString(), keyWordDelete);
+ docB.insertString(docB.getLength(), sbBDelete.toString(), keyWordDelete);
+
+ break;
+ case Equal:
+ EqualDelta equalDelta = (EqualDelta) delta;
+ StringBuilder sb = new StringBuilder();
+ for (ByteBuffer data : equalDelta.getData()) {
+ sb.append(convertByteBufferToString(data)).append("\n");
+ }
+ docA.insertString(docA.getLength(), sb.toString(), null);
+ docB.insertString(docB.getLength(), sb.toString(), null);
+ break;
+ case Insert:
+ System.out.println("inserted");
+ InsertDelta insertDelta = (InsertDelta) delta;
+ StringBuilder sbA = new StringBuilder();
+ StringBuilder sbB = new StringBuilder();
+ for (ByteBuffer data : insertDelta.getInsertedData()) {
+ sbA.append("\n");
+ sbB.append(convertByteBufferToString(data)).append("\n");
+ }
+ SimpleAttributeSet keyWordInsert = new SimpleAttributeSet();
+ StyleConstants.setForeground(keyWordInsert, Color.BLACK);
+ StyleConstants.setBackground(keyWordInsert, Color.GREEN);
+
+ docA.insertString(docA.getLength(), sbA.toString(), keyWordInsert);
+ docB.insertString(docB.getLength(), sbB.toString(), keyWordInsert);
+
+ break;
+ }
+ }
+ } catch (Exception e) {
+ System.out.println("Error generating diff: " + e.getMessage());
+ e.printStackTrace();
+ }
+
+ }
+
+
+
+ panelA.add(vboxA);
+ panelB.add(vboxB);
+
+ mainPanel.add(panelA);
+ mainPanel.add(panelB);
+ revalidate();
+ }
+
+}
+
diff --git a/src/main/java/com/itextpdf/rups/view/diff/PdfDiffModifier.java b/src/main/java/com/itextpdf/rups/view/diff/PdfDiffModifier.java
new file mode 100644
index 00000000..d32c583c
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/view/diff/PdfDiffModifier.java
@@ -0,0 +1,88 @@
+package com.itextpdf.rups.view.diff;
+
+import com.itextpdf.io.source.ByteArrayOutputStream;
+
+import java.io.IOException;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+
+public class PdfDiffModifier {
+
+ private final byte[] data;
+
+ private int position = 0;
+
+ public PdfDiffModifier(byte[] data) {
+ this.data = data;
+ }
+
+ public static byte[] decompress(byte[] input) throws DataFormatException {
+ Inflater inflater = new Inflater();
+ inflater.setInput(input);
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+
+ while (!inflater.finished()) {
+ int decompressedSize = inflater.inflate(buffer);
+ outputStream.write(buffer, 0, decompressedSize);
+ }
+
+ return outputStream.toByteArray();
+ }
+
+
+ public ByteArrayOutputStream next() throws DataFormatException, IOException {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ while (position < data.length) {
+ if (peekNextCharIsCharArray(0, "stream".toCharArray())) {
+ position += "stream\n".length();
+ int streamSize = findStartLocationOfNextOccurance("\nendstream\n".toCharArray());
+ result.write("stream\n".getBytes());
+ byte[] streamData = new byte[streamSize];
+ for (int i = 0; i < streamSize; i++) {
+ streamData[i] = data[position + i];
+ }
+ byte[] decompressed = decompress(streamData);
+ result.write(decompressed);
+ position += streamSize;
+ position += "\nendstream\n".length();
+ result.write("\nendstream\n".getBytes());
+
+ } else {
+ result.write(data[position]);
+ position++;
+ }
+ }
+ return result;
+ }
+
+ private boolean peekNextCharIsCharArray(int offset, char[] needle) {
+ for (int i = 0; i < needle.length; i++) {
+ if (data[position + offset + i] != needle[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private int findStartLocationOfNextOccurance(char[] needle) {
+ for (int i = position; i < data.length; i++) {
+ if (data[i] == needle[0]) {
+ boolean found = true;
+ for (int j = 1; j < needle.length; j++) {
+ if (data[i + j] != needle[j]) {
+ found = false;
+ break;
+ }
+ }
+ if (found) {
+ return i - position;
+ }
+ }
+ }
+ return -1;
+
+ }
+}
+
diff --git a/src/main/java/com/itextpdf/rups/view/diff/ViewerOptions.java b/src/main/java/com/itextpdf/rups/view/diff/ViewerOptions.java
new file mode 100644
index 00000000..0bfb3041
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/view/diff/ViewerOptions.java
@@ -0,0 +1,22 @@
+package com.itextpdf.rups.view.diff;
+
+import java.io.File;
+
+public class ViewerOptions {
+ public boolean decompress;
+ public boolean ignoreXrefTable;
+ public boolean ignoreStreams;
+ public File fileA;
+ public File fileB;
+
+ @Override
+ public String toString() {
+ return "ViewerOptions{" +
+ "decompress=" + decompress +
+ ", ignoreXrefTable=" + ignoreXrefTable +
+ ", ignoreStreams=" + ignoreStreams +
+ ", fileA=" + fileA +
+ ", fileB=" + fileB +
+ '}';
+ }
+}
diff --git a/src/main/resources/cmp_simpleParagraphTest.pdf b/src/main/resources/cmp_simpleParagraphTest.pdf
new file mode 100644
index 00000000..ef52fb5a
Binary files /dev/null and b/src/main/resources/cmp_simpleParagraphTest.pdf differ
diff --git a/src/main/resources/simpleParagraphTest.pdf b/src/main/resources/simpleParagraphTest.pdf
new file mode 100644
index 00000000..15ff54f2
Binary files /dev/null and b/src/main/resources/simpleParagraphTest.pdf differ
diff --git a/src/test/java/com/itextpdf/rups/mock/MockedRupsController.java b/src/test/java/com/itextpdf/rups/mock/MockedRupsController.java
index 2bd53af4..344a50d2 100644
--- a/src/test/java/com/itextpdf/rups/mock/MockedRupsController.java
+++ b/src/test/java/com/itextpdf/rups/mock/MockedRupsController.java
@@ -93,4 +93,9 @@ public int getOpenedCount() {
@Override
public void reopenAsOwner() {
}
+
+ @Override
+ public void openDiffViewer(File fileA, File fileB) {
+
+ }
}
diff --git a/src/test/java/com/itextpdf/rups/view/diff/PdfDiffModifierTest.java b/src/test/java/com/itextpdf/rups/view/diff/PdfDiffModifierTest.java
new file mode 100644
index 00000000..f775e08b
--- /dev/null
+++ b/src/test/java/com/itextpdf/rups/view/diff/PdfDiffModifierTest.java
@@ -0,0 +1,23 @@
+package com.itextpdf.rups.view.diff;
+
+import com.itextpdf.io.source.ByteArrayOutputStream;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.zip.DataFormatException;
+
+public class PdfDiffModifierTest {
+
+ @Test
+ public void testSimple() throws IOException, DataFormatException {
+ Path p = Path.of("src/main/resources/simpleParagraphTest.pdf");
+ byte[] data = Files.readAllBytes(p);
+ PdfDiffModifier tokenizer = new PdfDiffModifier(data);
+ ByteArrayOutputStream baos = tokenizer.next();
+ System.out.println(new String(baos.toByteArray(), StandardCharsets.US_ASCII));
+ }
+
+}
\ No newline at end of file