/* * Copyright 2006-2012 ICEsoft Technologies Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the * License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS * IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package org.icepdf.core.views.swing; import org.icepdf.core.events.PaintPageEvent; import org.icepdf.core.events.PaintPageListener; import org.icepdf.core.pobjects.Page; import org.icepdf.core.pobjects.PageTree; import org.icepdf.core.pobjects.annotations.Annotation; import org.icepdf.core.pobjects.graphics.text.PageText; import org.icepdf.core.search.DocumentSearchController; import org.icepdf.core.util.*; import org.icepdf.core.views.AnnotationComponent; import org.icepdf.core.views.DocumentView; import org.icepdf.core.views.DocumentViewController; import org.icepdf.core.views.DocumentViewModel; import org.icepdf.core.views.common.AnnotationHandler; import org.icepdf.core.views.common.TextSelectionPageHandler; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.lang.ref.SoftReference; import java.util.logging.Level; import java.util.logging.Logger; /** * <p>This class represents a single page view of a PDF document as a JComponent. * This component can be used in any swing application to display a PDF page. The * default RI implemenation comes with four predefined page views which use this * component. If custom page views are need then the following class should * be referenced: </p> * <p/> * <p>This component assumes that white paper is the default and thus uses white * as the default background color for buffers and page painting if no color * is specified by the PDF. This default colour can be changed using the * system property org.icepdf.core.views.page.paper.color. This property * takes RRGGBB hex colours as values. eg. black=000000 or white=FFFFFFF.</p> * * @see org.icepdf.ri.common.views.OneColumnPageView * @see org.icepdf.ri.common.views.OnePageView * @see org.icepdf.ri.common.views.TwoColumnPageView * @see org.icepdf.ri.common.views.TwoPageView * @see org.icepdf.ri.common.views.AbstractDocumentView * @see org.icepdf.ri.common.views.AbstractDocumentViewModel * @see org.icepdf.core.views.DocumentViewController * <p/> * <p>The page view takes advantage of a buffered display to speed up page scrolling * and provide users with a better overall UI experiance. * The size of the buffer can also be set with the system properties * "org.icepdf.core.views.buffersize.vertical" and * "org.icepdf.core.views.buffersize.horizontal". These system * properties define the vertical and horizontal ratios in which the current * viewport will be extended to define the buffer size.</p> * @since 2.5 */ public class PageViewComponentImpl extends AbstractPageViewComponent implements PaintPageListener, FocusListener, ComponentListener { private static final Logger logger = Logger.getLogger(PageViewComponentImpl.class.toString()); private static Color pageColor; static { try { String color = Defs.sysProperty( "org.icepdf.core.views.page.paper.color", "#FFFFFF"); int colorValue = ColorUtil.convertColor(color); pageColor = new Color(colorValue >= 0 ? colorValue : Integer.parseInt("FFFFFF", 16)); } catch (NumberFormatException e) { logger.warning("Error reading page paper color."); } } // turn off page image buffer proxy loading. private static boolean enablePageLoadingProxy = Defs.booleanProperty("org.icepdf.core.views.page.proxy",true); private PageTree pageTree; private JScrollPane parentScrollPane; private int pageIndex; private Rectangle pageSize = new Rectangle(); // cached page size, we call this a lot. private Rectangle defaultPageSize = new Rectangle(); private boolean isPageSizeCalculated = false; private float currentZoom; private float currentRotation; protected DocumentView parentDocumentView; protected DocumentViewModel documentViewModel; protected DocumentViewController documentViewController; // annotation mouse, key and paint handler. protected AnnotationHandler annotationHandler; // text selection mouse/key and paint handler protected TextSelectionPageHandler textSelectionHandler; // the buffered image which will be painted to private SoftReference<Image> bufferedPageImageReference; // the bounds of the buffered image. private Rectangle bufferedPageImageBounds = new Rectangle(); private Timer isDirtyTimer; // private DirtyTimerAction dirtyTimerAction; private PageInitilizer pageInitilizer; private PagePainter pagePainter; private final Object paintCopyAreaLock = new Object(); private boolean disposing = false; // current clip private Rectangle clipBounds; private Rectangle oldClipBounds; private boolean inited; // vertical scale factor to extend buffer private static double verticalScaleFactor; // horizontal scale factor to extend buffer private static double horizontalScaleFactor; // dirty refresh timer call interval private static int dirtyTimerInterval = 5; // graphics configuration private GraphicsConfiguration gc; static { // default value have been assigned. Keep in mind that larger ratios will // result in more memory usage. try { verticalScaleFactor = Double.parseDouble(Defs.sysProperty("org.icepdf.core.views.buffersize.vertical", "1.015")); horizontalScaleFactor = Double.parseDouble(Defs.sysProperty("org.icepdf.core.views.buffersize.horizontal", "1.015")); } catch (NumberFormatException e) { logger.warning("Error reading buffered scale factor"); } try { dirtyTimerInterval = Defs.intProperty("org.icepdf.core.views.dirtytimer.interval", 5); } catch (NumberFormatException e) { logger.log(Level.FINE, "Error reading dirty timer interval"); } } public PageViewComponentImpl(DocumentViewModel documentViewModel, PageTree pageTree, int pageNumber, JScrollPane parentScrollPane) { this(documentViewModel, pageTree, pageNumber, parentScrollPane, 0, 0); } public PageViewComponentImpl(DocumentViewModel documentViewModel, PageTree pageTree, int pageNumber, JScrollPane parentScrollPane, int width, int height) { // removed focasable until we can build our own focus manager // for moving though a large number of pages. // setFocusable(true); // add focus listener // addFocusListener(this); // needed to propagate mouse events. this.documentViewModel = documentViewModel; this.parentScrollPane = parentScrollPane; // annotation action, selection and creation annotationHandler = new AnnotationHandler(this, documentViewModel); // text selection textSelectionHandler = new TextSelectionPageHandler(this, documentViewModel); currentRotation = documentViewModel.getViewRotation(); currentZoom = documentViewModel.getViewRotation(); this.pageTree = pageTree; this.pageIndex = pageNumber; clipBounds = new Rectangle(); oldClipBounds = new Rectangle(); bufferedPageImageReference = new SoftReference<Image>(null); // initialize page size if (width == 0 && height == 0) { calculatePageSize(pageSize); isPageSizeCalculated = true; } else { pageSize.setSize(width, height); defaultPageSize.setSize(width, height); } } /** * If no DocumentView is used then the various mouse and keyboard * listeners must be added tothis component. If there is a document * view then we let it delegate events to make life easier. */ public void addPageViewComponentListeners() { // add listeners addMouseListener(this); addMouseMotionListener(this); addComponentListener(this); // annotation pickups // handles, multiple selections and new annotation creation. addMouseListener(annotationHandler); addMouseMotionListener(annotationHandler); // text selection mouse handler addMouseMotionListener(textSelectionHandler); addMouseListener(textSelectionHandler); } /** * Adds the specified annotation to this page instance. The annotation * is wrapped with a AnnotationComponent and added to this components layout * manager. * * @param annotation annotation to add to this page instance. . */ public AnnotationComponent addAnnotation(Annotation annotation) { // delegate to handler. return annotationHandler.addAnnotationComponent(annotation); } /** * Removes the specified annotation from this page component * * @param annotationComp annotation to be removed. */ public void removeAnnotation(AnnotationComponent annotationComp) { // delegate to handler. annotationHandler.removeAnnotationComponent(annotationComp); } public void init() { if (inited) { return; } inited = true; // add repaint listener addPageRepaintListener(); // timer will dictate when buffer repaints can take place DirtyTimerAction dirtyTimerAction = new DirtyTimerAction(); isDirtyTimer = new Timer(dirtyTimerInterval, dirtyTimerAction); isDirtyTimer.setInitialDelay(0); // PageInilizer and painter commands pageInitilizer = new PageInitilizer(); pagePainter = new PagePainter(); } public void invalidatePage() { if (inited) { Page page = pageTree.getPage(pageIndex, this); page.getLibrary().disposeFontResources(); page.reduceMemory(); pageTree.releasePage(page, this); currentZoom = -1; } } public void dispose() { disposing = true; if (isDirtyTimer != null) isDirtyTimer.stop(); removeMouseListener(this); removeMouseMotionListener(this); removeComponentListener(this); // remove annotation listeners. removeMouseMotionListener(annotationHandler); removeMouseListener(annotationHandler); // text selection removeMouseMotionListener(textSelectionHandler); removeMouseListener(textSelectionHandler); // remove focus listener removeFocusListener(this); // remove repaint listener removePageRepaintListener(); if (bufferedPageImageReference != null) { Image pageBufferImage = bufferedPageImageReference.get(); if (pageBufferImage != null) { pageBufferImage.flush(); } } inited = false; } public Page getPageLock(Object lock) { return pageTree.getPage(pageIndex, lock); } public void releasePageLock(Page currentPage, Object lock) { pageTree.releasePage(currentPage, lock); } public void setDocumentViewCallback(DocumentView parentDocumentView) { this.parentDocumentView = parentDocumentView; documentViewController = this.parentDocumentView.getParentViewController(); // set annotation callback annotationHandler.setDocumentViewController(documentViewController); // set text selection callback textSelectionHandler.setDocumentViewController(documentViewController); } public int getPageIndex() { return pageIndex; } public Dimension getPreferredSize() { return pageSize.getSize(); } public void invalidate() { calculateRoughPageSize(pageSize); if (pagePainter != null) { pagePainter.setIsBufferDirty(true); } super.invalidate(); } public void paintComponent(Graphics gg) { if (!inited) { init(); } // make sure the initiate the pages size if (!isPageSizeCalculated) { calculatePageSize(pageSize); invalidate(); } else if (isPageStateDirty()) { calculatePageSize(pageSize); } Graphics2D g = (Graphics2D) gg.create(0, 0, pageSize.width, pageSize.height); g.setColor(pageColor); g.fillRect(0, 0, pageSize.width, pageSize.height); // multi thread page load if (enablePageLoadingProxy && isPageIntersectViewport() && !isDirtyTimer.isRunning()) { isDirtyTimer.start(); } // single threaded load of page content on awt thread (no flicker) else if (!enablePageLoadingProxy && isPageIntersectViewport() && (isPageStateDirty() || isBufferDirty())){ pageInitilizer.run(); pagePainter.run(); } // Update clip data // first choice is to use the parent view port but the clip will do otherwise if (parentScrollPane == null) { oldClipBounds.setBounds(clipBounds); clipBounds.setBounds(g.getClipBounds()); if (oldClipBounds.width == 0 && oldClipBounds.height == 0) { oldClipBounds.setBounds(clipBounds); } } if (bufferedPageImageReference != null) { Image pageBufferImage = bufferedPageImageReference.get(); // draw the clean buffer if (pageBufferImage != null && !isPageStateDirty()) { // block, if copy area is being done in painter thread synchronized (paintCopyAreaLock) { g.drawImage(pageBufferImage, bufferedPageImageBounds.x, bufferedPageImageBounds.y, this); } // if (!isPageRepaintListenerEnabled && !pagePainter.isRunning()){ // addPageRepaintListener(); // } // g.setColor(Color.blue); // g.drawRect(oldBufferedPageImageBounds.x + 1, // oldBufferedPageImageBounds.y + 1, // oldBufferedPageImageBounds.width - 3, // oldBufferedPageImageBounds.height- 3); // g.setColor(Color.red); // g.drawRect(bufferedPageImageBounds.x + 1, // bufferedPageImageBounds.y + 1, // bufferedPageImageBounds.width - 2, // bufferedPageImageBounds.height- 2); } // no pageBuffer to paint thus, we must recreate it. else { // mark as dirty currentZoom = -1; } // paint annotations annotationHandler.paintAnnotations(g); // Lazy paint of highlight and select all text states. Page currentPage = this.getPageLock(this); if (currentPage != null && currentPage.isInitiated()) { PageText pageText = currentPage.getViewText(); // paint any highlighted words DocumentSearchController searchController = documentViewController.getParentController() .getDocumentSearchController(); if (searchController.isSearchHighlightRefreshNeeded(pageIndex, pageText)) { searchController.searchHighlightPage(pageIndex); } // if select all we'll want to paint the selected text. if (documentViewModel.isSelectAll()) { documentViewModel.addSelectedPageText(this); pageText.selectAll(); } // paint selected test sprites. textSelectionHandler.paintSelectedText(g); } this.releasePageLock(currentPage, this); } } /** * Mouse clicked event priority is given to annotation clicks. Otherwise * the selected tool state is respected. * * @param e awt mouse event. */ public void mouseClicked(MouseEvent e) { // depending on tool state propagate mouse state if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) { textSelectionHandler.mouseClicked(e); } else if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_ZOOM_IN) { // correct click for coordinate of this component Point p = e.getPoint(); Point offset = documentViewModel.getPageBounds(pageIndex).getLocation(); p.setLocation(p.x + offset.x, p.y + offset.y); // request a zoom center on the new point documentViewController.setZoomIn(p); } else if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_ZOOM_OUT) { // correct click for coordinate of this component Point p = e.getPoint(); // request a zoom center on the new point documentViewController.setZoomOut(p); } else if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_SELECTION) { annotationHandler.mouseClicked(e); } } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mousePressed(MouseEvent e) { // request page focus // requestFocusInWindow(); if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) { textSelectionHandler.mousePressed(e); } else if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_SELECTION || documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_LINK_ANNOTATION) { annotationHandler.mousePressed(e); } } public void clearSelectedText() { if (textSelectionHandler != null) { textSelectionHandler.clearSelection(); } } public void mouseReleased(MouseEvent e) { if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) { textSelectionHandler.mouseReleased(e); } else if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_SELECTION || documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_LINK_ANNOTATION) { annotationHandler.mouseReleased(e); } } public void mouseDragged(MouseEvent e) { if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) { textSelectionHandler.mouseDragged(e); } else if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_SELECTION || documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_LINK_ANNOTATION) { annotationHandler.mouseDragged(e); } } public void setTextSelectionRectangle(Point cursorLocation, Rectangle selection) { textSelectionHandler.setSelectionRectangle(cursorLocation, selection); } public void mouseMoved(MouseEvent e) { // process text selection. if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) { textSelectionHandler.mouseMoved(e); } else if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_SELECTION) { annotationHandler.mouseMoved(e); } } public void focusGained(FocusEvent e) { int oldCurrentPage = documentViewModel.getViewCurrentPageIndex(); documentViewModel.setViewCurrentPageIndex(pageIndex); documentViewController.firePropertyChange(PropertyConstants.DOCUMENT_CURRENT_PAGE, oldCurrentPage, pageIndex); } public void focusLost(FocusEvent e) { } public void componentHidden(ComponentEvent e) { } public void componentMoved(ComponentEvent e) { } public void componentResized(ComponentEvent e) { } public void componentShown(ComponentEvent e) { } private void calculateRoughPageSize(Rectangle pageSize) { // use a ratio to figure out what the dimension should be a after // a specific scale. float width = defaultPageSize.width; float height = defaultPageSize.height; float totalRotation = documentViewModel.getViewRotation(); if (totalRotation == 0 || totalRotation == 180) { // Do nothing } // Rotated sideways else if (totalRotation == 90 || totalRotation == 270) { float temp = width; // width equals hight is ok in this case width = height; height = temp; } // Arbitrary rotation else { AffineTransform at = new AffineTransform(); double radians = Math.toRadians(totalRotation); at.rotate(radians); Rectangle2D.Double boundingBox = new Rectangle2D.Double(0.0, 0.0, 0.0, 0.0); Point2D.Double src = new Point2D.Double(); Point2D.Double dst = new Point2D.Double(); src.setLocation(0.0, height); // Top left at.transform(src, dst); boundingBox.add(dst); src.setLocation(width, height); // Top right at.transform(src, dst); boundingBox.add(dst); src.setLocation(0.0, 0.0); // Bottom left at.transform(src, dst); boundingBox.add(dst); src.setLocation(width, 0.0); // Bottom right at.transform(src, dst); boundingBox.add(dst); width = (float) boundingBox.getWidth(); height = (float) boundingBox.getHeight(); } pageSize.setSize((int) (width * documentViewModel.getViewZoom()), (int) (height * documentViewModel.getViewZoom())); } private void calculatePageSize(Rectangle pageSize) { if (pageTree != null) { Page currentPage = pageTree.getPage(pageIndex, this); if (currentPage != null) { pageSize.setSize(currentPage.getSize( documentViewModel.getPageBoundary(), documentViewModel.getViewRotation(), documentViewModel.getViewZoom()).toDimension()); defaultPageSize.setSize(currentPage.getSize( documentViewModel.getPageBoundary(), 0, 1).toDimension()); } pageTree.releasePage(currentPage, this); } } private boolean isBufferDirty() { if (disposing) return false; // if the page size is smaller then the view port then we will always be clean // as this is the policy defined when creating the buffer. if (pageSize.height <= clipBounds.height && pageSize.width <= clipBounds.width) { return false; } Rectangle tempClipBounds = new Rectangle(clipBounds); if (parentScrollPane != null) { tempClipBounds.setBounds(parentScrollPane.getViewport().getViewRect()); } // get the pages current bounds, may have changed sin Rectangle pageBounds = documentViewModel.getPageBounds(pageIndex); // adjust the buffered Page bounds to component space Rectangle normalizedBounds = new Rectangle(bufferedPageImageBounds); normalizedBounds.x += pageBounds.x; normalizedBounds.y += pageBounds.y; // if the normalized bounds are not contained in the intersection of // pageBounds and the clipBound then we have dirty buffer, that is, empty // white space is visible where page content should be. return !normalizedBounds.contains(pageBounds.intersection(tempClipBounds)); } /** * Utility method for setting up the buffered image painting. Take care * of all the needed transformation. * * @param pagePainter painter doing the painting work. */ private void createBufferedPageImage(PagePainter pagePainter) { if (disposing) return; boolean isPageStateDirty = isPageStateDirty(); // Mark the image size as being fixed, we don't want the timer to // accidentally stop the painting currentRotation = documentViewModel.getViewRotation(); currentZoom = documentViewModel.getViewZoom(); // clear the image if it is dirty, we don't want to paint the wrong size buffer Image pageBufferImage = bufferedPageImageReference.get(); // draw the clean buffer if (isPageStateDirty && pageBufferImage != null) { pageBufferImage.flush(); } // update buffer states, before we change bufferedPageImageBounds Rectangle oldBufferedPageImageBounds = new Rectangle(bufferedPageImageBounds); // get the pages current bounds, may have changed sin Rectangle pageBounds = documentViewModel.getPageBounds(pageIndex); if (parentScrollPane != null) { oldClipBounds.setBounds(clipBounds); clipBounds.setBounds(parentScrollPane.getViewport().getViewRect()); if (oldClipBounds.width == 0 && oldClipBounds.height == 0) { oldClipBounds.setBounds(clipBounds); } } // calculate the intersection of the clipBounds with the page size. // This will give us the basis for most of our calculations bufferedPageImageBounds.setBounds(pageBounds.intersection(clipBounds)); // calculate the size of the buffers width, if the page is smaller then // the clip bounds, then we use the page size as the buffer size. if (pageSize.width <= clipBounds.width) { bufferedPageImageBounds.x = 0; bufferedPageImageBounds.width = pageSize.width; } // otherwise we use the size of the clipBounds * horizontal scale factor else { if (horizontalScaleFactor > 1.0) { double width = ((clipBounds.width * horizontalScaleFactor) / 2.0); bufferedPageImageBounds.x = (int) (clipBounds.x - width); bufferedPageImageBounds.width = (int) (clipBounds.width + (width * 2.0)); } // we want clip bounds width else { bufferedPageImageBounds.width = clipBounds.width; } // but we need to normalize the x coordinate to component space bufferedPageImageBounds.x -= pageBounds.x; } // calculate the size of the buffers height if (pageSize.height <= clipBounds.height) { bufferedPageImageBounds.y = 0; bufferedPageImageBounds.height = clipBounds.height; } // otherwise we use the size of the clipBounds * horizontal scale factor else { if (verticalScaleFactor > 1.0) { double height = ((clipBounds.height * verticalScaleFactor) / 2.0); bufferedPageImageBounds.y = (int) (clipBounds.y - height); bufferedPageImageBounds.height = (int) (clipBounds.height + (height * 2.0)); } // we want clip bounds height else { bufferedPageImageBounds.height = clipBounds.height; } // but we need to normalize the y coordinate to component space bufferedPageImageBounds.y -= pageBounds.y; } // clean up old image if available, this is done before we correct the bounds, // keeps the same buffer size for the zoom/rotation, but manipulate its bounds // to avoid creating a series of new buffers and thus more flicker // Boolean isBufferDirty = isBufferDirty(); pageBufferImage = bufferedPageImageReference.get(); // draw the clean buffer if (isPageStateDirty || pageBufferImage == null) { // clear old buffer if (pageBufferImage != null) { pageBufferImage.flush(); } // create new image and get graphics context from image if (gc == null){ gc = getGraphicsConfiguration(); } if (gc != null && this.isShowing()) { // get the optimal image for the platform pageBufferImage = gc.createCompatibleImage( bufferedPageImageBounds.width, bufferedPageImageBounds.height); // paint white, try to avoid black flicker Graphics g = pageBufferImage.getGraphics(); g.setColor(pageColor); g.fillRect(0, 0, pageSize.width, pageSize.height); } bufferedPageImageReference = new SoftReference<Image>(pageBufferImage); // IMPORTANT! we don't won't to do a copy area if the page state is dirty. pagePainter.setIsBufferDirty(false); } // correct horizontal dimensions if (bufferedPageImageBounds.x < 0) { bufferedPageImageBounds.x = 0; } if ((bufferedPageImageBounds.x + bufferedPageImageBounds.width) > pageSize.width) { bufferedPageImageBounds.width = pageSize.width - bufferedPageImageBounds.x; } // correctly vertical dimensions if (bufferedPageImageBounds.y < 0) { bufferedPageImageBounds.y = 0; } if ((bufferedPageImageBounds.y + bufferedPageImageBounds.height) > pageSize.height) { bufferedPageImageBounds.height = pageSize.height - bufferedPageImageBounds.y; } if (pageBufferImage != null) { // get graphics context Graphics2D imageGraphics = (Graphics2D) pageBufferImage.getGraphics(); // jdk 1.3.1 doesn't like a none (0,0)location for the clip, imageGraphics.setClip(new Rectangle(0, 0, bufferedPageImageBounds.width, bufferedPageImageBounds.height)); // this is really important translate the image graphics // context so that we paint the correct area of the page to the image int xTrans = 0 - bufferedPageImageBounds.x; int yTrans = 0 - bufferedPageImageBounds.y; // copyRect is used for copy area from last good paint where possible. Rectangle copyRect; // block awt from repainting during copy area calculation and clear // background painting synchronized (paintCopyAreaLock) { // adjust the buffered Page bounds to component space Rectangle normalizedClipBounds = new Rectangle(clipBounds); normalizedClipBounds.x -= pageBounds.x; normalizedClipBounds.y -= pageBounds.y; // start an an area copy from the old buffer to the new buffer. if (!pagePainter.isLastPaintDirty() && pagePainter.isBufferDirty() && bufferedPageImageBounds.intersects(oldBufferedPageImageBounds)) { // calculate intersection for buffer copy of a visible area, as we // can only copy graphics that are visible. copyRect = bufferedPageImageBounds.intersection(oldBufferedPageImageBounds); copyRect = copyRect.intersection(normalizedClipBounds); // setup graphics context for copy, we need to use old buffer bounds int xTransOld = 0 - oldBufferedPageImageBounds.x; int yTransOld = 0 - oldBufferedPageImageBounds.y; // copy the old area, relative to whole page int dx = oldBufferedPageImageBounds.x - bufferedPageImageBounds.x; int dy = oldBufferedPageImageBounds.y - bufferedPageImageBounds.y; // notice the appending of xTransOld and yTransOld // this is the same as a imageGraphics.translate(xTransOld,yTransOld) // but the copyArea method on the mac does not respect the translate, Doh! imageGraphics.copyArea(copyRect.x + xTransOld, copyRect.y + yTransOld, copyRect.width, copyRect.height, dx, dy); // calculate the clip to set Area copyArea = new Area(copyRect); Area bufferArea = new Area(bufferedPageImageBounds); bufferArea.subtract(copyArea); // set the new clip, relative to whole page imageGraphics.translate(xTrans, yTrans); imageGraphics.setClip(bufferArea); // restore graphics context. imageGraphics.translate(-xTrans, -yTrans); } else { // set the new clip, relative to whole page imageGraphics.translate(xTrans, yTrans); imageGraphics.setClip(bufferedPageImageBounds); // restore graphics context. imageGraphics.translate(-xTrans, -yTrans); } // setup graphics context for repaint imageGraphics.translate(xTrans, yTrans); // paint background to white to avoid old buffer garbage paint. imageGraphics.setColor(pageColor); imageGraphics.fillRect(bufferedPageImageBounds.x, bufferedPageImageBounds.y, bufferedPageImageBounds.width, bufferedPageImageBounds.height); } // Paint the page content if (pageTree != null) { Page page = pageTree.getPage(pageIndex, this); page.paint(imageGraphics, GraphicsRenderingHints.SCREEN, documentViewModel.getPageBoundary(), documentViewModel.getViewRotation(), documentViewModel.getViewZoom(), pagePainter, false, false); // clean up pageTree.releasePage(page, this); if (pagePainter.isStopPaintingRequested()) { pagePainter.setIsLastPaintDirty(true); } else { pagePainter.setIsLastPaintDirty(false); pagePainter.setIsBufferDirty(false); } // one last paint once everything is done Runnable doSwingWork = new Runnable() { public void run() { if (!disposing) repaint(); } }; SwingUtilities.invokeLater(doSwingWork); } imageGraphics.dispose(); } } private void addPageRepaintListener() { Page currentPage = pageTree.getPage(pageIndex, this); if (currentPage != null) { currentPage.addPaintPageListener(this); } pageTree.releasePage(currentPage, this); } private void removePageRepaintListener() { if (inited) { Page currentPage = pageTree.getPage(pageIndex, this); if (currentPage != null) { currentPage.removePaintPageListener(this); } pageTree.releasePage(currentPage, this); } } private boolean isPageStateDirty() { return currentZoom != documentViewModel.getViewZoom() || currentRotation != documentViewModel.getViewRotation() || oldClipBounds.width != clipBounds.width || oldClipBounds.height != clipBounds.height ; } private boolean isPageIntersectViewport() { Rectangle pageBounds = documentViewModel.getPageBounds(pageIndex); return pageBounds != null && this.isShowing() && pageBounds.intersects(parentScrollPane.getViewport().getViewRect()); } public void paintPage(PaintPageEvent event) { Object source = event.getSource(); Page page = pageTree.getPage(pageIndex, this); if (page.equals(source)) { Runnable doSwingWork = new Runnable() { public void run() { if (!disposing) { repaint(); } } }; // initiate the repaint SwingUtilities.invokeLater(doSwingWork); } pageTree.releasePage(page, this); } public class PagePainter implements Runnable { private boolean isRunning; private boolean isLastPaintDirty; private boolean isBufferyDirty; private boolean isStopRequested; private final Object isRunningLock = new Object(); private boolean hasBeenQueued; public synchronized boolean isLastPaintDirty() { return isLastPaintDirty; } public void setIsLastPaintDirty(boolean isDirty) { isLastPaintDirty = isDirty; } public void setIsBufferDirty(boolean isDirty) { isBufferyDirty = isDirty; } public boolean isBufferDirty() { return isBufferyDirty; } public boolean isStopPaintingRequested() { return isStopRequested; } // stop painting public synchronized void stopPaintingPage() { isStopRequested = true; isLastPaintDirty = true; } public void run() { synchronized (isRunningLock) { isRunning = true; hasBeenQueued = false; } try { createBufferedPageImage(this); } catch (Throwable e) { logger.log(Level.WARNING, "Error creating buffer, page: " + pageIndex, e); // mark as dirty, so that it tries again to create buffer currentZoom = -1; } synchronized (isRunningLock) { isStopRequested = false; isRunning = false; } } public boolean hasBeenQueued() { synchronized (isRunningLock) { return hasBeenQueued; } } public void setHasBeenQueued(boolean hasBeenQueued) { this.hasBeenQueued = hasBeenQueued; } public boolean isRunning() { synchronized (isRunningLock) { return isRunning; } } } private class PageInitilizer implements Runnable { private boolean isRunning; private final Object isRunningLock = new Object(); // private AbstractPageViewComponent pageComponent; private boolean hasBeenQueued; public void run() { synchronized (isRunningLock) { isRunning = true; } try { Page page = pageTree.getPage(pageIndex, this); page.init(); // add annotation components to container, this only done // once, but Annotation state can be refreshed with the api // when needed. annotationHandler.initializeAnnotationComponents( page.getAnnotations()); // fire page annotation initialized callback if (documentViewController.getAnnotationCallback() != null) { documentViewController.getAnnotationCallback() .pageAnnotationsInitialized(page); } pageTree.releasePage(page, this); } catch (Throwable e) { logger.log(Level.WARNING, "Error initiating page: " + pageIndex, e); // make sure we don't try to re-initialize pageInitilizer.setHasBeenQueued(true); return; } synchronized (isRunningLock) { pageInitilizer.setHasBeenQueued(false); isRunning = false; } } public boolean hasBeenQueued() { return hasBeenQueued; } public void setHasBeenQueued(boolean hasBeenQueued) { this.hasBeenQueued = hasBeenQueued; } public boolean isRunning() { synchronized (isRunningLock) { return isRunning; } } } private class DirtyTimerAction implements ActionListener { public void actionPerformed(ActionEvent e) { if (disposing || !isPageIntersectViewport()) { isDirtyTimer.stop(); // stop painting and mark buffer as dirty if (pagePainter.isRunning()) { pagePainter.stopPaintingPage(); currentZoom = -1; } return; } // if we are scrolling, no new threads if (!disposing) { // we don't want to draw if we are scrolling if (parentScrollPane != null && parentScrollPane.getVerticalScrollBar().getValueIsAdjusting()) { return; } // lock page Page page = pageTree.getPage(pageIndex, this); // load the page content if (page != null && !page.isInitiated() && !pageInitilizer.isRunning() && !pageInitilizer.hasBeenQueued()) { try { pageInitilizer.setHasBeenQueued(true); documentViewModel.executePageInitialization(pageInitilizer); } catch (InterruptedException ex) { pageInitilizer.setHasBeenQueued(false); if (logger.isLoggable(Level.WARNING)) { logger.fine("Page Initialization Interrupted: " + pageIndex); } } } // paint page content boolean isBufferDirty = isBufferDirty(); if (page != null && !pageInitilizer.isRunning() && page.isInitiated() && !pagePainter.isRunning() && !pagePainter.hasBeenQueued() && (isPageStateDirty() || isBufferDirty) ) { try { pagePainter.setHasBeenQueued(true); pagePainter.setIsBufferDirty(isBufferDirty); documentViewModel.executePagePainter(pagePainter); } catch (InterruptedException ex) { pagePainter.setHasBeenQueued(false); if (logger.isLoggable(Level.WARNING)) { logger.fine("Page Painter Interrupted: " + pageIndex); } } } // paint page content if (page != null && !pageInitilizer.isRunning() && page.isInitiated() && !pagePainter.hasBeenQueued() && pagePainter.isRunning() ) { // stop painting and mark buffer as dirty if (isPageStateDirty()) { pagePainter.stopPaintingPage(); } } // unlock page pageTree.releasePage(page, this); } } } }