All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.redsix.pdfcompare.ui.Display Maven / Gradle / Ivy

Go to download

A simple Java library to compare two PDF files. Files are rendered and compared pixel by pixel.

The newest version!
package de.redsix.pdfcompare.ui;

import de.redsix.pdfcompare.CompareResultWithExpectedAndActual;
import de.redsix.pdfcompare.Exclusions;
import de.redsix.pdfcompare.PageArea;
import de.redsix.pdfcompare.PdfComparator;
import de.redsix.pdfcompare.cli.CliArguments;
import de.redsix.pdfcompare.env.DefaultEnvironment;
import de.redsix.pdfcompare.env.Environment;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class Display {

    private final JFrame frame = new JFrame();
    private ViewModel viewModel = new ViewModel(new CompareResultWithExpectedAndActual());
    private final ImagePanel leftPanel = new ImagePanel(viewModel.getLeftImage());
    private final ImagePanel resultPanel = new ImagePanel(viewModel.getDiffImage());
    private final JToggleButton expectedButton = new JToggleButton("Expected");
    private ExclusionsPanel exclusionsPanel;
    private boolean showExclusions = false;
    private PageArea dragArea;
    private final Environment environment = DefaultEnvironment.create();
    private final Exclusions exclusions = new Exclusions(environment);

    public void init(CliArguments cliArguments) {
        init();
        if (cliArguments.hasFileArguments()) {
            try {
                cliArguments.getExclusionsFile().ifPresent(ef -> {
                    exclusions.readExclusions(ef);
//                    exclusionsPanel.setSelectedFile(new File(ef));
                    exclusionsPanel.openExclusionFile(new File(ef));
                });
                openFiles(new File(cliArguments.getExpectedFile().get()), cliArguments.getExpectedPassword(),
                        new File(cliArguments.getActualFile().get()), cliArguments.getActualPassword(), cliArguments.getExclusionsFile());
            } catch (IOException ex) {
                displayExceptionDialog(frame, ex);
            }
        }
    }

    public void init() {
        final String title = "PDF Compare Studio";
        frame.setTitle(title);
        List icons = new ArrayList<>();
        icons.add(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/pdfcomparestudio16.png")));
        icons.add(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/pdfcomparestudio40.png")));
        frame.setIconImages(icons);
        frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        final BorderLayout borderLayout = new BorderLayout();
        frame.setLayout(borderLayout);
        frame.setMinimumSize(new Dimension(400, 200));
        final Rectangle screenBounds = getDefaultScreenBounds();
        frame.setSize(Math.min(screenBounds.width, 1700), Math.min(screenBounds.height, 1000));
        frame.setLocation(screenBounds.x, screenBounds.y);

        JToolBar toolBar = new JToolBar();
        toolBar.setRollover(true);
        toolBar.setFloatable(false);
        frame.add(toolBar, BorderLayout.PAGE_START);

        JScrollPane expectedScrollPane = new JScrollPane(leftPanel);
        expectedScrollPane.setMinimumSize(new Dimension(200, 200));
        JScrollPane actualScrollPane = new JScrollPane(resultPanel);
        actualScrollPane.setMinimumSize(new Dimension(200, 200));
        actualScrollPane.getViewport().addComponentListener(new ComponentAdapter() {

            @Override
            public void componentResized(final ComponentEvent e) {
                resultPanel.setViewSize(e.getComponent().getSize());
                super.componentResized(e);
            }
        });

        expectedScrollPane.getVerticalScrollBar().setModel(actualScrollPane.getVerticalScrollBar().getModel());
        expectedScrollPane.getHorizontalScrollBar().setModel(actualScrollPane.getHorizontalScrollBar().getModel());
        expectedScrollPane.getViewport().addComponentListener(new ComponentAdapter() {

            @Override
            public void componentResized(final ComponentEvent e) {
                leftPanel.setViewSize(e.getComponent().getSize());
                super.componentResized(e);
            }
        });

        final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, expectedScrollPane, actualScrollPane);
        splitPane.setResizeWeight(0.5);
        splitPane.setDividerLocation(0.5);
        splitPane.setOneTouchExpandable(true);
        frame.add(splitPane, BorderLayout.CENTER);

        exclusionsPanel = new ExclusionsPanel(this, environment);
        // panel is hidden until toggled
        exclusionsPanel.setVisible(showExclusions);
        frame.add(exclusionsPanel, BorderLayout.EAST);

        addToolBarButton(toolBar, "Open...", event -> {
            JFileChooser fileChooser = new JFileChooser();
            try {
                if (fileChooser.showDialog(frame, "Open expected PDF") == JFileChooser.APPROVE_OPTION) {
                    final File expectedFile = fileChooser.getSelectedFile();
                    final Optional passwordForExpectedFile = askForPasswordIfNeeded(expectedFile);

                    if (fileChooser.showDialog(frame, "Open actual PDF") == JFileChooser.APPROVE_OPTION) {
                        final File actualFile = fileChooser.getSelectedFile();
                        final Optional passwordForActualFile = askForPasswordIfNeeded(actualFile);

                        openFiles(expectedFile, passwordForExpectedFile, actualFile, passwordForActualFile, Optional.empty());
                    }
                }
            } catch (IOException ex) {
                displayExceptionDialog(frame, ex);
            }
        });

        toolBar.addSeparator();

        addToolBarButton(toolBar, "Page -", event -> {
            if (viewModel.decreasePage()) {
                leftPanel.setImage(applyExclusions(viewModel.getLeftImage()));
                resultPanel.setImage(applyExclusions(viewModel.getDiffImage()));
            }
        });

        addToolBarButton(toolBar, "Page +", event -> {
            if (viewModel.increasePage()) {
                leftPanel.setImage(applyExclusions(viewModel.getLeftImage()));
                resultPanel.setImage(applyExclusions(viewModel.getDiffImage()));
            }
        });

        toolBar.addSeparator();

        final JToggleButton pageZoomButton = new JToggleButton("Zoom Page");
        pageZoomButton.setSelected(true);
        pageZoomButton.setFocusable(false);
        pageZoomButton.addActionListener(event -> {
            leftPanel.zoomPage();
            resultPanel.zoomPage();
        });

        addToolBarButton(toolBar, "Zoom -", event -> {
            pageZoomButton.setSelected(false);
            leftPanel.decreaseZoom();
            resultPanel.decreaseZoom();
        });

        addToolBarButton(toolBar, "Zoom +", event -> {
            pageZoomButton.setSelected(false);
            leftPanel.increaseZoom();
            resultPanel.increaseZoom();
        });

        toolBar.add(pageZoomButton);

        addToolBarButton(toolBar, "Zoom 100%", event -> {
            pageZoomButton.setSelected(false);
            leftPanel.zoom100();
            resultPanel.zoom100();
        });

        toolBar.addSeparator();

        addToolBarButton(toolBar, "Center Split", event -> {
            splitPane.setDividerLocation(0.5);
            splitPane.revalidate();
        });

        toolBar.addSeparator();

        final ButtonGroup buttonGroup = new ButtonGroup();
        expectedButton.setSelected(true);
        expectedButton.setFocusable(false);
        expectedButton.addActionListener(event -> {
            viewModel.showExpected();
            leftPanel.setImage(applyExclusions(viewModel.getLeftImage()));
        });
        toolBar.add(expectedButton);
        buttonGroup.add(expectedButton);

        final JToggleButton actualButton = new JToggleButton("Actual");
        actualButton.setFocusable(false);
        actualButton.addActionListener(event -> {
            viewModel.showActual();
            leftPanel.setImage(applyExclusions(viewModel.getLeftImage()));
        });
        toolBar.add(actualButton);
        buttonGroup.add(actualButton);

        toolBar.addSeparator();

        final MouseAdapter mouseListener = new MouseAdapter() {
            private boolean dragging = false;
            private Point startPoint;

            @Override
            public void mouseClicked(MouseEvent e) {
                // pick selected area by clicking on it
                double zoom = leftPanel.getZoomFactor();
                ExclusionItemPanel item = exclusionsPanel.getItemAt(viewModel.getPageToShow() + 1,
                        (int) (e.getX() / zoom),
                        (int) (e.getY() / zoom));
                if (item != null) {
                    exclusionsPanel.setSelectedItem(item);
                }
            }

            @Override
            public void mousePressed(MouseEvent e) {
                startPoint = e.getPoint();
                dragging = true;
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                if (dragging) {
                    Point endPoint = e.getPoint();

                    double d = Point2D.distance(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
                    // assuming user didn't drag if less than 8 pixels
                    if (d > 7) {
                        int pageNumber = viewModel.getPageToShow();
                        PageArea pageArea = getPageArea(pageNumber, endPoint);
                        exclusionsPanel.addExclusion(pageArea);
                        ExclusionItemPanel item = exclusionsPanel.getItemForArea(pageArea);
                        exclusionsPanel.setSelectedItem(item);
                    }

                    dragging = false;
                    startPoint = null;
                    dragArea = null;

                    redrawImages();
                }
            }

            @Override
            public void mouseDragged(MouseEvent e) {
                if (dragging) {
                    int pageNumber = viewModel.getPageToShow();
                    Point endPoint = e.getPoint();

                    dragArea = getPageArea(pageNumber, endPoint);

                    redrawImages();
                }
            }

            private PageArea getPageArea(int pageNumber, Point endPoint) {
                double zoom = leftPanel.getZoomFactor();

                int x1 = (int) Math.min(startPoint.x / zoom, endPoint.x / zoom);
                int y1 = (int) Math.min(startPoint.y / zoom, endPoint.y / zoom);
                int x2 = (int) Math.max(startPoint.x / zoom, endPoint.x / zoom);
                int y2 = (int) Math.max(startPoint.y / zoom, endPoint.y / zoom);

                return new PageArea(pageNumber + 1, x1, y1, x2, y2);
            }
        };

        // zoom using the mouse wheel
        final MouseWheelListener mouseWheelListener = mouseWheelEvent -> {
            BoundedRangeModel horizontalModel = actualScrollPane.getHorizontalScrollBar().getModel();
            BoundedRangeModel verticalModel = actualScrollPane.getVerticalScrollBar().getModel();
            double horizontalOffset = horizontalModel.getValue();
            double verticalOffset = verticalModel.getValue();
            double horizontalExtent = horizontalModel.getExtent();
            double verticalExtent = verticalModel.getExtent();
            double zoomBefore = leftPanel.getZoomFactor();

            if (mouseWheelEvent.getWheelRotation() > 0) {
                leftPanel.decreaseZoom();
                resultPanel.decreaseZoom();

                // keep visible area centered on zooming out
                double zoomAfter = leftPanel.getZoomFactor();
                int horizontalValue = (int) ((horizontalOffset + horizontalExtent / 2) * zoomAfter / zoomBefore - horizontalExtent / 2);
                int verticalValue = (int) ((verticalOffset + verticalExtent / 2) * zoomAfter / zoomBefore - verticalExtent / 2);
                horizontalModel.setValue(horizontalValue);
                verticalModel.setValue(verticalValue);

            } else {
                leftPanel.increaseZoom();
                resultPanel.increaseZoom();

                // keep visible area centered on mouse cursor
                Point mousePoint = mouseWheelEvent.getPoint();
                double zoomAfter = leftPanel.getZoomFactor();
                int horizontalValue = (int) (mousePoint.x * zoomAfter / zoomBefore - horizontalExtent / 2);
                int verticalValue = (int) (mousePoint.y * zoomAfter / zoomBefore - verticalExtent / 2);

                // FIXME: this is a workaround because the zoom isn't applied to the scrollbars immediately and scroll maximum might be too small at this time.
                SwingUtilities.invokeLater(() -> {
                    horizontalModel.setValue(horizontalValue);
                    verticalModel.setValue(verticalValue);
                });
            }
        };

        KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(keyEvent -> {
            if (keyEvent.getKeyCode() == KeyEvent.VK_CONTROL) {
                if (keyEvent.getID() == KeyEvent.KEY_PRESSED) {
                    leftPanel.addMouseWheelListener(mouseWheelListener);
                    resultPanel.addMouseWheelListener(mouseWheelListener);
                } else if (keyEvent.getID() == KeyEvent.KEY_RELEASED) {
                    leftPanel.removeMouseWheelListener(mouseWheelListener);
                    resultPanel.removeMouseWheelListener(mouseWheelListener);
                }
            }
            return false;
        });

        final JToggleButton exclusionMode = new JToggleButton("Exclusions");
        exclusionMode.setFocusable(false);
        exclusionMode.addActionListener(event -> {
            if (exclusionMode.isSelected()) {
                leftPanel.addMouseListener(mouseListener);
                leftPanel.addMouseMotionListener(mouseListener);
                resultPanel.addMouseListener(mouseListener);
                resultPanel.addMouseMotionListener(mouseListener);
                // Cross mouse pointer shows active XY mode and makes positioning easier.
                leftPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
                showExclusions = true;
            } else {
                leftPanel.removeMouseListener(mouseListener);
                leftPanel.removeMouseMotionListener(mouseListener);
                resultPanel.removeMouseListener(mouseListener);
                resultPanel.removeMouseMotionListener(mouseListener);
                leftPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                showExclusions = false;
            }
            exclusionsPanel.setVisible(showExclusions);
            redrawImages();
            frame.setTitle(title);
        });
        toolBar.add(exclusionMode);

        frame.setVisible(true);
    }

    private void openFiles(File expectedFile, Optional passwordForExpectedFile, File actualFile, Optional passwordForActualFile,
            Optional exclusions)
            throws IOException {
        frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        try {
            PdfComparator pdfComparator
                    = new PdfComparator<>(expectedFile, actualFile, new CompareResultWithExpectedAndActual());
            passwordForExpectedFile.ifPresent(pdfComparator::withExpectedPassword);
            passwordForActualFile.ifPresent(pdfComparator::withActualPassword);
            exclusions.ifPresent(pdfComparator::withIgnore);
            final CompareResultWithExpectedAndActual compareResult = pdfComparator.compare();

            viewModel = new ViewModel(compareResult);
//            exclusionsPanel.useDifferencesFromCompare(compareResult.getDifferences());
            leftPanel.setImage(applyExclusions(viewModel.getLeftImage()));
            resultPanel.setImage(applyExclusions(viewModel.getDiffImage()));

            if (compareResult.isEqual()) {
                JOptionPane.showMessageDialog(frame, "The compared documents are identical.");
            }

            expectedButton.setSelected(true);
        } finally {
            frame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        }
    }

    static void displayExceptionDialog(final JFrame frame, final IOException ex) {
        final StringWriter stringWriter = new StringWriter();
        ex.printStackTrace(new PrintWriter(stringWriter));
        JTextArea textArea = new JTextArea(
                "An unexpected error has occurred: " + ex.getMessage() + "\n\n" + stringWriter);
        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setPreferredSize(new Dimension(900, 700));
        JOptionPane.showMessageDialog(frame, scrollPane);
    }

    private static void addToolBarButton(final JToolBar toolBar, final String label, final ActionListener actionListener) {
        final JButton button = new JButton(label);
        button.setFocusable(false);
        button.addActionListener(actionListener);
        toolBar.add(button);
    }

    private static Rectangle getDefaultScreenBounds() {
        return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().getBounds();
    }

    private static Optional askForPasswordIfNeeded(final File file) throws IOException {
        if (!isInvalidPassword(file, "")) {
            return Optional.empty();
        }

        JPasswordField passwordField = new JPasswordField(20);
        final JLabel label = new JLabel("Enter password: ");
        label.setLabelFor(passwordField);

        final JPanel textPane = new JPanel(new FlowLayout(FlowLayout.TRAILING));
        textPane.add(label);
        textPane.add(passwordField);

        JOptionPane.showMessageDialog(
                null,
                textPane,
                "PDF is encrypted",
                JOptionPane.INFORMATION_MESSAGE);

        label.setText("Password was invalid. Enter password: ");
        while (isInvalidPassword(file, String.valueOf(passwordField.getPassword()))) {
            passwordField.setText("");
            JOptionPane.showMessageDialog(
                    null,
                    textPane,
                    "PDF is encrypted",
                    JOptionPane.ERROR_MESSAGE);
        }
        return Optional.of(String.valueOf(passwordField.getPassword()));
    }

    private static boolean isInvalidPassword(final File file, final String password) throws IOException {
        try {
            Loader.loadPDF(file, password).close();
        } catch (InvalidPasswordException e) {
            return true;
        }
        return false;
    }

    public void redrawImages() {
        leftPanel.setImage(applyExclusions(viewModel.getLeftImage()));
        resultPanel.setImage(applyExclusions(viewModel.getDiffImage()));
    }

    /**
     * switches to the page and scrolls to show the area.
     */
    public void showPageArea(PageArea pageArea) {
        int pageNo = pageArea.getPage();
        viewModel.setPageToShow(pageNo - 1);
        redrawImages();

        if (pageArea.getX1() != -1) {
            double zoom = leftPanel.getZoomFactor();

            Rectangle rect = new Rectangle(
                    (int) (pageArea.getX1() * zoom),
                    (int) (pageArea.getY1() * zoom),
                    (int) ((pageArea.getX2() - pageArea.getX1()) * zoom),
                    (int) ((pageArea.getY2() - pageArea.getY1()) * zoom)
            );

            leftPanel.scrollRectToVisible(rect);
            frame.repaint();
        }
    }

    static private final Color SHADE_BORDER = new Color(0x708090);
    static private final Color SHADE = new Color(0x70606000, true);
    static private final Color SHADE_HIGHLIGHT = new Color(0xA0A0A0FF, true);

    /**
     * paints shaded areas over defined exclusions
     */
    public BufferedImage applyExclusions(BufferedImage image) {
        if (image == null) {
            return null;
        }
        if (!showExclusions) {
            return image;
        }

        int pageNo = viewModel.getPageToShow();

        BufferedImage bufferedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
        Graphics g = bufferedImage.getGraphics();

        g.drawImage(image, 0, 0, null);

        exclusionsPanel.getExclusions().forPage(pageNo + 1).getExclusions().forEach(pa -> {
//            if (pa.getPage() != -1 && pa.getPage() != pageNo + 1) {
//                // filter by page.
//                // NOTE: forPage doesn't work correctly
//                return;
//            }

            if (!pa.hasCoordinates()) {
                // full page
                g.setColor(exclusionsPanel.isSelected(pa) ? SHADE_HIGHLIGHT : SHADE);
                g.fillRect(0, 0, image.getWidth(), image.getHeight());
                g.setColor(SHADE_BORDER);
                g.drawRect(0, 0, image.getWidth(), image.getHeight());
            } else {
                g.setColor(exclusionsPanel.isSelected(pa) ? SHADE_HIGHLIGHT : SHADE);
                g.fillRect(pa.getX1(), pa.getY1(), pa.getX2() - pa.getX1(), pa.getY2() - pa.getY1());
                g.setColor(SHADE_BORDER);
                g.drawRect(pa.getX1(), pa.getY1(), pa.getX2() - pa.getX1(), pa.getY2() - pa.getY1());

                if (dragArea == null && exclusionsPanel.isSelected(pa)) {
                    // draw scan lines for the selected area
                    ((Graphics2D) g).setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[]{10, 20}, 5));
                    g.drawLine(0, pa.getY1(), image.getWidth(), pa.getY1());
                    g.drawLine(0, pa.getY2(), image.getWidth(), pa.getY2());
                    g.drawLine(pa.getX1(), 0, pa.getX1(), image.getHeight());
                    g.drawLine(pa.getX2(), 0, pa.getX2(), image.getHeight());
                    ((Graphics2D) g).setStroke(new BasicStroke());
                }
            }

        });

        // this is the overlay for mouse drawing a rectangle
        if (dragArea != null) {
            g.setColor(SHADE_HIGHLIGHT);
            g.fillRect(dragArea.getX1(), dragArea.getY1(), dragArea.getX2() - dragArea.getX1(), dragArea.getY2() - dragArea.getY1());
            g.setColor(SHADE_BORDER);
            g.drawRect(dragArea.getX1(), dragArea.getY1(), dragArea.getX2() - dragArea.getX1(), dragArea.getY2() - dragArea.getY1());

            // draw scan lines for the area
            ((Graphics2D) g).setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[]{10, 20}, 5));
            g.drawLine(0, dragArea.getY1(), image.getWidth(), dragArea.getY1());
            g.drawLine(0, dragArea.getY2(), image.getWidth(), dragArea.getY2());
            g.drawLine(dragArea.getX1(), 0, dragArea.getX1(), image.getHeight());
            g.drawLine(dragArea.getX2(), 0, dragArea.getX2(), image.getHeight());
            ((Graphics2D) g).setStroke(new BasicStroke());
        }

        return bufferedImage;
    }

    /**
     * @return current page number, 1 based
     */
    public int getPageNumber() {
        return viewModel.getPageToShow() + 1;
    }

    public Exclusions getExclusions() {
        return exclusions;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy