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