diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/PositioningComposite.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/PositioningComposite.java index d998c996..d9125e99 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/PositioningComposite.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/PositioningComposite.java @@ -16,24 +16,18 @@ package at.asit.pdfover.gui.composites; // Imports -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.EventQueue; -import java.awt.Frame; -import java.awt.Image; -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; import java.awt.geom.Point2D; import org.apache.pdfbox.pdmodel.PDDocument; import org.eclipse.swt.SWT; -import org.eclipse.swt.awt.SWT_AWT; +import org.eclipse.swt.events.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.ScrollBar; @@ -41,7 +35,6 @@ import at.asit.pdfover.gui.utils.SWTUtils; import at.asit.pdfover.gui.workflow.states.State; import at.asit.pdfover.signer.SignaturePosition; -import lombok.extern.slf4j.Slf4j; /** * Composite which allows to position the signature on a preview of the document @@ -49,12 +42,14 @@ public class PositioningComposite extends StateComposite { SignaturePanel viewer = null; - Frame frame = null; Composite mainArea = null; Composite bottomBar = null; Button btnNewPage = null; Label lblPage = null; ScrollBar scrollbar = null; + private final Display display; + private static final int MOUSE_WHEEL_PAGE_THRESHOLD = 3; + private int mouseWheelAccumulator = 0; private SignaturePosition position = null; int currentPage = 0; @@ -69,6 +64,7 @@ public class PositioningComposite extends StateComposite { */ public PositioningComposite(Composite parent, int style, State state) { super(parent, style, state); + this.display = getDisplay(); this.setLayout(new FormLayout()); this.bottomBar = new Composite(this, SWT.NONE); @@ -85,20 +81,20 @@ public PositioningComposite(Composite parent, int style, State state) { this.lblPage = new Label(this.bottomBar, SWT.CENTER); SWTUtils.anchor(lblPage).left(0).right(btnNewPage, 5).bottom(100); - this.mainArea = new Composite(this, SWT.EMBEDDED | SWT.V_SCROLL); + this.mainArea = new Composite(this, SWT.V_SCROLL); SWTUtils.anchor(mainArea).left(0).right(100).top(0).bottom(bottomBar, -5); + this.mainArea.setLayout(new FormLayout()); this.scrollbar = this.mainArea.getVerticalBar(); - this.frame = SWT_AWT.new_Frame(this.mainArea); - this.frame.addKeyListener(this.keyListener); - this.frame.addMouseWheelListener(this.mouseListener); - - this.viewer = new SignaturePanel(); - this.viewer.setSignaturePlaceholderBorderColor(new Color( - Constants.MAINBAR_ACTIVE_BACK_DARK.getRed(), - Constants.MAINBAR_ACTIVE_BACK_DARK.getGreen(), - Constants.MAINBAR_ACTIVE_BACK_DARK.getBlue())); - this.frame.add(this.viewer, BorderLayout.CENTER); + this.viewer = new SignaturePanel(this.mainArea, SWT.NONE); + SWTUtils.anchor(viewer).left(0).right(100).top(0).bottom(100); + this.viewer.setSignaturePlaceholderBorderColor(Constants.MAINBAR_ACTIVE_BACK_DARK); + this.viewer.setSignaturePositionAvailableCallback(() -> { + if (!isDisposed() && !this.btnSign.isDisposed()) + this.btnSign.setEnabled(true); + }); + this.viewer.addKeyListener(this.keyListener); + this.viewer.addListener(SWT.MouseVerticalWheel, this::handleMouseWheel); SWTUtils.addSelectionListener(btnNewPage, () -> { if (this.currentPage > this.numPages) @@ -125,10 +121,7 @@ public PositioningComposite(Composite parent, int style, State state) { * document to display */ public void displayDocument(final PDDocument document) { - EventQueue.invokeLater(() -> { - this.viewer.setDocument(document); - getDisplay().asyncExec(() -> this.btnSign.setEnabled(true)); - }); + this.viewer.setDocument(document); if (document != null) { @@ -138,9 +131,14 @@ public void displayDocument(final PDDocument document) { } } + public void clearDocument() { + if (this.viewer != null && !this.viewer.isDisposed()) + this.viewer.setDocument(null); + } + @Override public void dispose() { - this.viewer.setDocument(null); + clearDocument(); super.dispose(); } @@ -148,16 +146,13 @@ public void dispose() { * Request focus (to enable keyboard input) */ public void requestFocus() { - getDisplay().asyncExec(() -> { + if (this.display.isDisposed()) + return; + this.display.asyncExec(() -> { if (!this.isDisposed() && !this.mainArea.isDisposed()) { this.mainArea.setFocus(); - EventQueue.invokeLater(() -> { - if (!this.isDisposed()) { - if (!this.frame.hasFocus()) { - this.frame.requestFocus(); - } - } - }); + if (!this.viewer.isDisposed()) + this.viewer.setFocus(); } }); } @@ -173,61 +168,63 @@ public void requestFocus() { * @param height * height of the placeholder in page space */ - public void setPlaceholder(final Image placeholder) { - EventQueue.invokeLater(() -> { + public void setPlaceholder(final ImageData placeholder) { + if (this.display.isDisposed()) + return; + this.display.asyncExec(() -> { + if (isDisposed()) + return; if (this.viewer == null) return; this.viewer.setSignaturePlaceholder(placeholder); }); } - KeyListener keyListener = new KeyAdapter() { + private KeyAdapter keyListener = new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { int newPage = PositioningComposite.this.currentPage; int sigXOffset = 0; int sigYOffset = 0; - switch (e.getKeyCode()) { - case KeyEvent.VK_PAGE_DOWN: + switch (e.keyCode) { + case SWT.PAGE_DOWN: if (PositioningComposite.this.currentPage < PositioningComposite.this.numPages) ++newPage; break; - case KeyEvent.VK_PAGE_UP: + case SWT.PAGE_UP: if (PositioningComposite.this.currentPage > 1) --newPage; break; - case KeyEvent.VK_END: + case SWT.END: newPage = PositioningComposite.this.numPages; break; - case KeyEvent.VK_HOME: + case SWT.HOME: newPage = 1; break; - case KeyEvent.VK_ENTER: + case SWT.CR: + case SWT.KEYPAD_CR: + e.doit = false; setFinalPosition(); break; - case KeyEvent.VK_LEFT: - case KeyEvent.VK_KP_LEFT: + case SWT.ARROW_LEFT: sigXOffset -= Constants.SIGNATURE_KEYBOARD_POSITIONING_OFFSET; break; - case KeyEvent.VK_RIGHT: - case KeyEvent.VK_KP_RIGHT: + case SWT.ARROW_RIGHT: sigXOffset += Constants.SIGNATURE_KEYBOARD_POSITIONING_OFFSET; break; - case KeyEvent.VK_UP: - case KeyEvent.VK_KP_UP: + case SWT.ARROW_UP: sigYOffset += Constants.SIGNATURE_KEYBOARD_POSITIONING_OFFSET; break; - case KeyEvent.VK_DOWN: - case KeyEvent.VK_KP_DOWN: + case SWT.ARROW_DOWN: sigYOffset -= Constants.SIGNATURE_KEYBOARD_POSITIONING_OFFSET; break; } @@ -240,37 +237,33 @@ public void keyPressed(KeyEvent e) { } }; - MouseWheelListener mouseListener = new MouseWheelListener() { - private long lastEventTime = 0; + private void handleMouseWheel(Event e) { + e.doit = false; - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - e.consume(); - // Workaround for Linux: Events fire twice - if (e.getWhen() == this.lastEventTime) - return; - this.lastEventTime = e.getWhen(); - - int change = e.isShiftDown() ? 5 : 1; - int newPage = PositioningComposite.this.currentPage; + int change = (e.stateMask & SWT.SHIFT) != 0 ? 5 : 1; + int newPage = PositioningComposite.this.currentPage; + this.mouseWheelAccumulator += e.count; - if (e.getWheelRotation() < 0) { - newPage = Math.max(1, newPage - change); - } else if (e.getWheelRotation() > 0) { - newPage = Math.min(newPage + change, PositioningComposite.this.numPages); - } - - if (newPage != PositioningComposite.this.currentPage) - showPage(newPage); + if (this.mouseWheelAccumulator >= MOUSE_WHEEL_PAGE_THRESHOLD) { + newPage = Math.max(1, newPage - change); + this.mouseWheelAccumulator = 0; + } else if (this.mouseWheelAccumulator <= -MOUSE_WHEEL_PAGE_THRESHOLD) { + newPage = Math.min(newPage + change, PositioningComposite.this.numPages); + this.mouseWheelAccumulator = 0; } - }; + + if (newPage != PositioningComposite.this.currentPage) + showPage(newPage); + } private Button btnSign; void showPage(final int page) { final int previousPage = this.currentPage; this.currentPage = page; - getDisplay().asyncExec(() -> { + if (this.display.isDisposed()) + return; + this.display.asyncExec(() -> { int currentPage = this.currentPage; int numPages = this.numPages; if ((previousPage > numPages) && (currentPage <= numPages)) { @@ -289,9 +282,7 @@ void showPage(final int page) { this.scrollbar.setSelection(currentPage); SWTUtils.setLocalizedText(lblPage, "positioning.page", currentPage, numPages); }); - EventQueue.invokeLater(() -> { - PositioningComposite.this.viewer.showPage(page); - }); + PositioningComposite.this.viewer.showPage(page); } /** @@ -303,9 +294,7 @@ void showPage(final int page) { * signature placeholder vertical position offset (negative is down) */ public void translateSignaturePosition(final int sigXOffset, final int sigYOffset) { - EventQueue.invokeLater(() -> { - this.viewer.translateSignaturePagePosition(sigXOffset, sigYOffset); - }); + this.viewer.translateSignaturePagePosition(sigXOffset, sigYOffset); } /** diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/SignaturePanel.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/SignaturePanel.java index fc47a386..6eb800b8 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/SignaturePanel.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/SignaturePanel.java @@ -1,478 +1,528 @@ -/* - * Copyright 2012 by A-SIT, Secure Information Technology Center Austria - * - * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the "Licence"); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * http://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - */ -package at.asit.pdfover.gui.composites; - -// Imports -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Cursor; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Image; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; -import java.awt.image.BufferedImage; -import java.io.IOException; - -import javax.swing.JPanel; - -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.rendering.PDFRenderer; - -import at.asit.pdfover.commons.Messages; -import lombok.extern.slf4j.Slf4j; - -/** - * - */ -@Slf4j -public class SignaturePanel extends JPanel { - - /** Default serial version ID */ - private static final long serialVersionUID = 1L; - - /** The PDF file being displayed */ - private PDDocument pdf = null; - private PDFRenderer renderer = null; - - /** The image of the rendered PDF page being displayed */ - Image currentImage = null; - - /** The current scale for rendering pdf to image */ - private float pageToImageScale; - /** The current scale for rendering image to screen */ - private double imageToScreenScale; - /* scaling */ - private enum U { - /* (0,0) is bottom-left of page */ - PAGE_ABS, - /* (0,0) is top-left of page */ - PAGE_REL, - /* pixels, (0,0) is top-left of image */ - IMAGE, - /* pixels, (0,0) is top-left of image */ - SCREEN_REL, - /* pixels, (0,0) is top-left of canvas */ - SCREEN_ABS }; - private enum Dim { X, Y }; - private double scale(double v, U from, U to, Dim d) - { - if (from == to) return v; - - if (from == U.PAGE_ABS) { - return scale((d == Dim.X) ? v : (this.pageHeight - v), U.PAGE_REL, to, d); - } else if (from == U.PAGE_REL) { - if (to == U.PAGE_ABS) - return ((d == Dim.X) ? v : (this.pageHeight - v)); - else - return scale(v * this.pageToImageScale, U.IMAGE, to, d); - } else if (from == U.IMAGE) { - if ((to == U.PAGE_ABS) || (to == U.PAGE_REL)) - return scale(v / this.pageToImageScale, U.PAGE_REL, to, d); - else - return scale(v * this.imageToScreenScale, U.SCREEN_REL, to, d); - } else if (from == U.SCREEN_REL) { - if (to == U.SCREEN_ABS) - return (v + ((d == Dim.X) ? this.offX : this.offY)); - else - return scale(v / this.imageToScreenScale, U.IMAGE, to, d); - } else if (from == U.SCREEN_ABS) { - return scale(v - ((d == Dim.X) ? this.offX : this.offY), U.SCREEN_REL, to, d); - } else throw new RuntimeException("unreachable"); - } - - private float pageWidth = 0; - private float pageHeight = 0; - /** The horizontal offset of the image from the left edge of the panel */ - private int offX = 0; - /** The vertical offset of the image from the top of the panel */ - private int offY = 0; - /** The position of the top-left corner of the signature, in absolute page space */ - private Point2D sigPagePos = null; - public Point2D getSigPagePos() { return this.sigPagePos; } - /** The signature placeholder image */ - private Image sigPlaceholder = null; - /** Width of the signature placeholder in page space */ - private int sigPageWidth = 0; - /** Height of the signature placeholder in page space */ - private int sigPageHeight = 0; - /** Color of the signature placeholder border */ - private Color sigPlaceholderBorderColor = Color.BLUE; - /** Current page */ - private int currentPageNo = 0; - /** Number of pages in the document */ - private int numPages = 0; - /** Cursor types */ - private static enum Cursors {DEFAULT, HAND, MOVE}; - /** Default arrow cursor */ - private final Cursor defaultCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR); - /** Hand cursor */ - private final Cursor handCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); - /** Move cursor */ - private final Cursor moveCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR); - /** Current cursor */ - private Cursors currentCursor = Cursors.DEFAULT; - - /** - * Create a new PagePanel. - */ - public SignaturePanel() { - super(new BorderLayout()); - setDocument(null); - setFocusable(true); - addMouseListener(this.mouseListener); - addMouseMotionListener(this.mouseListener); - } - - /** - * Set a new document to be displayed - * @param pdf the PDFFile to be displayed - */ - public void setDocument(PDDocument pdf) { - this.pdf = pdf; - this.sigPagePos = null; - if (pdf != null) - { - this.renderer = new PDFRenderer(pdf); - this.numPages = pdf.getNumberOfPages(); - this.currentPageNo = -1; - showPage(this.numPages); - } - else - { - this.renderer = null; - this.currentPageNo = 0; - this.numPages = 0; - renderPageToImage(); - repaint(); - } - } - - /** - * Set the signature placeholder image - * @param placeholder signature placeholder - * @param width width of the placeholder in page space - * @param height height of the placeholder in page space - */ - public void setSignaturePlaceholder(Image placeholder) { - this.sigPlaceholder = placeholder; - // TODO figure out why this is divided by 4 (factor ported from old code) - this.sigPageWidth = placeholder.getWidth(null) / 4; - this.sigPageHeight = placeholder.getHeight(null) / 4; - renderPageToImage(); - if (this.sigPagePos != null) - setSignaturePosition(this.sigPagePos.getX(), this.sigPagePos.getY()); - repaint(); - } - - /** - * Set the color of the signature placeholder border - * @param color new signature placeholder border color - */ - public void setSignaturePlaceholderBorderColor(Color color) { - this.sigPlaceholderBorderColor = color; - } - - /** - * Change the currently displayed page - * @param page the number of the page to display - */ - public void showPage(int page) { - if (this.currentPageNo == page) return; - this.currentPageNo = page; - renderPageToImage(); - repaint(); - } - - /** - * Add and display a new page at the end of the document - * - * This page has the same dimensions as the old last page - */ - public void addNewLastPage() { - showPage(this.numPages + 1); - } - - /** - * Set the signature placeholder position - * @param x the horizontal signature position - * @param y the vertical signature position - */ - public void setSignaturePosition(double x, double y) - { - this.sigPagePos = new Point2D.Double( - clamp(x, 0, this.pageWidth - this.sigPageWidth), - clamp(y, this.sigPageHeight, this.pageHeight) - ); - repaint(); - } - - public void translateSignaturePagePosition(float dX, float dY) { - setSignaturePosition(this.sigPagePos.getX() + dX, this.sigPagePos.getY() + dY); - } - - /** - * Stop the generation of any previous page, and draw the new one. - * - * @param page the PDFPage to draw. - */ - private synchronized void renderPageToImage() { - if (this.pdf == null) - { - this.currentImage = null; - return; - } - - boolean newPage = false; - PDPage currentPage; - // set up the new page - if (this.currentPageNo > this.numPages) - { - // New last page - use old last page as template - currentPage = this.pdf.getPage(this.numPages-1); - newPage = true; - } - else - currentPage = this.pdf.getPage(this.currentPageNo-1); - - - if (currentPage == null) { - // no page - this.currentImage = null; - return; - } - - boolean isRotated = ((currentPage.getRotation()%180) == 90); - PDRectangle actualPageSize = currentPage.getBBox(); - this.pageWidth = isRotated ? actualPageSize.getHeight() : actualPageSize.getWidth(); - this.pageHeight = isRotated ? actualPageSize.getWidth() : actualPageSize.getHeight(); - this.pageToImageScale = getToolkit().getScreenSize().height / this.pageHeight; - - // get the new image - if (newPage) - { - int renderHeight = (int)(0.5 + this.scale(this.pageHeight, U.PAGE_REL, U.IMAGE, Dim.X)); - int renderWidth = (int)(0.5 + this.scale(this.pageWidth, U.PAGE_REL, U.IMAGE, Dim.Y)); - this.currentImage = new BufferedImage(renderWidth, renderHeight, BufferedImage.TYPE_INT_RGB); - Graphics g = this.currentImage.getGraphics(); - g.setColor(Color.WHITE); - g.fillRect(0, 0, renderWidth, renderHeight); - } - else - { - int whichPage = Math.min(this.currentPageNo, this.numPages); - - try { - this.currentImage = renderer.renderImage(whichPage-1, this.pageToImageScale); - } catch (IOException e) { - log.error(String.format("Failed to render image for page %d of %d", whichPage, this.numPages), e); - this.currentImage = null; - } - } - - if (this.sigPagePos == null) - { - setSignaturePosition( - this.pageWidth * .5, - this.pageHeight * .25 - ); - } - } - - /** - * Draw the image. - */ - @Override - public void paint(Graphics g) { - Dimension renderPanelSize = getSize(); - g.setColor(getBackground()); - g.fillRect(0, 0, getWidth(), getHeight()); - if (this.currentImage == null) { - g.setColor(Color.black); - g.drawString(Messages.getString("common.working"), getWidth() / 2 - 30, getHeight() / 2); - } else { - this.imageToScreenScale = Math.min( - renderPanelSize.getWidth() / this.currentImage.getWidth(null), - renderPanelSize.getHeight() / this.currentImage.getHeight(null)); - // draw the image - int actualRenderWidth = (int)(this.currentImage.getWidth(null) * this.imageToScreenScale); - int actualRenderHeight = (int)(this.currentImage.getHeight(null) * this.imageToScreenScale); - - // draw it centered within the panel - this.offX = (renderPanelSize.width - actualRenderWidth) / 2; - this.offY = (renderPanelSize.height - actualRenderHeight) / 2; - - // draw document - g.drawImage(this.currentImage, this.offX, this.offY, actualRenderWidth, actualRenderHeight, null); - - - // draw signature - int sigX = (int) this.scale(this.sigPagePos.getX(), U.PAGE_ABS, U.SCREEN_ABS, Dim.X); - int sigY = (int) this.scale(this.sigPagePos.getY(), U.PAGE_ABS, U.SCREEN_ABS, Dim.Y); - if (this.sigPlaceholder == null) { - g.setColor(Color.red); - g.drawRect(sigX, sigY, 100, 40); - } - else { - int sigScreenWidth = (int)this.scale(this.sigPageWidth, U.PAGE_REL, U.SCREEN_REL, Dim.X); - int sigScreenHeight = (int)this.scale(this.sigPageHeight, U.PAGE_REL, U.SCREEN_REL, Dim.Y); - g.drawImage(this.sigPlaceholder, sigX, sigY, sigScreenWidth, sigScreenHeight, null); - g.setColor(this.sigPlaceholderBorderColor); - g.drawRect(sigX, sigY, sigScreenWidth-1, sigScreenHeight-1); - } - } - } - - /** - * Handles notification of the fact that some part of the image changed. - * Repaints that portion. - * - * @return true if more updates are desired. - */ - @Override - public boolean imageUpdate(Image img, int infoflags, int x, int y, - int width, int height) { - if ((infoflags & (SOMEBITS | ALLBITS)) != 0) { - repaint(x + this.offX, y + this.offY, width, height); - } - return ((infoflags & (ALLBITS | ERROR | ABORT)) == 0); - } - - private MouseAdapter mouseListener = new MouseAdapter() { - - private boolean doDrag = false; - private int dragXOffset = 0; - private int dragYOffset = 0; - - private void updateSigPosDrag(MouseEvent evt) { - SignaturePanel.this.setSignaturePosition( - SignaturePanel.this.scale(evt.getX() - this.dragXOffset, U.SCREEN_ABS, U.PAGE_ABS, Dim.X), - SignaturePanel.this.scale(evt.getY() - this.dragYOffset, U.SCREEN_ABS, U.PAGE_ABS, Dim.Y) - ); - } - - /** Handles a mouseMoved event */ - @Override - public void mouseMoved(MouseEvent evt) { - try { - boolean onSig = isOnSignature(evt); - setCursor(onSig ? Cursors.HAND : Cursors.DEFAULT); - } catch (NullPointerException e) { - // do nothing - } - } - - /** Handles a mousePressed event */ - @Override - public void mousePressed(MouseEvent evt) { - if (evt.getButton() == MouseEvent.BUTTON1) - { - this.doDrag = true; - if (isOnSignature(evt)) { - /* offsets (in screen units) from top-left corner of signature to cursor on drag start */ - this.dragXOffset = (int)(evt.getX() - SignaturePanel.this.scale(SignaturePanel.this.sigPagePos.getX(), U.PAGE_ABS, U.SCREEN_ABS, Dim.X)); - this.dragYOffset = (int)(evt.getY() - SignaturePanel.this.scale(SignaturePanel.this.sigPagePos.getY(), U.PAGE_ABS, U.SCREEN_ABS, Dim.Y)); - } else { - this.dragXOffset = 0; - this.dragYOffset = 0; - } - updateSigPosDrag(evt); - setCursor(Cursors.MOVE); - } - } - - /** Handles a mouseReleased event */ - @Override - public void mouseReleased(MouseEvent evt) { - this.doDrag = false; - boolean onSig = isOnSignature(evt); - setCursor(onSig ? Cursors.HAND : Cursors.DEFAULT); - } - - /** - * Handles a mouseDragged event. - */ - @Override - public void mouseDragged(MouseEvent evt) { - if (this.doDrag) - updateSigPosDrag(evt); - } - }; - - /** - * Sets the mouse cursor - * @param cursor cursor to set - */ - void setCursor(Cursors cursor) - { - if (this.currentCursor == cursor) - return; - this.currentCursor = cursor; - Cursor cur = null; - switch (cursor) { - case DEFAULT: - cur = this.defaultCursor; - break; - case HAND: - cur = this.handCursor; - break; - case MOVE: - cur = this.moveCursor; - break; - } - this.getParent().setCursor(cur); - } - - /** - * Check whether given point is on signature placeholder - * @param x x coordinate (screen) - * @param y y coordinate (screen) - * @return true if given point is on signature placeholder - */ - private boolean isOnSignature(MouseEvent evt) - { - if (this.sigPagePos == null) - return false; - - Rectangle2D sig = new Rectangle2D.Double( - this.scale(this.sigPagePos.getX(), U.PAGE_ABS, U.SCREEN_ABS, Dim.X), - this.scale(this.sigPagePos.getY(), U.PAGE_ABS, U.SCREEN_ABS, Dim.Y), - this.scale(this.sigPageWidth, U.PAGE_REL, U.SCREEN_REL, Dim.X), - this.scale(this.sigPageHeight, U.PAGE_REL, U.SCREEN_REL, Dim.Y) - ); - return sig.contains(evt.getX(), evt.getY()); - } - - /** - * Clamp x to be within [min-max] - * @param x int to clamp - * @param min minimum value - * @param max maximum value - * @return clamped x - */ - private static double clamp(double x, double min, double max) - { - if (x < min) - x = min; - else if (x > max) - x = max; - return x; - } -} +/* + * Copyright 2012 by A-SIT, Secure Information Technology Center Austria + * + * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * http://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + */ +package at.asit.pdfover.gui.composites; + +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; + +import at.asit.pdfover.commons.Messages; +import at.asit.pdfover.commons.utils.ImageUtil; +import lombok.extern.slf4j.Slf4j; + +/** + * Displays a PDF page and allows the user to place the signature placeholder. + */ +@Slf4j +public class SignaturePanel extends Canvas { + + /** The PDF file being displayed */ + private PDDocument pdf = null; + private PDFRenderer renderer = null; + + /** The image of the rendered PDF page being displayed */ + private Image currentImage = null; + + /** The current scale for rendering pdf to image */ + private float pageToImageScale; + /** The current scale for rendering image to screen */ + private double imageToScreenScale = 1.0; + /* scaling */ + private enum U { + /* (0,0) is bottom-left of page */ + PAGE_ABS, + /* (0,0) is top-left of page */ + PAGE_REL, + /* pixels, (0,0) is top-left of image */ + IMAGE, + /* pixels, (0,0) is top-left of image */ + SCREEN_REL, + /* pixels, (0,0) is top-left of canvas */ + SCREEN_ABS }; + private enum Dim { X, Y }; + private double scale(double v, U from, U to, Dim d) + { + if (from == to) return v; + + if (from == U.PAGE_ABS) { + return scale((d == Dim.X) ? v : (this.pageHeight - v), U.PAGE_REL, to, d); + } else if (from == U.PAGE_REL) { + if (to == U.PAGE_ABS) + return ((d == Dim.X) ? v : (this.pageHeight - v)); + else + return scale(v * this.pageToImageScale, U.IMAGE, to, d); + } else if (from == U.IMAGE) { + if ((to == U.PAGE_ABS) || (to == U.PAGE_REL)) + return scale(v / this.pageToImageScale, U.PAGE_REL, to, d); + else + return scale(v * this.imageToScreenScale, U.SCREEN_REL, to, d); + } else if (from == U.SCREEN_REL) { + if (to == U.SCREEN_ABS) + return (v + ((d == Dim.X) ? this.offX : this.offY)); + else + return scale(v / this.imageToScreenScale, U.IMAGE, to, d); + } else if (from == U.SCREEN_ABS) { + return scale(v - ((d == Dim.X) ? this.offX : this.offY), U.SCREEN_REL, to, d); + } else throw new RuntimeException("unreachable"); + } + + private float pageWidth = 0; + private float pageHeight = 0; + /** The horizontal offset of the image from the left edge of the panel */ + private int offX = 0; + /** The vertical offset of the image from the top of the panel */ + private int offY = 0; + /** The position of the top-left corner of the signature, in absolute page space */ + private Point2D sigPagePos = null; + private Point2D pendingSigPagePos = null; + public Point2D getSigPagePos() { return this.sigPagePos; } + /** The signature placeholder image */ + private Image sigPlaceholder = null; + /** Width of the signature placeholder in page space */ + private int sigPageWidth = 0; + /** Height of the signature placeholder in page space */ + private int sigPageHeight = 0; + /** Color of the signature placeholder border */ + private Color sigPlaceholderBorderColor = null; + /** Current page */ + private int currentPageNo = 0; + /** Number of pages in the document */ + private int numPages = 0; + /** Cursor types */ + private static enum Cursors {DEFAULT, HAND, MOVE}; + /** Current cursor */ + private Cursors currentCursor = Cursors.DEFAULT; + private final AtomicInteger renderGeneration = new AtomicInteger(); + private final Object renderLock = new Object(); + private boolean doDrag = false; + private int dragXOffset = 0; + private int dragYOffset = 0; + private Runnable signaturePositionAvailableCallback = null; + + /** + * Create a new PagePanel. + */ + public SignaturePanel(Composite parent, int style) { + super(parent, style | SWT.NO_BACKGROUND | SWT.DOUBLE_BUFFERED); + setDocument(null); + addPaintListener(e -> paint(e.gc)); + addMouseListener(this.mouseListener); + addMouseMoveListener(this.mouseMoveListener); + addDisposeListener(e -> { + this.renderGeneration.incrementAndGet(); + disposeCurrentImage(); + disposePlaceholder(); + }); + } + + /** + * Set a new document to be displayed + * @param pdf the PDFFile to be displayed + */ + public void setDocument(PDDocument pdf) { + if (isDisposed()) + return; + this.renderGeneration.incrementAndGet(); + synchronized (this.renderLock) { + this.pdf = pdf; + this.sigPagePos = null; + this.pendingSigPagePos = null; + if (pdf != null) + { + this.renderer = new PDFRenderer(pdf); + this.numPages = pdf.getNumberOfPages(); + this.currentPageNo = -1; + } + else + { + this.renderer = null; + this.currentPageNo = 0; + this.numPages = 0; + } + } + if (pdf != null) + showPage(this.numPages); + else { + disposeCurrentImage(); + if (!isDisposed()) + redraw(); + } + } + + /** + * Set the signature placeholder image + * @param placeholder signature placeholder + */ + public void setSignaturePlaceholder(ImageData placeholder) { + if (isDisposed()) + return; + disposePlaceholder(); + if (placeholder != null) { + this.sigPlaceholder = new Image(getDisplay(), placeholder); + // TODO figure out why this is divided by 4 (factor ported from old code) + this.sigPageWidth = placeholder.width / 4; + this.sigPageHeight = placeholder.height / 4; + } else { + this.sigPageWidth = 0; + this.sigPageHeight = 0; + } + if (this.sigPagePos != null) + setSignaturePosition(this.sigPagePos.getX(), this.sigPagePos.getY()); + redraw(); + } + + /** + * Set the color of the signature placeholder border + * @param color new signature placeholder border color + */ + public void setSignaturePlaceholderBorderColor(Color color) { + if (isDisposed()) + return; + this.sigPlaceholderBorderColor = color; + } + + public void setSignaturePositionAvailableCallback(Runnable callback) { + if (isDisposed()) + return; + this.signaturePositionAvailableCallback = callback; + } + + /** + * Change the currently displayed page + * @param page the number of the page to display + */ + public void showPage(int page) { + if (isDisposed()) + return; + if (this.currentPageNo == page) return; + this.currentPageNo = page; + renderPageToImage(); + redraw(); + } + + /** + * Set the signature placeholder position + * @param x the horizontal signature position + * @param y the vertical signature position + */ + public void setSignaturePosition(double x, double y) + { + if (isDisposed()) + return; + if (this.pageWidth <= 0 || this.pageHeight <= 0) { + this.pendingSigPagePos = new Point2D.Double(x, y); + return; + } + boolean hadNoSignaturePosition = (this.sigPagePos == null); + this.sigPagePos = new Point2D.Double( + clamp(x, 0, this.pageWidth - this.sigPageWidth), + clamp(y, this.sigPageHeight, this.pageHeight) + ); + if (hadNoSignaturePosition && this.signaturePositionAvailableCallback != null) + this.signaturePositionAvailableCallback.run(); + redraw(); + } + + public void translateSignaturePagePosition(float dX, float dY) { + if (isDisposed()) + return; + if (this.sigPagePos != null) + setSignaturePosition(this.sigPagePos.getX() + dX, this.sigPagePos.getY() + dY); + } + + private void disposeCurrentImage() { + if (this.currentImage != null && !this.currentImage.isDisposed()) + this.currentImage.dispose(); + this.currentImage = null; + } + + private void disposePlaceholder() { + if (this.sigPlaceholder != null && !this.sigPlaceholder.isDisposed()) + this.sigPlaceholder.dispose(); + this.sigPlaceholder = null; + } + + /** + * Stop the generation of any previous page, and draw the new one. + */ + private void renderPageToImage() { + if (isDisposed()) + return; + final int generation = this.renderGeneration.incrementAndGet(); + final int pageNo = this.currentPageNo; + final int screenHeight = getShell().getMonitor().getBounds().height; + final Display display = getDisplay(); + + Thread renderThread = new Thread(() -> { + ImageData imageData = null; + float renderedPageWidth = 0; + float renderedPageHeight = 0; + float renderedScale = 1; + + try { + synchronized (this.renderLock) { + if (this.pdf == null || generation != this.renderGeneration.get()) + return; + + boolean newPage = false; + PDPage currentPage; + if (pageNo > this.numPages) + { + currentPage = this.pdf.getPage(this.numPages-1); + newPage = true; + } + else + currentPage = this.pdf.getPage(pageNo-1); + + if (currentPage == null) + return; + + boolean isRotated = ((currentPage.getRotation()%180) == 90); + PDRectangle actualPageSize = currentPage.getBBox(); + renderedPageWidth = isRotated ? actualPageSize.getHeight() : actualPageSize.getWidth(); + renderedPageHeight = isRotated ? actualPageSize.getWidth() : actualPageSize.getHeight(); + renderedScale = screenHeight / renderedPageHeight; + + if (newPage) + { + int renderHeight = (int)(0.5 + (renderedPageHeight * renderedScale)); + int renderWidth = (int)(0.5 + (renderedPageWidth * renderedScale)); + BufferedImage blankImage = new BufferedImage(renderWidth, renderHeight, BufferedImage.TYPE_INT_RGB); + java.awt.Graphics g = blankImage.getGraphics(); + g.setColor(java.awt.Color.WHITE); + g.fillRect(0, 0, renderWidth, renderHeight); + g.dispose(); + imageData = ImageUtil.convertToSWT(blankImage); + } + else + { + int whichPage = Math.min(pageNo, this.numPages); + BufferedImage renderedImage = this.renderer.renderImage(whichPage-1, renderedScale); + imageData = ImageUtil.convertToSWT(convertToRGB(renderedImage)); + } + } + } catch (IOException e) { + log.error(String.format("Failed to render image for page %d of %d", Math.min(pageNo, this.numPages), this.numPages), e); + } catch (RuntimeException e) { + log.error(String.format("Failed to prepare image for page %d of %d", Math.min(pageNo, this.numPages), this.numPages), e); + } + + final ImageData finalImageData = imageData; + final float finalPageWidth = renderedPageWidth; + final float finalPageHeight = renderedPageHeight; + final float finalScale = renderedScale; + if (display.isDisposed()) + return; + display.asyncExec(() -> { + if (isDisposed() || generation != this.renderGeneration.get()) + return; + disposeCurrentImage(); + this.pageWidth = finalPageWidth; + this.pageHeight = finalPageHeight; + this.pageToImageScale = finalScale; + if (finalImageData != null) + this.currentImage = new Image(display, finalImageData); + if (this.pendingSigPagePos != null) { + Point2D pendingPosition = this.pendingSigPagePos; + this.pendingSigPagePos = null; + setSignaturePosition(pendingPosition.getX(), pendingPosition.getY()); + } else if (this.sigPagePos != null) { + setSignaturePosition(this.sigPagePos.getX(), this.sigPagePos.getY()); + } else if (this.sigPagePos == null && this.pageWidth > 0 && this.pageHeight > 0) { + setSignaturePosition(this.pageWidth * .5, this.pageHeight * .25); + } + redraw(); + }); + }, "PDF-Over page renderer"); + renderThread.setDaemon(true); + renderThread.start(); + } + + private static BufferedImage convertToRGB(BufferedImage image) { + if (image.getType() == BufferedImage.TYPE_INT_RGB) + return image; + BufferedImage rgbImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); + java.awt.Graphics g = rgbImage.getGraphics(); + g.drawImage(image, 0, 0, java.awt.Color.WHITE, null); + g.dispose(); + return rgbImage; + } + + /** + * Draw the image. + */ + private void paint(GC gc) { + Point renderPanelSize = getSize(); + gc.setBackground(getBackground()); + gc.fillRectangle(0, 0, renderPanelSize.x, renderPanelSize.y); + if (this.currentImage == null) { + gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_BLACK)); + gc.drawString(Messages.getString("common.working"), Math.max(0, renderPanelSize.x / 2 - 30), Math.max(0, renderPanelSize.y / 2), true); + } else { + Rectangle imageBounds = this.currentImage.getBounds(); + this.imageToScreenScale = Math.min( + renderPanelSize.x / (double)imageBounds.width, + renderPanelSize.y / (double)imageBounds.height); + int actualRenderWidth = (int)(imageBounds.width * this.imageToScreenScale); + int actualRenderHeight = (int)(imageBounds.height * this.imageToScreenScale); + + this.offX = (renderPanelSize.x - actualRenderWidth) / 2; + this.offY = (renderPanelSize.y - actualRenderHeight) / 2; + + gc.drawImage(this.currentImage, 0, 0, imageBounds.width, imageBounds.height, this.offX, this.offY, actualRenderWidth, actualRenderHeight); + + if (this.sigPagePos == null) + return; + + int sigX = (int) this.scale(this.sigPagePos.getX(), U.PAGE_ABS, U.SCREEN_ABS, Dim.X); + int sigY = (int) this.scale(this.sigPagePos.getY(), U.PAGE_ABS, U.SCREEN_ABS, Dim.Y); + if (this.sigPlaceholder == null) { + gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_RED)); + gc.drawRectangle(sigX, sigY, 100, 40); + } + else { + Rectangle placeholderBounds = this.sigPlaceholder.getBounds(); + int sigScreenWidth = (int)this.scale(this.sigPageWidth, U.PAGE_REL, U.SCREEN_REL, Dim.X); + int sigScreenHeight = (int)this.scale(this.sigPageHeight, U.PAGE_REL, U.SCREEN_REL, Dim.Y); + gc.drawImage(this.sigPlaceholder, 0, 0, placeholderBounds.width, placeholderBounds.height, sigX, sigY, sigScreenWidth, sigScreenHeight); + gc.setForeground(this.sigPlaceholderBorderColor != null ? this.sigPlaceholderBorderColor : getDisplay().getSystemColor(SWT.COLOR_BLUE)); + gc.drawRectangle(sigX, sigY, sigScreenWidth-1, sigScreenHeight-1); + } + } + } + + private MouseMoveListener mouseMoveListener = evt -> { + if (this.doDrag) { + updateSigPosDrag(evt); + return; + } + try { + boolean onSig = isOnSignature(evt); + setCursor(onSig ? Cursors.HAND : Cursors.DEFAULT); + } catch (NullPointerException e) { + // do nothing + } + }; + + private MouseAdapter mouseListener = new MouseAdapter() { + /** Handles a mousePressed event */ + @Override + public void mouseDown(MouseEvent evt) { + if (evt.button == 1) + { + setFocus(); + SignaturePanel.this.doDrag = true; + if (isOnSignature(evt)) { + SignaturePanel.this.dragXOffset = (int)(evt.x - SignaturePanel.this.scale(SignaturePanel.this.sigPagePos.getX(), U.PAGE_ABS, U.SCREEN_ABS, Dim.X)); + SignaturePanel.this.dragYOffset = (int)(evt.y - SignaturePanel.this.scale(SignaturePanel.this.sigPagePos.getY(), U.PAGE_ABS, U.SCREEN_ABS, Dim.Y)); + } else { + SignaturePanel.this.dragXOffset = 0; + SignaturePanel.this.dragYOffset = 0; + } + updateSigPosDrag(evt); + setCursor(Cursors.MOVE); + } + } + + /** Handles a mouseReleased event */ + @Override + public void mouseUp(MouseEvent evt) { + SignaturePanel.this.doDrag = false; + boolean onSig = isOnSignature(evt); + setCursor(onSig ? Cursors.HAND : Cursors.DEFAULT); + } + }; + + private void updateSigPosDrag(MouseEvent evt) { + setSignaturePosition( + scale(evt.x - this.dragXOffset, U.SCREEN_ABS, U.PAGE_ABS, Dim.X), + scale(evt.y - this.dragYOffset, U.SCREEN_ABS, U.PAGE_ABS, Dim.Y) + ); + } + + /** + * Sets the mouse cursor + * @param cursor cursor to set + */ + void setCursor(Cursors cursor) + { + if (this.currentCursor == cursor) + return; + this.currentCursor = cursor; + switch (cursor) { + case DEFAULT: + setCursor(getDisplay().getSystemCursor(SWT.CURSOR_ARROW)); + break; + case HAND: + setCursor(getDisplay().getSystemCursor(SWT.CURSOR_HAND)); + break; + case MOVE: + setCursor(getDisplay().getSystemCursor(SWT.CURSOR_SIZEALL)); + break; + } + } + + /** + * Check whether given point is on signature placeholder + * @return true if given point is on signature placeholder + */ + private boolean isOnSignature(MouseEvent evt) + { + if (this.sigPagePos == null) + return false; + + double sigX = this.scale(this.sigPagePos.getX(), U.PAGE_ABS, U.SCREEN_ABS, Dim.X); + double sigY = this.scale(this.sigPagePos.getY(), U.PAGE_ABS, U.SCREEN_ABS, Dim.Y); + double sigWidth = this.scale(this.sigPageWidth, U.PAGE_REL, U.SCREEN_REL, Dim.X); + double sigHeight = this.scale(this.sigPageHeight, U.PAGE_REL, U.SCREEN_REL, Dim.Y); + return (evt.x >= sigX) && (evt.x <= (sigX + sigWidth)) && (evt.y >= sigY) && (evt.y <= (sigY + sigHeight)); + } + + /** + * Clamp x to be within [min-max] + * @param x int to clamp + * @param min minimum value + * @param max maximum value + * @return clamped x + */ + private static double clamp(double x, double min, double max) + { + if (x < min) + x = min; + else if (x > max) + x = max; + return x; + } +} diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/workflow/states/PositioningState.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/workflow/states/PositioningState.java index cf7e91c8..753c7b32 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/workflow/states/PositioningState.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/workflow/states/PositioningState.java @@ -63,6 +63,8 @@ private void closePDFDocument() { if (this.document != null) { + if (this.positionComposite != null && !this.positionComposite.isDisposed()) + this.positionComposite.clearDocument(); try { this.document.close(); } catch (IOException e) { log.warn("Failed to close PDF", e); } this.document = null; } @@ -117,7 +119,7 @@ private PositioningComposite getPositioningComposite(PDDocument document) { PdfAs4SignaturePlaceholder.For(param, (p) -> { if (p.hasImage()) - this.positionComposite.setPlaceholder(p.getAWTImage()); + this.positionComposite.setPlaceholder(p.getSWTImage()); }); if (this.previousPosition != null && !this.previousPosition.useAutoPositioning())