org.jpedal.display.PageFlowFX Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of OpenViewerFX Show documentation
Show all versions of OpenViewerFX Show documentation
An Open Source JavaFX PDF Viewer
/*
* ===========================================
* Java Pdf Extraction Decoding Access Library
* ===========================================
*
* Project Info: http://www.idrsolutions.com
* Help section for developers at http://www.idrsolutions.com/support/
*
* (C) Copyright 1997-2015 IDRsolutions and Contributors.
*
* This file is part of JPedal/JPDF2HTML5
*
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* ---------------
* PageFlowFX.java
* ---------------
*/
package org.jpedal.display;
import java.awt.image.BufferedImage;
import java.util.Timer;
import java.util.TimerTask;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.EventHandler;
import javafx.scene.CacheHint;
import javafx.scene.ImageCursor;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.CheckBox;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.effect.PerspectiveTransform;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import org.jpedal.PdfDecoderInt;
import org.jpedal.objects.acroforms.AcroRenderer;
import org.jpedal.parser.DecoderOptions;
import org.jpedal.utils.LogWriter;
import org.jpedal.utils.Messages;
/**
* Main JavaFX Code for Page Flow Display Mode used
* by both SwingViewer and JavaFXViewer.
*/
public class PageFlowFX extends AnchorPane{
private final boolean isFX;
public PageFlowFX(final PdfDecoderInt pdfDecoder,final boolean isFX) {
// JavaFX cannot be restarted after exiting, so turn off auto exit.
Platform.setImplicitExit(false);
// Setup PDF info
pdf = pdfDecoder;
this.isFX =isFX;
//pageData = pdf.getPdfPageData();
pageCount = pdf.getPageCount();
setPageNumber(pdfDecoder.getPageNumber());
pageFocus = pageNumber;
// Setup for memory management
pagesInMemory = 0;
runtime = Runtime.getRuntime();
final long maxMem = runtime.maxMemory();
if (maxMem * 0.25f < 36000000) {
memoryLimit = maxMem - 36000000;
} else {
memoryLimit = (long) (maxMem * 0.75f);
}
//Disable forms, but store whether they were on so we can restore on dispose()
final AcroRenderer formRenderer = pdf.getFormRenderer();
if (formRenderer != null) {
formsIgnoredStore = formRenderer.ignoreForms();
formRenderer.setIgnoreForms(true);
}
pages = new Page[pageCount];
createScene();
pageLimit = 50;
}
protected final PdfDecoderInt pdf;
private boolean stopAddingPages;
private final int pageCount;
private int displayRotation;
protected int pageNumber;
private int pagesToGenerate = 21;
private static final int textureSize = 256;
private Page[] pages;
private double totalPageWidth;
private Rectangle backgroundTop;
private Rectangle backgroundBottom;
private NavBar navBar;
private ZoomBar zoomBar;
private CheckBox perspectiveCheckBox;
private CheckBox reflectionCheckBox;
//private EventHandler pageListener, messageListener;
private javafx.scene.Cursor defaultCursor, grabbingCursor, grabCursor;
private double sceneXOffset, sceneYOffset;
private boolean currentlyAddingPages;
private boolean memoryWarningShown;
private boolean pageFlowEnding;
private double scaling = 1.5;
private double pageFocus = 1;
private int currentZPosition = -1;
private boolean formsIgnoredStore;
private int pagesInMemory;
private final long memoryLimit;
private final Runtime runtime;
private final int pageLimit;
private boolean pageClickEvent;
private boolean enableReflection = true, enablePerspectiveTransform = true;
private void createScene() {
final ObservableList children = this.getChildren();
sceneXOffset = newSceneWidth / 2;
sceneYOffset = newSceneHeight / 2;
// Create 2 tone background colours.
backgroundTop = new Rectangle(0, 0, newSceneWidth, newSceneHeight / 2);
backgroundTop.setFill(new Color(55 / 255f, 55 / 255f, 65 / 255f, 1));
backgroundBottom = new Rectangle(0, newSceneHeight / 2, newSceneWidth, newSceneHeight / 2);
backgroundBottom.setFill(new Color(28 / 255f, 28 / 255f, 32 / 255f, 1));
// Create nav & zoom bars
navBar = new NavBar();
zoomBar = new ZoomBar();
perspectiveCheckBox = new CheckBox("Perspectives");
perspectiveCheckBox.setLayoutX(5);
perspectiveCheckBox.setLayoutY(5);
perspectiveCheckBox.setTextFill(Color.WHITE);
perspectiveCheckBox.setSelected(true);
perspectiveCheckBox.setOnAction(new EventHandler() {
@Override
public void handle(final javafx.event.ActionEvent actionEvent) {
togglePerspectives();
}
});
reflectionCheckBox = new CheckBox("Reflections");
reflectionCheckBox.setLayoutX(5);
reflectionCheckBox.setLayoutY(25);
reflectionCheckBox.setTextFill(Color.WHITE);
reflectionCheckBox.setSelected(true);
reflectionCheckBox.setOnAction(new EventHandler() {
@Override
public void handle(final javafx.event.ActionEvent actionEvent) {
toggleReflections();
}
});
if (DecoderOptions.isRunningOnLinux) {
toggleReflections();
togglePerspectives();
}
children.addAll(backgroundTop, backgroundBottom, navBar, zoomBar, perspectiveCheckBox, reflectionCheckBox);
setupMouseHandlers();
setupWindowResizeListeners();
addPages();
}
private double newSceneWidth;
private double newSceneHeight;
private void repositionObjects(){
newSceneWidth = getWidth();
newSceneHeight = getHeight();
sceneXOffset = newSceneWidth / 2;
totalPageWidth = pageCount * getPageWidthOrHeight();
sceneYOffset = newSceneHeight / 2;
navBar.update();
zoomBar.update();
Platform.runLater(new Runnable() {
@Override
public void run() {
backgroundTop.setWidth(newSceneWidth);
backgroundBottom.setWidth(newSceneWidth);
backgroundTop.setHeight(newSceneHeight);
backgroundBottom.setHeight(newSceneHeight);
backgroundBottom.setY(newSceneHeight / 2);
if (pages[pageNumber - 1] != null) {
pages[pageNumber - 1].setMain(true);
}
for (final Page page : pages) {
if (page != null) {
page.update();
}
}
}
});
}
// Listen out for window resizes to update the x,y,width,height of pages dynamically.
private void setupWindowResizeListeners() {
widthProperty().addListener(new ChangeListener() {
@Override
public void changed(final ObservableValue extends Number> observableValue, final Number oldSceneWidth, final Number newSceneWidth) {
repositionObjects();
}
});
heightProperty().addListener(new ChangeListener() {
@Override
public void changed(final ObservableValue extends Number> observableValue, final Number oldSceneHeight, final Number newSceneHeight) {
repositionObjects();
}
});
}
// volatile fields because they get used across threads so we don't want optimisations like caching.
private volatile double x; //oldX is used so we know how far the mouse has been dragged since the last event.
private volatile boolean isAnimating, stopAnimating;
private void setupMouseHandlers() {
/**
* ******************************************************
* Handle with care please, especially releasedHandler. *
*******************************************************
*/
// If moving then clicked, stop moving
setOnMousePressed(new EventHandler() {
@Override
public void handle(final MouseEvent mouseEvent) {
if (GUIDisplay.allowChangeCursor) {
setCursor(grabbingCursor);
}
if (navBar.isNavBarPress(mouseEvent)) {
// Event is handled in the event check
} else if (zoomBar.isZoomBarPress(mouseEvent)) {
// Event is handled in the event check
} else {
// If we are currently scrolling, tell it to stopAnimating
if (isAnimating) {
stopAnimating = true;
}
//Reset all X positions to current pageFocus.
x = mouseEvent.getSceneX();
}
}
});
// Move pages in the direction being dragged
setOnMouseDragged(new EventHandler() {
@Override
public void handle(final MouseEvent mouseEvent) {
if (navBar.isNavBarDrag(mouseEvent)) {
// The nav bar is handling the drag within navBar.isNavBarDrag().
} else if (zoomBar.isZoomBarDrag(mouseEvent)) {
// The zoom bar is handling the drag within zoomBar.isZoomBarDrag().
} else {
// Move the pages by the amount the mouse was dragged.
final double newPosition = pageFocus - (((mouseEvent.getSceneX() - x) / totalPageWidth) * 4 * pageCount);
if (newPosition > 1 && newPosition < pageCount) {
isAnimating = true;
reorderPages(pageFocus, false);
pageFocus = newPosition;
navBar.update();
for (final Page page : pages) {
if (page != null) {
page.update();
}
}
isAnimating = false;
}
final int newPageNumber = (int) (pageFocus + 0.5);
if (pageNumber != newPageNumber) {
setPageNumber(newPageNumber);
}
addPages();
// Set x positions for next mouse event to use.
x = mouseEvent.getSceneX();
}
}
});
// Move to center of nearest page
setOnMouseReleased(new EventHandler() {
@Override
public void handle(final MouseEvent mouseEvent) {
if (GUIDisplay.allowChangeCursor) {
setCursor(grabCursor);
}
//Reset cursor after delay
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
if (GUIDisplay.allowChangeCursor) {
setCursor(defaultCursor);
}
}
}, 350);
if (navBar.isNavBarRelease(mouseEvent)) {
// The nav bar is handling the release.
} else if (zoomBar.isZoomBarRelease()) {
// The zoom bar is handling the release.
} else {
if (!pageClickEvent) {
if (pageFocus < 1) {
pageFocus = 1;
} else if (pageFocus > pageCount) {
pageFocus = pageCount;
}
goTo((int) (pageFocus + 0.5));
} else {
pageClickEvent = false;
}
}
}
});
// Set default cursor when moving
setOnMouseMoved(new EventHandler() {
@Override
public void handle(final MouseEvent mouseEvent) {
if (navBar.isNavBarHover(mouseEvent) || zoomBar.isZoomBarHover(mouseEvent)) {
if (GUIDisplay.allowChangeCursor) {
setCursor(grabCursor);
}
} else {
if (GUIDisplay.allowChangeCursor) {
setCursor(defaultCursor);
}
}
}
});
// Left/right keyboard page change listener
setOnKeyPressed(new EventHandler() {
@Override
public void handle(final KeyEvent keyEvent) {
final KeyCode key = keyEvent.getCode();
switch (key) {
case RIGHT: {
final int dest = pageNumber + 1;
if (dest <= pageCount) {
goTo(dest);
}
break;
}
case LEFT: {
final int dest = pageNumber - 1;
if (dest > 0) {
goTo(dest);
}
break;
}
case R: {
toggleReflections();
break;
}
case T: {
togglePerspectives();
break;
}
}
}
});
// Scroll pages or scroll zoom if ctrl held.
setOnScroll(new EventHandler() {
@Override
public void handle(final ScrollEvent event) {
final double value = event.getDeltaY();
if (event.isControlDown()) {
if (value < 0) {
if (scaling < 2) {
scaling += 0.1;
if (scaling > 2) {
scaling = 2;
}
zoomBar.update();
for (final Page page : pages) {
if (page != null) {
page.update();
}
}
}
} else if ((value > 0) && (scaling > 1)) {
scaling -= 0.1;
if (scaling < 1) {
scaling = 1;
}
zoomBar.update();
for (final Page page : pages) {
if (page != null) {
page.update();
}
}
}
} else {
if (value > 0) {
final int dest = pageNumber - 1;
if (dest > 0) {
goTo(dest);
}
} else {
final int dest = pageNumber + 1;
if (dest <= pageCount) {
goTo(dest);
}
}
}
}
});
// Double click to toggle max/min zoom
setOnMouseClicked(new EventHandler() {
@Override
public void handle(final MouseEvent mouseEvent) {
if (mouseEvent.getClickCount() == 2) {
if (scaling != 1) {
scaling = 1;
} else {
scaling = 2;
}
zoomBar.update();
for (final Page page : pages) {
if (page != null) {
page.update();
}
}
}
}
});
}
// Z positioning in JavaFX is based on the order nodes appear in the Node list/tree.
// Reorder needs to be forced when pages get added.
private void reorderPages(final double pageFocus, final boolean forceReorder) {
final int position = (int) (pageFocus + 0.5);
// Check if reorder is required
if (!forceReorder && (currentZPosition == position || position < 1 || position > pageCount)) {
return;
}
currentZPosition = position;
Platform.runLater(new Runnable() {
@Override
public void run() {
// Add nodes behind the pages. e.g. background
getChildren().clear();
getChildren().add(backgroundTop);
getChildren().add(backgroundBottom);
// Add pages to the right of the current page, then add those to the left.
int i = pageCount;
while (i > position) {
if (pages[i - 1] != null) {
getChildren().add(pages[i - 1]);
if (enableReflection) {
getChildren().add(pages[i - 1].getReflection());
}
}
i--;
}
i = 1;
while (i < position) {
if (pages[i - 1] != null) {
getChildren().add(pages[i - 1]);
if (enableReflection) {
getChildren().add(pages[i - 1].getReflection());
}
}
i++;
}
// Add main page
if (pages[position - 1] != null) {
getChildren().add(pages[position - 1]);
if (enableReflection) {
getChildren().add(pages[position - 1].getReflection());
}
}
// Add nodes appearing in front of pages e.g. nav/zoom controls.
getChildren().add(navBar);
getChildren().add(zoomBar);
getChildren().add(perspectiveCheckBox);
getChildren().add(reflectionCheckBox);
}
});
}
public void setRotation(final int displayRotation) {
//Reload pages if rotation changes
if (this.displayRotation != displayRotation) {
this.displayRotation = displayRotation;
for (final Page p : pages) {
if (p != null) {
p.dispose();
}
}
stop();
stopAddingPages = false;
goTo(pageNumber);
}
}
private void toggleReflections() {
if (enableReflection) {
enableReflection = false;
Platform.runLater(new Runnable() {
@Override
public void run() {
reflectionCheckBox.setSelected(false);
}
});
for (final Page page : pages) {
if (page != null) {
page.disposeReflection();
}
}
reorderPages(pageFocus, true);
} else {
if (enablePerspectiveTransform) {
enableReflection = true;
Platform.runLater(new Runnable() {
@Override
public void run() {
reflectionCheckBox.setSelected(true);
}
});
for (final Page page : pages) {
if (page != null) {
page.setupReflection();
page.update();
}
}
reorderPages(pageFocus, true);
}
}
}
private void togglePerspectives() {
if (enablePerspectiveTransform) {
if (enableReflection) {
toggleReflections();
}
enablePerspectiveTransform = false;
Platform.runLater(new Runnable() {
@Override
public void run() {
perspectiveCheckBox.setSelected(false);
reflectionCheckBox.setDisable(true);
}
});
for (final Page page : pages) {
if (page != null) {
page.disposePerspectiveTransform();
page.update();
}
}
} else {
enablePerspectiveTransform = true;
Platform.runLater(new Runnable() {
@Override
public void run() {
perspectiveCheckBox.setSelected(true);
reflectionCheckBox.setDisable(false);
}
});
for (final Page page : pages) {
if (page != null) {
page.setupPerspectiveTransform();
page.update();
}
}
}
}
public void setCursors(final BufferedImage grab, final BufferedImage grabbing) {
if (grab != null) {
grabCursor = new ImageCursor(SwingFXUtils.toFXImage(grab, new WritableImage(grab.getWidth(), grab.getHeight())), 8, 8);
} else {
grabCursor = javafx.scene.Cursor.DEFAULT;
}
if (grabbing != null) {
grabbingCursor = new ImageCursor(SwingFXUtils.toFXImage(grabbing, new WritableImage(grabbing.getWidth(), grabbing.getHeight())), 8, 8);
} else {
grabbingCursor = javafx.scene.Cursor.DEFAULT;
}
defaultCursor = javafx.scene.Cursor.DEFAULT;
}
private int newDestination;
private double speed;
public void goTo(final int firstDestination) {
setPageNumber(firstDestination);
// This is required because when we drag the pages around, the main does not get unset due to the updating of the page number as we drag.
for (int i = 0; i < pageCount; i++) {
if (pages[i] != null && i != pageNumber - 1) {
pages[i].setMain(false);
}
}
if (pages[pageNumber - 1] != null) {
pages[pageNumber - 1].setMain(true);
}
//update pdfdecoder's pagenumber (-100 flag to prevent loop)
pdf.setPageParameters(-100f, firstDestination);//Purpose is to maintain same page when swapping to Single Pages view-mode.
addPages();
//If already moving set variable to let thread know destination has changed
if (isAnimating) {
newDestination = firstDestination;
return;
}
final Thread thread = new Thread("PageFlow-goTo") {
@Override
public void run() {
int destination = firstDestination;
while (!stopAnimating
&& (pageFocus > destination || pageFocus < destination)) {
//Pick up if destination changed
if (newDestination != 0) {
destination = newDestination;
newDestination = 0;
}
//accelerate
if (pageFocus < destination) {
if (speed < 0.2f) {
speed = 0.2f;
}
speed *= 1.15f;
} else {
if (speed > -0.2f) {
speed = -0.2f;
}
speed *= 1.15f;
}
//cap speed
final double maxSpeed = (destination - pageFocus) / 4;
if (Math.abs(speed) > Math.abs(maxSpeed)) {
speed = maxSpeed;
}
//update page positions
pageFocus += speed;
// If close to int, round off to an int as it improves the image quality and reduces number of updates.
// Don't be too greedy otherwise it will cause pages to jump into final position when they near it.
if (pageFocus - (int) pageFocus > 0.99) {
pageFocus = (int) pageFocus + 1;
} else if (pageFocus - (int) pageFocus < 0.01) {
pageFocus = (int) pageFocus;
}
navBar.update();
reorderPages(pageFocus, false);
for (final Page page : pages) {
if (page != null) {
page.update();
}
}
try {
Thread.sleep(40); // 25fps
} catch (final Exception e) {
//tell user and log
if (LogWriter.isOutput()) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
//
}
//Pick up if destination changed
if (newDestination != 0) {
destination = newDestination;
newDestination = 0;
}
}
stopAnimating = false;
isAnimating = false;
}
};
thread.setDaemon(true);
isAnimating = true;
thread.start();
}
private synchronized Image getPageImage(final int pageNumber, final int rotation, final int quality) {
//Only generate a hi res texture for the current page
if (pageNumber != this.pageNumber && quality > textureSize) {
return null;
}
final int width = pdf.getPdfPageData().getCropBoxWidth(pageNumber);
final int height = pdf.getPdfPageData().getCropBoxHeight(pageNumber);
final float scale;
if (width > height) {
scale = (float) quality / width;
} else {
scale = (float) quality / height;
}
try {
final float currentScaling = pdf.getScaling();
pdf.setScaling(scale);
final BufferedImage raw = pdf.getPageAsImage(pageNumber);
pdf.setScaling(currentScaling);
final BufferedImage result = new BufferedImage(quality, quality, BufferedImage.TYPE_INT_ARGB);
final java.awt.Graphics2D g2 = (java.awt.Graphics2D) result.getGraphics();
g2.rotate((rotation / 180.0) * Math.PI, quality / 2, quality / 2);
final int x = (quality - raw.getWidth()) / 2;
final int y = quality - raw.getHeight();
g2.drawImage(raw, x, y, raw.getWidth(), raw.getHeight(), null);
return SwingFXUtils.toFXImage(result, new WritableImage(result.getWidth(), result.getHeight()));
} catch (final Exception e) {
//tell user and log
if (LogWriter.isOutput()) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
//
}
return null;
}
// Clean up
@SuppressWarnings("UnusedDeclaration")
public void dispose() {
// Dispose of pages & containers
for (final Page page : pages) {
if (page != null) {
page.dispose();
}
}
pages = null;
//Restore forms setting
final AcroRenderer formRenderer = pdf.getFormRenderer();
if (formRenderer != null) {
formRenderer.setIgnoreForms(formsIgnoredStore);
}
// System.gc();
}
public void stop() {
stopAddingPages = true;
if(!isFX){
while (currentlyAddingPages) {
try {
Thread.sleep(100);
} catch (final InterruptedException e) {
//tell user and log
if (LogWriter.isOutput()) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
//
}
}
}
}
/**
* Add pages around the current page
*/
private void addPages() {
final Task task = new Task() {
@Override
protected Void call() throws Exception {
currentlyAddingPages = true;
int firstPage = pageNumber;
//Add pages on either side
for (int i = 0; i <= pagesToGenerate; i++) {
if (checkMemory()) {
return null;
}
final int spacesLeft = ((pagesToGenerate * 2) - 1) - pagesInMemory;
if (spacesLeft < 2) {
removeFurthestPages(2 - spacesLeft);
}
if (i == pagesToGenerate - 1) {
long used = runtime.totalMemory() - runtime.freeMemory();
if (used < memoryLimit && pagesToGenerate < pageCount && pagesToGenerate < pageLimit) {
pagesToGenerate++;
} else {
used = runtime.totalMemory() - runtime.freeMemory();
if (used < memoryLimit && pagesToGenerate < pageCount && pagesToGenerate < pageLimit) {
pagesToGenerate++;
}
}
}
if (stopAddingPages) {
currentlyAddingPages = false;
stopAddingPages = false;
return null;
}
int pn = firstPage + i;
//if 40 from center start doing only even pages
if (i > 40) {
pn += i - 40;
pn -= (pn & 1);
//fill in the odd pages
if (pn > pageCount) {
pn -= (pageCount - (firstPage + 40));
if ((pn & 1) == 0) {
pn--;
}
}
}
if (pn <= pageCount && pages != null && pages[pn - 1] == null) {
try {
final Page page = new Page(pn);
if (pages != null) {
pages[pn - 1] = page;
reorderPages(pageFocus, true);
pagesInMemory++;
if (pn == pageNumber) {
page.setMain(true);
}
}
} catch (final Exception e) {
if (pages != null) {
pages[pn - 1] = null;
}
pagesInMemory--;
//tell user and log
if (LogWriter.isOutput()) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
//
}
}
if (stopAddingPages) {
currentlyAddingPages = false;
stopAddingPages = false;
return null;
}
pn = firstPage - i;
//if 40 from center start doing only even pages
if (i > 40) {
pn -= i - 40;
pn += (pn & 1);
//fill in the odd pages
if (pn < 1) {
pn += (firstPage - 41);
if ((pn & 1) == 0) {
pn--;
}
}
}
if (pn > 0 && pages != null && pages[pn - 1] == null) {
final Page page = new Page(pn);
if (pages != null) {
pages[pn - 1] = page;
reorderPages(pageFocus, true);
pagesInMemory++;
}
}
//update page range to generate
if (firstPage != pageNumber) {
i = -1;
firstPage = pageNumber;
}
//Mandatory sleep so Swing can do it's thing
if (i > 10) {
while ((speed > 0.005f || speed < -0.005f) && firstPage == pageNumber) {
try {
Thread.sleep(10);
} catch (final InterruptedException e) {
//tell user and log
if (LogWriter.isOutput()) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
//
}
}
}
}
currentlyAddingPages = false;
return null;
}
};
final Thread th = new Thread(task);
if (!currentlyAddingPages) {
currentlyAddingPages = true;
th.setDaemon(true);
th.start();
}
}
private boolean checkMemory() {
final int threshold = 32000000;
final boolean debugMemory = false;
if (runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory()) < threshold) {
if (debugMemory) {
System.out.println("mem less than threshold - " + pagesInMemory + " pages in memory");
}
if (pagesInMemory > 1) {
if (debugMemory) {
System.out.println("Clearing old pages, calling GC and retesting");
}
// System.gc();
boolean shrinkingSuccessful = true;
while (runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory()) < threshold) {
if (pagesToGenerate > 5) {
pagesToGenerate--;
final int toRemove = pagesInMemory - ((pagesToGenerate * 2) - 1);
if (toRemove > 0) {
removeFurthestPages(toRemove);
}
// System.gc();
} else {
shrinkingSuccessful = false;
}
}
if (shrinkingSuccessful) {
return false;
}
if (runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory()) < threshold) {
if (!memoryWarningShown) {
if (debugMemory) {
System.out.println("Warning about memory issues.");
}
if (!pageFlowEnding) {
isUpdateMemory = true;
memoryMessage = Messages.getMessage("PdfViewer.PageFlowLowMemory");
}else{
isUpdateMemory = false;
}
memoryWarningShown = true;
}
if (debugMemory) {
System.out.println("Testing finished - no more pages will be added.");
}
currentlyAddingPages = false;
return true;
}
} else {
if (debugMemory) {
System.out.println("Removing and cleaning up");
}
stop();
// canvas.stopRenderer(); // No FX equivalent
if (Platform.isFxApplicationThread()) {
// currentGUI.setDisplayView(Display.SINGLE_PAGE, Display.DISPLAY_CENTERED);
} else {
final Runnable doPaintComponent = new Runnable() {
@Override
public void run() {
// currentGUI.setDisplayView(Display.SINGLE_PAGE, Display.DISPLAY_CENTERED);
}
};
Platform.runLater(doPaintComponent);
}
// pdf.setDisplayView(Display.SINGLE_PAGE, Display.DISPLAY_CENTERED); // Commented in Java3D also
if (!pageFlowEnding) {
pageFlowEnding = true;
isUpdateMemory = true;
memoryMessage = Messages.getMessage("PdfViewer.PageFlowNotEnoughMemory");
}else{
isUpdateMemory = false;
}
return true;
}
}
if (debugMemory) {
System.out.println("Testing finished - adding may resume");
}
return false;
}
private void removeFurthestPages(final int pagesToRemove) {
int pagesRemoved = 0;
final int before = pageNumber - 1;
final int after = pageCount - pageNumber;
final int max = before > after ? before : after;
int cursor = max;
//Remove even pages further than 40 from center first
while (pagesRemoved < pagesToRemove) {
if (cursor < 40) {
break;
}
int pre = (pageNumber - cursor);
pre -= (1 - (pre & 1));
if (pre > 0 && pages[pre - 1] != null) {
pages[pre - 1].dispose();
pagesRemoved++;
}
if (pagesRemoved != pagesToRemove) {
int post = pageNumber + cursor;
post -= (1 - (post & 1));
if (post <= pageCount && pages[post - 1] != null) {
pages[post - 1].dispose();
pagesRemoved++;
}
cursor--;
}
}
cursor = max;
//remove all pages furthest from center now
while (pagesRemoved < pagesToRemove) {
if (cursor < 0) {
break;
}
final int pre = (pageNumber - cursor);
if (pre > 0 && pages[pre - 1] != null) {
pages[pre - 1].dispose();
pagesRemoved++;
}
if (pagesRemoved != pagesToRemove) {
final int post = pageNumber + cursor;
if (post <= pageCount && pages[post - 1] != null) {
pages[post - 1].dispose();
pagesRemoved++;
}
cursor--;
}
}
// System.gc();
}
private double getFullPageWidthOrHeight() {
return (newSceneHeight / (double) 13) * 12;
}
private double getPageWidthOrHeight() {
return (newSceneHeight / (13 * scaling)) * 12;
}
private class Page extends ImageView {
private final Image lowResImage;
private final int page;
private final int rotation;
private PerspectiveTransform trans;
private ColorAdjust colorAdjust;
private int mainTextureSize;
private ImageView reflection;
private PerspectiveTransform reflectionTransform;
private double x, y, widthHeight, altWidthHeight;
Page(final int page) {
this.rotation = displayRotation;
this.page = page;
lowResImage = getPageImage(page, rotation, textureSize);
if (lowResImage == null) {
dispose();
widthHeight = 0;
return;
}
setImage(lowResImage);
setupMouseHandlers();
if (enableReflection) {
setupReflection();
}
colorAdjust = new ColorAdjust();
if (enablePerspectiveTransform) {
setupPerspectiveTransform();
} else {
//setEffect(colorAdjust); // This causes lag too.
}
setCache(true);
setCacheHint(CacheHint.QUALITY); // This seems to have little effect
update();
}
public void setupReflection() {
reflection = new ImageView();
reflectionTransform = new PerspectiveTransform();
reflectionTransform.setInput(new ColorAdjust(0, 0, -0.75, 0));
reflection.setEffect(reflectionTransform);
reflection.setImage(lowResImage);
}
public void disposeReflection() {
reflection = null;
reflectionTransform = null;
}
public void setupPerspectiveTransform() {
trans = new PerspectiveTransform();
trans.setInput(colorAdjust);
setEffect(trans);
}
public void disposePerspectiveTransform() {
trans = null;
setEffect(null);
//setEffect(colorAdjust); // This causes lag too.
}
private void setupMouseHandlers() {
/**
* pageClickEvent is required to let the scene know that we are
* handling the mouse event. setOnMouseClicked still runs even when
* the click includes a drag, hence detecting manually.
*/
setOnMousePressed(new EventHandler() {
@Override
public void handle(final MouseEvent e) {
pageClickEvent = true;
}
});
setOnMouseDragged(new EventHandler() {
@Override
public void handle(final MouseEvent e) {
pageClickEvent = false;
}
});
setOnMouseReleased(new EventHandler() {
@Override
public void handle(final MouseEvent e) {
if (pageClickEvent) {
goTo(page);
}
}
});
}
// Update pageFocus values then redraw.
public void update() {
// Set width based on height.
// Set height based on size of scene & current scaling.
widthHeight = getPageWidthOrHeight();
// Set y to move page up slightly.
y = -widthHeight / 40;
// Set x based on current pageFocus.
// If diff > 1 then put pages closer together (divide distance by 5 essentially).
double diff = page - pageFocus;
if (diff > 1) {
diff = ((diff - 1) / 5) + 1;
} else if (diff < -1) {
diff = ((diff + 1) / 5) - 1;
}
x = diff * widthHeight;
redraw();
}
private void redraw() {
// If new or old pageFocus is visible then update pageFocus & transform.
final boolean update = !enablePerspectiveTransform || trans != null && ((getRealX(x) + widthHeight > 0 && getRealX(x) < newSceneWidth)
|| (trans.getUrx() > 0 && trans.getUlx() < newSceneWidth));
if (update) {
Platform.runLater(new Runnable() {
@Override
public void run() {
// Use distance from current pageFocus to define the transform.
double diff = page - pageFocus;
double ddiff = Math.abs(diff);
if (ddiff > 1) {
ddiff = ((ddiff - 1) / (enablePerspectiveTransform ? 16 : 8)) + 1;
}
final double distanceScale = Math.pow(1 - 0.3, ddiff);
if (diff > 1) {
diff = 1;
}
if (diff < -1) {
diff = -1;
}
final boolean rightSide = diff > 0;
diff = Math.abs(diff);
// If the page is in the middle, turn off the PerspectiveTransform as it will sharpen the image by a good margin!
if (diff == 0) {
setEffect(null);
} else {
if (enablePerspectiveTransform) {
setEffect(trans);
}
}
colorAdjust.setBrightness(-diff / 2);
final double halfWidthHeight = widthHeight / 2;
double halfDiff = diff / 2;
if (!enablePerspectiveTransform) {
halfDiff /= 8;
}
final double quarterDiff = diff / 4;
//Set X values
final double lx = getRealX(halfWidthHeight + x - (1 - halfDiff) * halfWidthHeight * distanceScale);
final double rx = getRealX(halfWidthHeight + x + (1 - halfDiff) * halfWidthHeight * distanceScale);
if (enablePerspectiveTransform) {
trans.setLlx(lx);
trans.setUlx(lx);
trans.setLrx(rx);
trans.setUrx(rx);
// Set Y values
if (rightSide) { // Slant to left
trans.setLly(getRealY(halfWidthHeight + y + (1 - quarterDiff) * halfWidthHeight * distanceScale));
trans.setUly(getRealY(halfWidthHeight + y - (1 - quarterDiff) * halfWidthHeight * distanceScale));
trans.setLry(getRealY(halfWidthHeight + y + halfWidthHeight * distanceScale));
trans.setUry(getRealY(halfWidthHeight + y - halfWidthHeight * distanceScale));
} else { // Slant to right
trans.setLry(getRealY(halfWidthHeight + y + (1 - quarterDiff) * halfWidthHeight * distanceScale));
trans.setUry(getRealY(halfWidthHeight + y - (1 - quarterDiff) * halfWidthHeight * distanceScale));
trans.setLly(getRealY(halfWidthHeight + y + halfWidthHeight * distanceScale));
trans.setUly(getRealY(halfWidthHeight + y - halfWidthHeight * distanceScale));
}
}
if (enableReflection) {
// Set reflection X values
reflectionTransform.setLlx(lx);
reflectionTransform.setUlx(lx);
reflectionTransform.setLrx(rx);
reflectionTransform.setUrx(rx);
// Set reflection y values
reflectionTransform.setLly(trans.getLly());
reflectionTransform.setLry(trans.getLry());
reflectionTransform.setUly(trans.getLly() + (trans.getLly() - trans.getUly()));
reflectionTransform.setUry(trans.getLry() + (trans.getLry() - trans.getUry()));
}
// Set X,Y,Width,Height.
// This is redundant after setting the PerspectiveTransform but still useful to set.
if (!enablePerspectiveTransform) {
altWidthHeight = (rx - lx);
if (diff == 0) {
setFitWidth((int) altWidthHeight);
setFitHeight((int) altWidthHeight);
} else {
setFitWidth(altWidthHeight);
setFitHeight(altWidthHeight);
}
} else {
if (diff == 0) {
setFitWidth((int) widthHeight);
setFitHeight((int) widthHeight);
} else {
setFitWidth(widthHeight);
setFitHeight(widthHeight);
}
}
if (diff == 0) {
setX((int) getRealX(x));
setY((int) getRealY(y));
} else {
setX(getRealX(x));
setY(getRealY(y));
}
}
});
}
}
public void setMain(final boolean isMain) {
if (isMain) {
final Thread t = new Thread("FX-setMain") {
@Override
public void run() {
if (checkMemory()) {
return;
}
// if (!isFX) {
mainTextureSize = (int) getFullPageWidthOrHeight();
final Image img = getPageImage(page, rotation, mainTextureSize);
Platform.runLater(new Runnable() {
@Override
public void run() {
if (img != null) {
setImage(img);
}
}
});
// }
}
};
t.setDaemon(true);
t.start();
} else {
Platform.runLater(new Runnable() {
@Override
public void run() {
setImage(lowResImage);
}
});
}
}
private ImageView getReflection() {
return reflection;
}
// Convert an x pageFocus into a real world coordinate.
private double getRealX(final double x) {
return sceneXOffset + getXOffset() + x;
}
// Convert a y pageFocus into a real world coordinate.
private double getRealY(final double y) {
return sceneYOffset + getYOffset() + y;
}
private double getXOffset() {
if (enablePerspectiveTransform) {
return -widthHeight / 2;
} else {
return -altWidthHeight / 2;
}
}
private double getYOffset() {
if (enablePerspectiveTransform) {
return -widthHeight / 2;
} else {
return -altWidthHeight / 2;
}
}
public void dispose() {
Platform.runLater(new Runnable() {
@Override
public void run() {
setImage(null);
}
});
pagesInMemory--;
pages[page - 1] = null;
}
}
private class NavBar extends Parent {
private final Line navLine;
private final Circle navCircle;
private static final int distanceFromSides = 20;
private static final int distanceFromBottom = 15;
private boolean handlingMouse;
NavBar() {
navLine = new Line();
navLine.setStrokeWidth(1.5);
navLine.setStroke(Color.WHITE);
navCircle = new Circle(5);
navCircle.setStrokeWidth(2);
navCircle.setStroke(Color.WHITE);
navCircle.setFill(Color.GRAY);
getChildren().addAll(navLine, navCircle);
}
public boolean isNavBarHover(final MouseEvent mouseEvent) {
return mouseEvent.getY() > newSceneHeight - (distanceFromBottom * 2);
}
public boolean isNavBarPress(final MouseEvent mouseEvent) {
if (mouseEvent.getY() > newSceneHeight - (distanceFromBottom * 2)) {
handlingMouse = true;
return true;
} else {
return false;
}
}
public boolean isNavBarDrag(final MouseEvent mouseEvent) {
if (handlingMouse) {
double x = mouseEvent.getX();
if (x < distanceFromSides) {
x = distanceFromSides;
}
if (x > newSceneWidth - distanceFromSides) {
x = newSceneWidth - distanceFromSides;
}
if (x != navCircle.getCenterX()) {
navCircle.setCenterX(x);
final double percent = (x - distanceFromSides) / (newSceneWidth - (distanceFromSides * 2));
pageFocus = ((pageCount - 1) * percent) + 1;
final int newPageNumber = (int) (pageFocus + 0.5);
if (pageNumber != newPageNumber) {
setPageNumber(newPageNumber);
}
addPages();
reorderPages(pageFocus, false);
for (final Page page : pages) {
if (page != null) {
page.update();
}
}
}
return true;
} else {
return false;
}
}
public boolean isNavBarRelease(final MouseEvent mouseEvent) {
if (handlingMouse) {
double x = mouseEvent.getX();
if (x < distanceFromSides) {
x = distanceFromSides;
}
if (x > newSceneWidth - distanceFromSides) {
x = newSceneWidth - distanceFromSides;
}
final double percent = (x - distanceFromSides) / (newSceneWidth - (distanceFromSides * 2));
final int newPageNumber = (int) ((((pageCount - 1) * percent) + 1) + 0.5);
if (pageNumber != newPageNumber) {
setPageNumber(newPageNumber);
}
goTo(pageNumber);
handlingMouse = false;
return true;
} else {
return false;
}
}
public void update() {
Platform.runLater(new Runnable() {
@Override
public void run() {
navCircle.setCenterY(newSceneHeight - distanceFromBottom);
navLine.setStartX(distanceFromSides);
navLine.setStartY(newSceneHeight - distanceFromBottom + 0.5);// +0.5 to make pixel perfect line
navLine.setEndX(newSceneWidth - distanceFromSides);
navLine.setEndY(newSceneHeight - distanceFromBottom + 0.5);// +0.5 to make pixel perfect line
final double percent = (pageFocus - 1) / (pageCount - 1);
final double x = distanceFromSides + ((newSceneWidth - (distanceFromSides * 2)) * percent);
navCircle.setCenterX(x);
}
});
}
}
private class ZoomBar extends Parent {
private final Line zoomLine;
private final Circle zoomCircle;
private static final int distanceFromSide = 15;
private boolean handlingMouse;
ZoomBar() {
zoomLine = new Line();
zoomLine.setStrokeWidth(1.5);
zoomLine.setStroke(Color.WHITESMOKE);
zoomCircle = new Circle(5);
zoomCircle.setStrokeWidth(2);
zoomCircle.setStroke(Color.WHITE);
zoomCircle.setFill(Color.GRAY);
getChildren().addAll(zoomLine, zoomCircle);
}
public boolean isZoomBarHover(final MouseEvent mouseEvent) {
return mouseEvent.getX() < distanceFromSide * 2
&& mouseEvent.getY() > getStartY() - 5 && mouseEvent.getY() < getEndY() + 5; // Allow +/- 5
}
public boolean isZoomBarPress(final MouseEvent mouseEvent) {
if (mouseEvent.getX() < distanceFromSide * 2
&& mouseEvent.getY() > getStartY() - 5 && mouseEvent.getY() < getEndY() + 5) { // Allow +/- 5
handlingMouse = true;
isZoomBarDrag(mouseEvent);// Borrow what happens with the drag!
return true;
} else {
return false;
}
}
public boolean isZoomBarDrag(final MouseEvent mouseEvent) {
if (handlingMouse) {
double y = mouseEvent.getY();
final double start = getStartY();
final double end = getEndY();
if (y < start) {
y = start;
}
if (y > end) {
y = end;
}
if (y != zoomCircle.getCenterY()) {
zoomCircle.setCenterY(y);
final double percent = (y - start) / (end - start);
scaling = 1 + percent;
for (final Page page : pages) {
if (page != null) {
page.update();
}
}
}
return true;
} else {
return false;
}
}
public boolean isZoomBarRelease() {
if (handlingMouse) {
handlingMouse = false;
return true;
} else {
return false;
}
}
public void update() {
Platform.runLater(new Runnable() {
@Override
public void run() {
zoomCircle.setCenterX(distanceFromSide);
zoomLine.setStartX(distanceFromSide + 0.5);// +0.5 to make pixel perfect line
zoomLine.setStartY(newSceneHeight * 0.2);
zoomLine.setEndX(distanceFromSide + 0.5);// +0.5 to make pixel perfect line
zoomLine.setEndY(newSceneHeight * 0.4);
final double percent = 2 - scaling;
final double start = getStartY();
final double end = getEndY();
zoomCircle.setCenterY(end - ((end - start) * percent));
}
});
}
private double getStartY() {
return newSceneHeight * 0.2;
}
private double getEndY() {
return newSceneHeight * 0.4;
}
}
public PdfDecoderInt getPdfDecoderInt() {
return pdf;
}
final DoubleProperty pageNumberProperty = new SimpleDoubleProperty();
public DoubleProperty getPageNumber() {
return pageNumberProperty;
}
private void setPageNumber(final int pn){
pageNumberProperty.set(pn);
pageNumber = pn;
}
private boolean isUpdateMemory;
public boolean isUpdateMemory(){
return isUpdateMemory;
}
private String memoryMessage;
public String getMemoryMessage(){
return memoryMessage;
}
}