diff mupdf-source/platform/java/example/Viewer.java @ 3:2c135c81b16c

MERGE: upstream PyMuPDF 1.26.4 with MuPDF 1.26.7
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:44:09 +0200
parents b50eed0cc0ef
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/platform/java/example/Viewer.java	Mon Sep 15 11:44:09 2025 +0200
@@ -0,0 +1,2085 @@
+// Copyright (C) 2004-2022 Artifex Software, Inc.
+//
+// This file is part of MuPDF.
+//
+// MuPDF is free software: you can redistribute it and/or modify it under the
+// terms of the GNU Affero General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// MuPDF 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 Affero General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
+//
+// Alternative licensing terms are available from the licensor.
+// For commercial licensing, see <https://www.artifex.com/> or contact
+// Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
+// CA 94129, USA, for further information.
+
+package example;
+
+import com.artifex.mupdf.fitz.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.image.*;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.lang.reflect.Field;
+import java.util.Vector;
+
+public class Viewer extends Frame implements WindowListener, ActionListener, ItemListener, KeyListener, MouseWheelListener, Context.Log, ViewerCore.Callback
+{
+	protected Worker worker;
+	protected ViewerCore doc;
+	protected Location location;
+	protected int chapterNumber;
+	protected int pageNumber;
+	protected ViewerCore.OutlineItem[] outline;
+
+	protected int layoutWidth = 450;
+	protected int layoutHeight = 600;
+	protected int layoutEm = 12;
+
+	protected String title;
+	protected String author;
+	protected String format;
+	protected String encryption;
+	protected boolean print;
+	protected boolean copy;
+	protected boolean edit;
+	protected boolean annotate;
+	protected boolean form;
+	protected boolean accessibility;
+	protected boolean assemble;
+	protected boolean printHq;
+	protected boolean reflowable;
+	protected boolean linearized;
+	protected int updates;
+	protected int firstUpdate;
+	protected int chapters;
+	protected int pages;
+
+	protected Pixmap pixmap;
+	protected Rect bbox;
+	protected Rect[] links;
+	protected String[] linkURIs;
+	protected Quad[][] hits;
+
+	protected int pixmapWidth;
+	protected int pixmapHeight;
+	protected float pixelScale;
+	protected int screenDPI;
+	protected Dimension screenSize;
+
+	protected int searchDirection = 1;
+	protected String searchNeedle = null;
+
+	protected final int MIN_ZOOM_DPI = 18;
+	protected final int MAX_ZOOM_DPI = 288;
+	protected int[] zoomList = {
+		18, 24, 36, 54, 72, 96, 120, 144, 180, 216, 288
+	};
+	protected boolean customZoom = false;
+	protected int currentDPI = 72;
+
+	protected int rotate = 0;
+	protected boolean icc = true;
+	protected int antialias = 8;
+	protected boolean invert = false;
+	protected boolean tint = false;
+	protected int tintBlack = 0x303030;
+	protected int tintWhite = 0xFFFFF0;
+	protected boolean showLinks = false;
+	protected boolean isFullscreen = false;
+
+	protected ScrollPane pageScroll;
+	protected Panel pageHolder;
+	protected PageCanvas pageCanvas;
+
+	protected Button firstButton, prevButton, nextButton, lastButton;
+	protected TextField pageField;
+	protected Label pageLabel;
+	protected Button zoomInButton, zoomOutButton;
+	protected Choice zoomChoice;
+	protected Panel reflowPanel;
+	protected Button fontIncButton, fontDecButton;
+	protected Label fontSizeLabel;
+
+	protected TextField searchField;
+	protected Button searchPrevButton, searchNextButton;
+	protected Panel searchStatusPanel;
+	protected Label searchStatus;
+
+	protected Panel outlinePanel;
+	protected List outlineList;
+
+	protected OCRProgressmeter OCRmeter;
+	protected RenderProgressmeter renderMeter;
+
+	protected int number = 0;
+
+	protected class Mark {
+		Location loc;
+
+		protected Mark(Location loc) {
+			this.loc = loc;
+		}
+	}
+
+	protected int historyCount = 0;
+	protected Mark[] history = new Mark[256];
+	protected int futureCount = 0;
+	protected Mark[] future = new Mark[256];
+	protected Mark[] marks = new Mark[10];
+
+	protected static void messageBox(Frame owner, String title, String message) {
+		final Dialog box = new Dialog(owner, title, true);
+		box.add(new Label(message), BorderLayout.CENTER);
+		Panel buttonPane = new Panel(new FlowLayout());
+		Button okayButton = new Button("Okay");
+		okayButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				box.setVisible(false);
+			}
+		});
+		buttonPane.add(okayButton);
+		box.add(buttonPane, BorderLayout.SOUTH);
+		box.pack();
+		box.setVisible(true);
+		box.dispose();
+	}
+
+	protected static String passwordDialog(Frame owner, String title) {
+		final Dialog box = new Dialog(owner, title, true);
+		final TextField textField = new TextField(20);
+		textField.setEchoChar('*');
+		Panel buttonPane = new Panel(new FlowLayout());
+		Button cancelButton = new Button("Cancel");
+		Button okayButton = new Button("Okay");
+		cancelButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				textField.setText("");
+				box.setVisible(false);
+			}
+		});
+		okayButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				box.setVisible(false);
+			}
+		});
+		box.add(new Label("Password:"), BorderLayout.NORTH);
+		box.add(textField, BorderLayout.CENTER);
+		buttonPane.add(cancelButton);
+		buttonPane.add(okayButton);
+		box.add(buttonPane, BorderLayout.SOUTH);
+		box.pack();
+		box.setVisible(true);
+		box.dispose();
+		String pwd = textField.getText();
+		if (pwd.length() == 0)
+			return null;
+		return pwd;
+	}
+
+	public class LogDialog extends Dialog implements ActionListener, KeyListener{
+		TextArea info = new TextArea("");
+		Button okay = new Button("Okay");
+
+		public LogDialog(Frame parent, String title, String message) {
+			super(parent, title, true);
+
+			setLayout(new GridLayout(2, 1));
+			info.setText(message);
+			info.setEnabled(false);
+			add(info);
+
+			Panel buttonPanel = new Panel(new FlowLayout(FlowLayout.CENTER));
+			{
+				okay.addActionListener(this);
+				okay.addKeyListener(this);
+				buttonPanel.add(okay);
+			}
+			add(buttonPanel);
+
+			pack();
+			setResizable(false);
+			okay.requestFocusInWindow();
+			setLocationRelativeTo(parent);
+			setVisible(true);
+		}
+
+		public void actionPerformed(ActionEvent e) {
+			if (e.getSource() == okay)
+				dispose();
+		}
+
+		public void keyPressed(KeyEvent e) { }
+		public void keyReleased(KeyEvent e) { }
+
+		public void keyTyped(KeyEvent e) {
+			if (e.getKeyChar() == '\u001b')
+				dispose();
+		}
+	}
+
+	public void exception(Throwable t) {
+		StringWriter sw = new StringWriter();
+		PrintWriter pw = new PrintWriter(sw);
+		t.printStackTrace(pw);
+		System.out.println("Exception: " + sw.toString());
+		LogDialog ld = new LogDialog(this, "Exception!", "Exception: " + sw.toString());
+	}
+
+	public void error(String message) {
+		LogDialog ld = new LogDialog(this, "Error!", "Error: " + message);
+	}
+
+	public void warning(String message) {
+		LogDialog ld = new LogDialog(this, "Warning!", "Warning: " + message);
+	}
+
+	public Viewer(String documentPath) {
+		pixelScale = getRetinaScale();
+		screenDPI = getScreenDPI();
+		screenSize = getScreenSize();
+
+		currentDPI = zoomList[findNextLargerZoomLevel(screenDPI - 1)];
+
+		setTitle("MuPDF: ");
+
+		outlinePanel = new Panel(new BorderLayout());
+		{
+			outlineList = new List();
+			outlineList.addItemListener(this);
+			outlinePanel.add(outlineList, BorderLayout.CENTER);
+		}
+		this.add(outlinePanel, BorderLayout.WEST);
+		outlinePanel.setMinimumSize(new Dimension(300, 300));
+		outlinePanel.setPreferredSize(new Dimension(300, 300));
+		outlinePanel.setVisible(false);
+
+		Panel rightPanel = new Panel(new BorderLayout());
+		{
+			Panel toolpane = new Panel(new GridBagLayout());
+			{
+				GridBagConstraints c = new GridBagConstraints();
+				c.fill = GridBagConstraints.HORIZONTAL;
+				c.anchor = GridBagConstraints.WEST;
+
+				Panel toolbar = new Panel(new FlowLayout(FlowLayout.LEFT));
+				{
+					firstButton = new Button("|<");
+					firstButton.addActionListener(this);
+					prevButton = new Button("<");
+					prevButton.addActionListener(this);
+					nextButton = new Button(">");
+					nextButton.addActionListener(this);
+					lastButton = new Button(">|");
+					lastButton.addActionListener(this);
+					pageField = new TextField(4);
+					pageField.addActionListener(this);
+					pageLabel = new Label("/ " + pages);
+
+					toolbar.add(firstButton);
+					toolbar.add(prevButton);
+					toolbar.add(pageField);
+					toolbar.add(pageLabel);
+					toolbar.add(nextButton);
+					toolbar.add(lastButton);
+				}
+				c.gridy = 0;
+				toolpane.add(toolbar, c);
+
+				toolbar = new Panel(new FlowLayout(FlowLayout.LEFT));
+				{
+					zoomOutButton = new Button("Zoom-");
+					zoomOutButton.addActionListener(this);
+					zoomInButton = new Button("Zoom+");
+					zoomInButton.addActionListener(this);
+
+					zoomChoice = new Choice();
+					for (int i = 0; i < zoomList.length; ++i) {
+						zoomChoice.add(String.valueOf(zoomList[i]));
+						if (zoomList[i] == currentDPI)
+							zoomChoice.select(i);
+					}
+					zoomChoice.addItemListener(this);
+
+					toolbar.add(zoomOutButton);
+					toolbar.add(zoomChoice);
+					toolbar.add(zoomInButton);
+				}
+				c.gridy += 1;
+				toolpane.add(toolbar, c);
+
+				reflowPanel = new Panel(new FlowLayout(FlowLayout.LEFT));
+				{
+					fontDecButton = new Button("Font-");
+					fontDecButton.addActionListener(this);
+					fontIncButton = new Button("Font+");
+					fontIncButton.addActionListener(this);
+					fontSizeLabel = new Label(String.valueOf(layoutEm));
+
+					reflowPanel.add(fontDecButton);
+					reflowPanel.add(fontSizeLabel);
+					reflowPanel.add(fontIncButton);
+				}
+				c.gridy += 1;
+				toolpane.add(reflowPanel, c);
+
+				toolbar = new Panel(new FlowLayout(FlowLayout.LEFT));
+				{
+					searchField = new TextField(20);
+					searchField.addActionListener(this);
+					searchPrevButton = new Button("<");
+					searchPrevButton.addActionListener(this);
+					searchNextButton = new Button(">");
+					searchNextButton.addActionListener(this);
+
+					toolbar.add(searchField);
+					toolbar.add(searchPrevButton);
+					toolbar.add(searchNextButton);
+				}
+				searchField.addKeyListener(this);
+				c.gridy += 1;
+				toolpane.add(toolbar, c);
+
+				searchStatusPanel = new Panel(new FlowLayout(FlowLayout.LEFT));
+				{
+					searchStatus = new Label();
+
+					searchStatusPanel.add(searchStatus);
+				}
+				c.gridy += 1;
+				toolpane.add(searchStatusPanel, c);
+			}
+			rightPanel.add(toolpane, BorderLayout.NORTH);
+		}
+		this.add(rightPanel, BorderLayout.EAST);
+
+		pageScroll = new ScrollPane(ScrollPane.SCROLLBARS_NEVER);
+		{
+			pageHolder = new Panel(new GridBagLayout());
+			{
+				pageHolder.setBackground(Color.gray);
+				pageCanvas = new PageCanvas(pixelScale);
+				pageHolder.add(pageCanvas);
+			}
+			pageCanvas.addKeyListener(this);
+			pageCanvas.addMouseWheelListener(this);
+			pageScroll.add(pageHolder);
+		}
+		this.add(pageScroll, BorderLayout.CENTER);
+
+		addWindowListener(this);
+
+		Toolkit toolkit = Toolkit.getDefaultToolkit();
+		EventQueue eq = toolkit.getSystemEventQueue();
+		worker = new Worker(eq);
+		worker.start();
+
+		pack();
+
+		pageCanvas.requestFocusInWindow();
+
+		doc = new ViewerCore(worker, this, documentPath);
+		doc.openDocument(new ViewerCore.OnException() {
+			public void run(Throwable t) {
+				exception(t);
+			}
+		});
+	}
+
+	public void dispose() {
+		doc.cancelSearch(null);
+		doc.worker.stop();
+		super.dispose();
+	}
+
+	public void keyPressed(KeyEvent e) {
+	}
+
+	public void keyReleased(KeyEvent e) {
+		if (e.getSource() == pageCanvas) {
+			int c = e.getKeyCode();
+
+			switch(c)
+			{
+			case KeyEvent.VK_F1: showHelp(); break;
+
+			case KeyEvent.VK_LEFT: pan(-10, 0); break;
+			case KeyEvent.VK_RIGHT: pan(+10, 0); break;
+			case KeyEvent.VK_UP: pan(0, -10); break;
+			case KeyEvent.VK_DOWN: pan(0, +10); break;
+
+			case KeyEvent.VK_PAGE_UP: doc.flipPages(-number, null); break;
+			case KeyEvent.VK_PAGE_DOWN: doc.flipPages(+number, null); break;
+			}
+		}
+	}
+
+	public void keyTyped(KeyEvent e) {
+		if (e.getSource() == pageCanvas)
+			canvasKeyTyped(e);
+		else if (e.getSource() == searchField)
+			searchFieldKeyTyped(e);
+	}
+
+	protected void searchFieldKeyTyped(KeyEvent e) {
+		if (e.getExtendedKeyCodeForChar(e.getKeyChar()) == java.awt.event.KeyEvent.VK_ESCAPE)
+			clearSearch();
+
+	}
+
+	protected void canvasKeyTyped(KeyEvent e) {
+		char c = e.getKeyChar();
+
+		switch(c)
+		{
+		case 'r': doc.reloadDocument(null); break;
+		case 'q': dispose(); break;
+		case 'S': save(); break;
+
+		case 'f': toggleFullscreen(); break;
+
+		case 'm': mark(number); break;
+		case 't': jumpHistoryBack(number); break;
+		case 'T': jumpHistoryForward(number); break;
+
+		case '>': relayout(number > 0 ? number : +1); break;
+		case '<': relayout(number > 0 ? number : -1); break;
+
+		case 'I': toggleInvert(); break;
+		case 'E': toggleICC(); break;
+		case 'A': toggleAntiAlias(); break;
+		case 'C': toggleTint(); break;
+		case 'o': toggleOutline(); break;
+		case 'L': toggleLinks(); break;
+		case 'i': showInfo(); break;
+
+		case '[': rotate(-90); break;
+		case ']': rotate(+90); break;
+
+		case '+': zoomIn(); break;
+		case '-': zoomOut(); break;
+		case 'z': zoomToDPI(number); break;
+
+		case 'w': shrinkWrap(); break;
+		case 'W': fitWidth(); break;
+		case 'H': fitHeight(); break;
+		case 'Z': fitPage(); break;
+
+		case 'k': pan(0, pageCanvas != null ? pageCanvas.getHeight() / -10 : -10); break;
+		case 'j': pan(0, pageCanvas != null ? pageCanvas.getWidth() / +10 : +10); break;
+		case 'h': pan(pageCanvas != null ? pageCanvas.getHeight() / -10 : -10, 0); break;
+		case 'l': pan(pageCanvas != null ? pageCanvas.getWidth() / +10 : +10, 0); break;
+
+		case 'b': smartMove(-1, number); break;
+		case ' ': smartMove(+1, number); break;
+
+		case ',': flipPages(-number); break;
+		case '.': flipPages(+number); break;
+
+		case 'g': gotoPage(number); break;
+		case 'G': gotoLastPage(); break;
+
+		case '/': editSearchNeedle(+1); break;
+		case '?': editSearchNeedle(-1); break;
+		case 'N': search(-1); break;
+		case 'n': search(+1); break;
+		case '\u001b': clearSearch(); break;
+		}
+
+		if (c >= '0' && c <= '9')
+			number = number * 10 + c - '0';
+		else
+			number = 0;
+	}
+
+	public void mouseWheelMoved(MouseWheelEvent e) {
+		int mod = e.getModifiersEx();
+		int rot = e.getWheelRotation();
+		if ((mod & MouseWheelEvent.CTRL_DOWN_MASK) != 0) {
+			if (rot < 0)
+				zoomIn();
+			else
+				zoomOut();
+		} else if ((mod & MouseWheelEvent.SHIFT_DOWN_MASK) != 0) {
+			if (rot < 0)
+				pan(pageCanvas != null ? pageCanvas.getHeight() / -10 : -10, 0);
+			else
+				pan(pageCanvas != null ? pageCanvas.getHeight() / +10 : +10, 0);
+		} else if (mod == 0) {
+			if (rot < 0)
+				pan(0, pageCanvas != null ? pageCanvas.getHeight() / -10 : -10);
+			else
+				pan(0, pageCanvas != null ? pageCanvas.getHeight() / +10 : +10);
+		}
+	}
+
+	protected void editSearchNeedle(int direction) {
+		clearSearch();
+		searchDirection = direction;
+		searchField.requestFocusInWindow();
+	}
+
+	protected void cancelSearch() {
+		doc.cancelSearch(null);
+	}
+
+	protected void clearSearch() {
+		cancelSearch();
+		searchField.setText("");
+		searchStatus.setText("");
+		searchStatusPanel.validate();
+		validate();
+		hits = null;
+		redraw();
+	}
+
+	public void search(int direction) {
+		if (searchField.isEnabled()) {
+			cancelSearch();
+			searchField.setEnabled(false);
+			searchNextButton.setEnabled(false);
+			searchPrevButton.setEnabled(false);
+			pageCanvas.requestFocusInWindow();
+			doc.search(searchField.getText(), direction, new ViewerCore.OnException() {
+					public void run(Throwable t) {
+						exception(t);
+					}
+			});
+		}
+	}
+
+	protected void render() {
+		if (bbox == null)
+			return;
+
+		float width = bbox.x1 - bbox.x0;
+		float height = bbox.y1 - bbox.y0;
+		float scaleX = (float) Math.floor(width * (currentDPI/72.0f) * pixelScale + 0.5f) / width;
+		float scaleY = (float) Math.floor(height * (currentDPI/72.0f) * pixelScale + 0.5f) / height;
+		Matrix ctm = new Matrix().scale(scaleX, scaleY).rotate(rotate);
+
+		Rect atOrigin = new Rect(bbox).transform(ctm);
+		ctm.e -= atOrigin.x0;
+		ctm.f -= atOrigin.y0;
+		Rect bounds = new Rect(bbox).transform(ctm);
+
+		Cookie cookie = new Cookie();
+
+		renderMeter = new RenderProgressmeter(this, "Rendering...", cookie, 250);
+		renderMeter.setLocationRelativeTo(this);
+		pageCanvas.requestFocusInWindow();
+
+		doc.renderPage(ctm, bounds, icc, antialias, invert, tint, tintBlack, tintWhite, cookie,
+			new ViewerCore.OnException() {
+				public void run(Throwable t) {
+					if (!renderMeter.cancelled)
+						exception(t);
+				}
+			}
+		);
+	}
+
+	protected int findExactZoomLevel(int dpi) {
+		for (int level = 0; level < zoomList.length - 1; level++)
+			if (zoomList[level] == dpi)
+				return level;
+		return -1;
+	}
+
+	protected int findNextSmallerZoomLevel(int dpi) {
+		for (int level = zoomList.length - 1; level >= 0; level--)
+			if (zoomList[level] < dpi)
+				return level;
+		return 0;
+	}
+
+	protected int findNextLargerZoomLevel(int dpi) {
+		for (int level = 0; level < zoomList.length - 1; level++)
+			if (zoomList[level] > dpi)
+				return level;
+		return zoomList.length - 1;
+	}
+
+	protected void zoomToDPI(int newDPI) {
+		if (newDPI == 0)
+			newDPI = screenDPI;
+		if (newDPI < MIN_ZOOM_DPI)
+			newDPI = MIN_ZOOM_DPI;
+		if (newDPI > MAX_ZOOM_DPI)
+			newDPI = MAX_ZOOM_DPI;
+
+		if (newDPI == currentDPI)
+			return;
+
+		int level = findExactZoomLevel(newDPI);
+		if (level < 0) {
+			if (customZoom)
+				zoomChoice.remove(0);
+			customZoom = true;
+			zoomChoice.insert(String.valueOf(newDPI), 0);
+			zoomChoice.select(0);
+		} else {
+			if (customZoom) {
+				customZoom = false;
+				zoomChoice.remove(0);
+			}
+			zoomChoice.select(level);
+		}
+
+		currentDPI = newDPI;
+		render();
+	}
+
+	protected void zoomToLevel(int level) {
+		if (level < 0)
+			level = 0;
+		if (level >= zoomList.length - 1)
+			level = zoomList.length - 1;
+		zoomToDPI(zoomList[level]);
+	}
+
+	protected void zoomIn() {
+		zoomToLevel(findNextLargerZoomLevel(currentDPI));
+	}
+
+	protected void zoomOut() {
+		zoomToLevel(findNextSmallerZoomLevel(currentDPI));
+	}
+
+	protected void zoomToScale(float newZoom) {
+		zoomToDPI((int)(newZoom * 72));
+	}
+
+	protected void fit(float desired, float unscaled) {
+		zoomToScale(desired / unscaled);
+	}
+
+	protected float unscaledWidth() {
+		return bbox != null ? bbox.x1 - bbox.x0 : 0;
+	}
+
+	protected float unscaledHeight() {
+		return bbox != null ? bbox.y1 - bbox.y0 : 0;
+	}
+
+	protected void fitWidth() {
+		fit(pageScroll.getSize().width, unscaledWidth());
+	}
+
+	protected void fitHeight() {
+		fit(pageScroll.getSize().height, unscaledHeight());
+	}
+
+	protected void fitPage() {
+		Dimension size = pageScroll.getSize();
+		float width = bbox != null ? bbox.x1 - bbox.x0 : 0;
+		float height = bbox != null ? bbox.y1 - bbox.y0 : 0;
+		float pageAspect = (width == 0 || height == 0) ? 0 : (width / height);
+		float canvasAspect = (float) size.width / (float) size.height;
+		if (pageAspect > canvasAspect)
+			fitWidth();
+		else
+			fitHeight();
+	}
+
+	protected void rotate(int change) {
+		int newRotate = rotate + change;
+		while (newRotate < 0) newRotate += 360;
+		while (newRotate >= 360) newRotate -= 360;
+
+		if (newRotate - rotate == 0)
+			return;
+		rotate = newRotate;
+
+		render();
+	}
+
+	protected void toggleAntiAlias() {
+		int newAntialias = number != 0 ? number : (antialias == 8 ? 0 : 8);
+		if (newAntialias - antialias == 0)
+			return;
+
+		antialias = newAntialias;
+		render();
+	}
+
+	protected void toggleICC() {
+		icc = !icc;
+		render();
+	}
+
+	protected void toggleInvert() {
+		invert = !invert;
+		render();
+	}
+
+	protected void toggleTint() {
+		tint = !tint;
+		render();
+	}
+
+	protected void toggleOutline() {
+		if (outlineList.getItemCount() <= 0)
+			return;
+
+		outlinePanel.setVisible(!outlinePanel.isVisible());
+		pack();
+		validate();
+	}
+
+	protected void toggleLinks() {
+		showLinks = !showLinks;
+		redraw();
+	}
+
+	protected boolean isShrinkWrapped(int oldPixmapWidth, int oldPixmapHeight) {
+		Dimension size = pageScroll.getSize();
+		if (oldPixmapWidth == 0 && oldPixmapHeight == 0)
+			return true;
+		if (oldPixmapWidth + 4 == size.width && oldPixmapHeight + 4 == size.height)
+			return true;
+		return false;
+	}
+
+	protected void shrinkWrap() {
+		Dimension newSize = new Dimension(pixmapWidth, pixmapHeight);
+		newSize.width += 4;
+		newSize.height += 4;
+
+		if (newSize.width > screenSize.width)
+			newSize.width = screenSize.width;
+		if (newSize.height > screenSize.height)
+			newSize.height = screenSize.height;
+
+		pageScroll.setPreferredSize(newSize);
+		pack();
+	}
+
+	protected void redraw() {
+		boolean wasShrinkWrapped = isShrinkWrapped(pixmapWidth, pixmapHeight);
+
+		if (pixmap != null) {
+			pixmapWidth = pixmap.getWidth();
+			pixmapHeight = pixmap.getHeight();
+			BufferedImage image = new BufferedImage(pixmapWidth, pixmapHeight, BufferedImage.TYPE_3BYTE_BGR);
+			image.setRGB(0, 0, pixmapWidth, pixmapHeight, pixmap.getPixels(), 0, pixmapWidth);
+			pageCanvas.setPage(image, showLinks ? links : null, hits);
+		} else {
+			pixmapWidth = 0;
+			pixmapHeight = 0;
+			pageCanvas.setPage(null, null, null);
+		}
+
+		if (wasShrinkWrapped)
+			shrinkWrap();
+
+		pageCanvas.invalidate();
+		validate();
+	}
+
+	protected static String paperSizeName(int w, int h)
+	{
+		/* ISO A */
+		if (w == 2384 && h == 3370) return "A0";
+		if (w == 1684 && h == 2384) return "A1";
+		if (w == 1191 && h == 1684) return "A2";
+		if (w == 842 && h == 1191) return "A3";
+		if (w == 595 && h == 842) return "A4";
+		if (w == 420 && h == 595) return "A5";
+		if (w == 297 && h == 420) return "A6";
+
+		/* US */
+		if (w == 612 && h == 792) return "Letter";
+		if (w == 612 && h == 1008) return "Legal";
+		if (w == 792 && h == 1224) return "Ledger";
+		if (w == 1224 && h == 792) return "Tabloid";
+
+		return null;
+	}
+
+	protected void showHelp() {
+		final Dialog box = new Dialog(this, "Help", true);
+		box.addWindowListener(new WindowListener() {
+			public void windowActivated(WindowEvent event) { }
+			public void windowDeactivated(WindowEvent event) { }
+			public void windowIconified(WindowEvent event) { }
+			public void windowDeiconified(WindowEvent event) { }
+			public void windowOpened(WindowEvent event) { }
+			public void windowClosed(WindowEvent event) { }
+			public void windowClosing(WindowEvent event) {
+				box.setVisible(false);
+				pageCanvas.requestFocusInWindow();
+			}
+		});
+
+		String help[] = {
+			"The middle mouse button (scroll wheel button) pans the document view.",
+			//"The right mouse button selects a region and copies the marked text to the clipboard.",
+			//"",
+			//"",
+			"F1 - show this message",
+			"i - show document information",
+			"o - show document outline",
+			//"a - show annotation editor",
+			"L - highlight links",
+			//"F - highlight form fields",
+			"r - reload file",
+			"S - save file",
+			"q - quit",
+			"",
+			"< - decrease E-book font size",
+			"> - increase E-book font size",
+			"A - toggle anti-aliasing",
+			"I - toggle inverted color mode",
+			"C - toggle tinted color mode",
+			"E - toggle ICC color management",
+			//"e - toggle spot color emulation",
+			"",
+			"f - fullscreen window",
+			"w - shrink wrap window",
+			"W - fit to width",
+			"H - fit to height",
+			"Z - fit to page",
+			"z - reset zoom",
+			"[number] z - set zoom resolution in DPI",
+			"plus - zoom in",
+			"minus - zoom out",
+			"[ - rotate counter-clockwise",
+			"] - rotate clockwise",
+			"arrow keys - scroll in small increments",
+			"h, j, k, l - scroll in small increments",
+			"",
+			"b - smart move backward",
+			"space - smart move forward",
+			"comma or page up - go backward",
+			"period or page down - go forward",
+			"g - go to first page",
+			"G - go to last page",
+			"[number] g - go to page number",
+			"",
+			"m - save current location in history",
+			"t - go backward in history",
+			"T - go forward in history",
+			"[number] m - save current location in numbered bookmark",
+			"[number] t - go to numbered bookmark",
+			"",
+			"/ - search for text forward",
+			"? - search for text backward",
+			"n - repeat search",
+			"N - repeat search in reverse direction",
+		};
+
+		Panel helpPanel = new Panel(new GridLayout(help.length, 1));
+
+		for (int i = 0; i < help.length; i++)
+			helpPanel.add(new Label(help[i]));
+
+		Button button = new Button("OK");
+		button.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				box.setVisible(false);
+			}
+		});
+		button.addKeyListener(new KeyListener() {
+			public void keyPressed(KeyEvent e) { }
+			public void keyReleased(KeyEvent e) {
+				if (e.getKeyCode() == KeyEvent.VK_F1)
+					box.setVisible(false);
+			}
+			public void keyTyped(KeyEvent e) {
+				if (e.getKeyChar() == '\u001b' || e.getKeyChar() == '\r' || e.getKeyChar() == '\n')
+					box.setVisible(false);
+			}
+		});
+
+		Panel buttonPane = new Panel(new FlowLayout());
+		buttonPane.add(button);
+
+		box.add(helpPanel, BorderLayout.CENTER);
+		box.add(buttonPane, BorderLayout.SOUTH);
+
+		box.setResizable(false);
+		box.pack();
+
+		java.awt.Point winLoc = this.getLocation();
+		Dimension winDim = this.getSize();
+		int winCenterX = winLoc.x + winDim.width / 2;
+		int winCenterY = winLoc.y + winDim.height / 2;
+
+		Dimension diagDim = box.getSize();
+		int x = winCenterX - diagDim.width / 2;
+		int y = winCenterY - diagDim.height / 2;
+
+		box.setLocation(x, y);
+
+		button.requestFocusInWindow();
+		box.setVisible(true);
+
+		box.dispose();
+	}
+
+	protected void showInfo() {
+		StringBuffer buffer;
+
+		cancelSearch();
+
+		final Dialog box = new Dialog(this, "Document info", true);
+		box.addWindowListener(new WindowListener() {
+			public void windowActivated(WindowEvent event) { }
+			public void windowDeactivated(WindowEvent event) { }
+			public void windowIconified(WindowEvent event) { }
+			public void windowDeiconified(WindowEvent event) { }
+			public void windowOpened(WindowEvent event) { }
+			public void windowClosed(WindowEvent event) { }
+			public void windowClosing(WindowEvent event) {
+				box.setVisible(false);
+				pageCanvas.requestFocusInWindow();
+			}
+		});
+
+		Panel infoPanel = new Panel();
+		int rows = 0;
+
+		if (title != null) rows++;
+
+		if (author != null) rows++;
+
+		if (format != null) rows++;
+
+		if (encryption != null) rows++;
+
+		buffer = new StringBuffer();
+		if (print)
+			buffer.append("print, ");
+		if (copy)
+			buffer.append("copy, ");
+		if (edit)
+			buffer.append("edit, ");
+		if (annotate)
+			buffer.append("annotate, ");
+		if (form)
+			buffer.append("form, ");
+		if (accessibility)
+			buffer.append("accessibility, ");
+		if (assemble)
+			buffer.append("assemble, ");
+		if (printHq)
+			buffer.append("print-hq, ");
+		if (buffer.length() > 2)
+			buffer.delete(buffer.length() - 2, buffer.length());
+		String permissions = buffer.length() > 0 ? buffer.toString() : null;
+		if (permissions != null) rows++;
+
+		buffer = new StringBuffer();
+		if (doc.equals("PDF")) {
+			buffer.append("PDF ");
+			if (linearized)
+				buffer.append("linearized ");
+			buffer.append("document with ");
+			buffer.append(updates);
+			if (updates == 1)
+				buffer.append(" update");
+			else
+				buffer.append(" updates");
+		}
+		String versions = buffer.length() > 0 ? buffer.toString() : null;
+		if (versions != null) rows++;
+
+		buffer = new StringBuffer();
+		if (doc.equals("PDF")) {
+			if (updates > 1) {
+				if (firstUpdate == 0)
+					buffer.append("Change firstUpdate seems valid.");
+				else if (firstUpdate == 1)
+					buffer.append("Invalid changes made to the document in the last update.");
+				else if (firstUpdate == 2)
+					buffer.append("Invalid changes made to the document in the penultimate update.");
+				else {
+					buffer.append("Invalid changes made to the document ");
+					buffer.append(firstUpdate);
+					buffer.append(" updates ago.");
+				}
+			}
+		}
+		String validation = buffer.length() > 0 ? buffer.toString() : null;
+		if (validation != null) rows++;
+
+		buffer = new StringBuffer();
+		int w = 0;
+		int h = 0;
+		if (bbox != null) {
+			w = (int)(bbox.x1 - bbox.x0 + 0.5f);
+			h = (int)(bbox.y1 - bbox.y0 + 0.5f);
+		}
+		buffer.append(w);
+		buffer.append(" x ");
+		buffer.append(h);
+		String name = paperSizeName(w, h);
+		if (name == null)
+			name = paperSizeName(h, w);
+		if (name != null)
+			buffer.append("(" + name + ")");
+		String paperSize = buffer.length() > 0 ? buffer.toString() : null;
+		if (paperSize != null) rows++;
+
+		buffer = new StringBuffer();
+		buffer.append(pageNumber + 1);
+		buffer.append(" / ");
+		buffer.append(pages);
+		String page = buffer.length() > 0 ? buffer.toString() : null;
+		if (page != null) rows++;
+
+		String iccstring = icc ? "on" : "off";
+		rows++;
+
+		String antialiasstring = Integer.toString(antialias);
+		rows++;
+
+		infoPanel.setLayout(new GridLayout(rows, 1));
+
+		if (title != null) infoPanel.add(new Label("Title: " + title));
+		if (author != null) infoPanel.add(new Label("Author: " + author));
+		if (format != null) infoPanel.add(new Label("Format: " + format));
+		if (encryption != null) infoPanel.add(new Label("Encryption: " + encryption));
+		if (permissions != null) infoPanel.add(new Label("Permissions: " + permissions));
+		if (versions != null) infoPanel.add(new Label(versions));
+		if (validation != null) infoPanel.add(new Label(validation));
+		infoPanel.add(new Label("Size: " + paperSize));
+		infoPanel.add(new Label("Page: " + page));
+		infoPanel.add(new Label("ICC rendering: " + iccstring));
+		infoPanel.add(new Label("Antialias rendering: " + antialiasstring));
+
+		Button button = new Button("OK");
+		button.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				box.setVisible(false);
+			}
+		});
+		button.addKeyListener(new KeyListener() {
+			public void keyPressed(KeyEvent e) { }
+			public void keyReleased(KeyEvent e) { }
+			public void keyTyped(KeyEvent e) {
+				if (e.getKeyChar() == '\u001b' || e.getKeyChar() == '\r' || e.getKeyChar() == '\n')
+					box.setVisible(false);
+			}
+		});
+
+		Panel buttonPane = new Panel(new FlowLayout());
+		buttonPane.add(button);
+
+		box.add(infoPanel, BorderLayout.CENTER);
+		box.add(buttonPane, BorderLayout.SOUTH);
+
+		button.requestFocusInWindow();
+
+		box.setResizable(false);
+		box.pack();
+
+		java.awt.Point winLoc = this.getLocation();
+		Dimension winDim = this.getSize();
+		int winCenterX = winLoc.x + winDim.width / 2;
+		int winCenterY = winLoc.y + winDim.height / 2;
+
+		Dimension diagDim = box.getSize();
+		int x = winCenterX - diagDim.width / 2;
+		int y = winCenterY - diagDim.height / 2;
+
+		box.setLocation(x, y);
+
+		box.setVisible(true);
+		box.dispose();
+	}
+
+	protected void toggleFullscreen() {
+		isFullscreen = !isFullscreen;
+
+		if (isFullscreen)
+			setExtendedState(Frame.MAXIMIZED_BOTH);
+		else
+			setExtendedState(Frame.NORMAL);
+	}
+
+	protected void mark(int number) {
+		cancelSearch();
+		if (number == 0)
+			pushHistory();
+		else if (number > 0 && number < marks.length)
+			marks[number] = saveMark();
+	}
+
+	protected void jumpHistoryBack(int number) {
+		cancelSearch();
+		if (number == 0) {
+			if (historyCount > 0)
+				popHistory();
+		} else if (number > 0 && number < marks.length)
+			restoreMark(marks[number]);
+	}
+
+	protected void jumpHistoryForward(int number) {
+		cancelSearch();
+		if (number == 0) {
+			if (futureCount > 0) {
+				popFuture();
+			}
+		}
+	}
+
+	protected Mark saveMark() {
+		return new Mark(location);
+	}
+
+	protected void restoreMark(Mark mark) {
+		if (mark != null) {
+			doc.gotoLocation(mark.loc, null);
+			pageCanvas.requestFocusInWindow();
+		}
+	}
+
+	protected void pushHistory() {
+		if (historyCount > 0 && location.equals(history[historyCount - 1].loc))
+		{
+			return;
+		}
+
+		if (historyCount + 1 >= history.length) {
+			for (int i = 0; i < history.length - 1; i++)
+				history[i] = history[i + 1];
+			history[historyCount] = saveMark();
+		} else {
+			history[historyCount++] = saveMark();
+		}
+	}
+
+	protected void pushFuture() {
+		if (futureCount + 1 >= future.length) {
+			for (int i = 0; i < future.length - 1; i++)
+				future[i] = future[i + 1];
+			future[futureCount] = saveMark();
+		} else {
+			future[futureCount++] = saveMark();
+		}
+	}
+
+	protected void clearFuture() {
+		futureCount = 0;
+	}
+
+	protected void popHistory() {
+		Location here = location;
+		pushFuture();
+		while (historyCount > 0 && location.equals(here))
+			restoreMark(history[--historyCount]);
+	}
+
+	protected void popFuture() {
+		Location here = location;
+		pushHistory();
+		while (futureCount > 0 && location.equals(here))
+			restoreMark(future[--futureCount]);
+	}
+
+	protected void pan(int panx, int pany) {
+		Adjustable hadj = pageScroll.getHAdjustable();
+		Adjustable vadj = pageScroll.getVAdjustable();
+		int h = hadj.getValue();
+		int v = vadj.getValue();
+		int newh = h + panx;
+		int newv = v + pany;
+
+		if (newh < hadj.getMinimum())
+			newh = hadj.getMinimum();
+		if (newh > hadj.getMaximum() - hadj.getVisibleAmount())
+			newh = hadj.getMaximum() - hadj.getVisibleAmount();
+		if (newv < vadj.getMinimum())
+			newv = vadj.getMinimum();
+		if (newv > vadj.getMaximum() - vadj.getVisibleAmount())
+			newv = vadj.getMaximum() - vadj.getVisibleAmount();
+
+		if (newh == h && newv == v)
+			return;
+
+		if (newh != h)
+			hadj.setValue(newh);
+		if (newv != v)
+			vadj.setValue(newv);
+	}
+
+	protected void smartMove(int direction, int moves) {
+		cancelSearch();
+
+		if (moves < 1)
+			moves = 1;
+
+		while (moves-- > 0)
+		{
+			Adjustable hadj = pageScroll.getHAdjustable();
+			Adjustable vadj = pageScroll.getVAdjustable();
+			int slop_x = hadj.getMaximum() / 20;
+			int slop_y = vadj.getMaximum() / 20;
+
+			if (direction > 0) {
+				int remaining_x = hadj.getMaximum() - hadj.getValue() - hadj.getVisibleAmount();
+				int remaining_y = vadj.getMaximum() - vadj.getValue() - vadj.getVisibleAmount();
+
+				if (remaining_y > slop_y) {
+					int value = vadj.getValue() + vadj.getVisibleAmount() * 9 / 10;
+					if (value > vadj.getMaximum())
+						value = vadj.getMaximum();
+					vadj.setValue(value);
+				} else if (remaining_x > slop_x) {
+					vadj.setValue(vadj.getMinimum());
+					int value = hadj.getValue() + hadj.getVisibleAmount() * 9 / 10;
+					if (value > hadj.getMaximum())
+						value = hadj.getMaximum();
+					hadj.setValue(value);
+				} else {
+					doc.flipPages(+1, null);
+					vadj.setValue(vadj.getMinimum());
+					hadj.setValue(hadj.getMinimum());
+				}
+			} else {
+				int remaining_x = Math.abs(hadj.getMinimum() - hadj.getValue());
+				int remaining_y = Math.abs(vadj.getMinimum() - vadj.getValue());
+
+				if (remaining_y > slop_y) {
+					int value = vadj.getValue() - vadj.getVisibleAmount() * 9 / 10;
+					if (value < vadj.getMinimum())
+						value = vadj.getMinimum();
+					vadj.setValue(value);
+				} else if (remaining_x > slop_x) {
+					vadj.setValue(vadj.getMaximum());
+					int value = hadj.getValue() - hadj.getVisibleAmount() * 9 / 10;
+					if (value < hadj.getMinimum())
+						value = hadj.getMinimum();
+					hadj.setValue(value);
+				} else {
+					doc.flipPages(-1, null);
+					vadj.setValue(vadj.getMaximum());
+					hadj.setValue(hadj.getMaximum());
+				}
+			}
+		}
+	}
+
+	protected void flipPages(int number) {
+		cancelSearch();
+		doc.flipPages(number, null);
+	}
+
+	protected void gotoPage(int number) {
+		cancelSearch();
+		doc.gotoPage(number - 1, null);
+	}
+
+	protected void gotoLastPage() {
+		cancelSearch();
+		doc.gotoLastPage(null);
+	}
+
+	protected void relayout(int change) {
+		int newEm = layoutEm + change;
+		if (newEm < 6)
+			newEm = 6;
+		if (newEm > 36)
+			newEm = 36;
+
+		if (newEm == layoutEm)
+			return;
+
+		layoutEm = newEm;
+		fontSizeLabel.setText(String.valueOf(layoutEm));
+		doc.relayoutDocument(layoutWidth, layoutHeight, layoutEm, null);
+	}
+
+	public void actionPerformed(ActionEvent event) {
+		Object source = event.getSource();
+
+		if (source == firstButton)
+			doc.gotoFirstPage(null);
+		if (source == lastButton)
+			doc.gotoLastPage(null);
+		if (source == prevButton)
+			doc.flipPages(-1, null);
+		if (source == nextButton)
+			doc.flipPages(+1, null);
+		if (source == pageField) {
+			doc.gotoPage(Integer.parseInt(pageField.getText()) - 1, null);
+			pageCanvas.requestFocusInWindow();
+		}
+
+		if (source == searchField)
+			search(searchDirection);
+		if (source == searchNextButton)
+			search(+1);
+		if (source == searchPrevButton)
+			search(-1);
+
+		if (source == fontIncButton && doc != null && reflowable)
+			relayout(+1);
+		if (source == fontDecButton && doc != null && reflowable)
+			relayout(-1);
+
+		if (source == zoomOutButton) {
+			zoomOut();
+			pageCanvas.requestFocusInWindow();
+		}
+		if (source == zoomInButton) {
+			zoomIn();
+			pageCanvas.requestFocusInWindow();
+		}
+	}
+
+	public void itemStateChanged(ItemEvent event) {
+		Object source = event.getSource();
+		if (source == zoomChoice) {
+			zoomToLevel(zoomChoice.getSelectedIndex());
+			pageCanvas.requestFocusInWindow();
+		}
+		if (source == outlineList) {
+			int i = outlineList.getSelectedIndex();
+			doc.gotoLocation(outline[i].location, null);
+			pageCanvas.requestFocusInWindow();
+		}
+	}
+
+	public void windowClosing(WindowEvent event) { dispose(); }
+	public void windowActivated(WindowEvent event) { }
+	public void windowDeactivated(WindowEvent event) { }
+	public void windowIconified(WindowEvent event) { }
+	public void windowDeiconified(WindowEvent event) { }
+	public void windowOpened(WindowEvent event) { }
+	public void windowClosed(WindowEvent event) { }
+
+	public void save() {
+		cancelSearch();
+
+		SaveOptionsDialog dialog = new SaveOptionsDialog(this);
+		dialog.populate();
+		dialog.setLocationRelativeTo(this);
+		dialog.setVisible(true);
+		dialog.dispose();
+
+		final String options = dialog.getOptions();
+		if (options == null)
+		{
+			pageCanvas.requestFocusInWindow();
+			return;
+		}
+
+		FileDialog fileDialog = new FileDialog(this, "MuPDF Save File", FileDialog.SAVE);
+		fileDialog.setDirectory(System.getProperty("user.dir"));
+		fileDialog.setFilenameFilter(new FilenameFilter() {
+			public boolean accept(File dir, String name) {
+				return Document.recognize(name);
+			}
+		});
+		fileDialog.setFile(doc.documentPath);
+		fileDialog.setVisible(true);
+		fileDialog.dispose();
+
+		if (fileDialog.getFile() == null)
+		{
+			pageCanvas.requestFocusInWindow();
+			return;
+		}
+
+		final String selectedPath = new StringBuffer(fileDialog.getDirectory()).append(File.separatorChar).append(fileDialog.getFile()).toString();
+		OCRmeter = new OCRProgressmeter(this, "Saving...", pages);
+		OCRmeter.setLocationRelativeTo(this);
+		OCRmeter.setVisible(true);
+		pageCanvas.requestFocusInWindow();
+
+		if (options.indexOf("ocr-language=") < 0)
+			doc.save(selectedPath, options, OCRmeter, new ViewerCore.OnException() {
+				public void run(Throwable t) {
+					if (t instanceof IOException)
+						exception(t);
+					else if (t instanceof RuntimeException && !OCRmeter.cancelled)
+						exception(t);
+				}
+			});
+		else
+		{
+			try {
+				FileStream fs = new FileStream(selectedPath, "rw");
+				doc.save(fs, options, OCRmeter, new ViewerCore.OnException() {
+					public void run(Throwable t) {
+						if (t instanceof RuntimeException && !OCRmeter.cancelled)
+							exception(t);
+					}
+				});
+			} catch (IOException e) {
+				exception(e);
+			}
+		}
+	}
+
+	public void onSaveComplete() {
+		if (OCRmeter != null)
+			OCRmeter.done();
+	}
+
+	class SaveOptionsDialog extends Dialog implements ActionListener, ItemListener, KeyListener {
+		Checkbox snapShot = new Checkbox("Snapshot", false);
+		Checkbox highSecurity = new Checkbox("High security", false);
+		Choice resolution = new Choice();
+		TextField language = new TextField("eng");
+		Checkbox incremental = new Checkbox("Incremental", false);
+
+		Checkbox prettyPrint = new Checkbox("Pretty print", false);
+		Checkbox ascii = new Checkbox("Ascii", false);
+		Checkbox decompress = new Checkbox("Decompress", false);
+		Checkbox compress = new Checkbox("Compress", true);
+		Checkbox compressImages = new Checkbox("Compress images", true);
+		Checkbox compressFonts = new Checkbox("Compress fonts", true);
+
+		Checkbox linearize = new Checkbox("Linearize", false);
+		Checkbox garbageCollect = new Checkbox("Garbage collect", false);
+		Checkbox cleanSyntax = new Checkbox("Clean syntax", false);
+		Checkbox sanitizeSyntax = new Checkbox("Sanitize syntax", false);
+
+		Choice encryption = new Choice();
+		TextField userPassword = new TextField();
+		TextField ownerPassword = new TextField();
+
+		Button cancel = new Button("Cancel");
+		Button save = new Button("Save");
+
+		String options = null;
+
+		public SaveOptionsDialog(Frame parent) {
+			super(parent, "MuPDF Save Options", true);
+
+			resolution.add("200dpi");
+			resolution.add("300dpi");
+			resolution.add("600dpi");
+			resolution.add("1200dpi");
+
+			encryption.add("Keep");
+			encryption.add("None");
+			encryption.add("RC4, 40bit");
+			encryption.add("RC4, 128bit");
+			encryption.add("AES, 128bit");
+			encryption.add("AES, 256bit");
+
+			snapShot.addItemListener(this);
+			highSecurity.addItemListener(this);
+			resolution.addItemListener(this);
+			language.addActionListener(this);
+			incremental.addItemListener(this);
+			prettyPrint.addItemListener(this);
+			ascii.addItemListener(this);
+			decompress.addItemListener(this);
+			compress.addItemListener(this);
+			compressImages.addItemListener(this);
+			compressFonts.addItemListener(this);
+			linearize.addItemListener(this);
+			garbageCollect.addItemListener(this);
+			cleanSyntax.addItemListener(this);
+			sanitizeSyntax.addItemListener(this);
+
+			encryption.addItemListener(this);
+			userPassword.addActionListener(this);
+			ownerPassword.addActionListener(this);
+
+			cancel.addActionListener(this);
+			save.addActionListener(this);
+			save.addKeyListener(this);
+
+			calculateOptions();
+		}
+
+		void populate(Container container, GridBagConstraints c, Component component) {
+			GridBagLayout gbl = (GridBagLayout) container.getLayout();
+			gbl.setConstraints(component, c);
+			container.add(component);
+		}
+
+		void populate() {
+			GridBagConstraints c = new GridBagConstraints();
+
+			c.fill = GridBagConstraints.BOTH;
+			c.weightx = 1.0;
+			c.gridwidth = GridBagConstraints.REMAINDER;
+
+			GridBagLayout gbl = new GridBagLayout();
+			setLayout(gbl);
+
+			Panel left = new Panel();
+			Panel right = new Panel();
+			GridBagLayout lgbl = new GridBagLayout();
+			GridBagLayout rgbl = new GridBagLayout();
+			left.setLayout(lgbl);
+			right.setLayout(rgbl);
+
+			populate(left, c, snapShot);
+			populate(left, c, highSecurity);
+			populate(left, c, resolution);
+			populate(left, c, language);
+			populate(left, c, incremental);
+
+			c.weighty = 1.5;
+			populate(left, c, new Panel());
+			c.weighty = 0.0;
+
+			populate(left, c, prettyPrint);
+			populate(left, c, ascii);
+			populate(left, c, decompress);
+			populate(left, c, compress);
+			populate(left, c, compressImages);
+			populate(left, c, compressFonts);
+
+			populate(right, c, linearize);
+			populate(right, c, garbageCollect);
+			populate(right, c, cleanSyntax);
+			populate(right, c, sanitizeSyntax);
+
+			c.weighty = 1.5;
+			populate(right, c, new Panel());
+			c.weighty = 0.0;
+
+			populate(right, c, new Label("Encryption"));
+			populate(right, c, encryption);
+			populate(right, c, new Label("User password"));
+			populate(right, c, userPassword);
+			populate(right, c, new Label("Owner password"));
+			populate(right, c, ownerPassword);
+
+			c.gridwidth = GridBagConstraints.REMAINDER;
+			populate(this, c, new Panel());
+
+			c.gridwidth = 1;
+			populate(this, c, left);
+			c.gridwidth = GridBagConstraints.REMAINDER;
+			populate(this, c, right);
+
+			c.gridwidth = GridBagConstraints.REMAINDER;
+			populate(this, c, new Panel());
+
+			c.gridwidth = 1;
+			populate(this, c, cancel);
+			c.gridwidth = GridBagConstraints.REMAINDER;
+			populate(this, c, save);
+
+			pack();
+			setResizable(false);
+			save.requestFocusInWindow();
+		}
+
+		public void keyPressed(KeyEvent e) { }
+		public void keyReleased(KeyEvent e) { }
+
+		public void keyTyped(KeyEvent e) {
+			if (e.getKeyChar() == '\u001b')
+				cancel();
+			else if (e.getKeyChar() == '\n')
+				save();
+		}
+
+		public void actionPerformed(ActionEvent e) {
+			if (e.getSource() == cancel)
+				cancel();
+			else if (e.getSource() == save)
+				save();
+		}
+
+		void cancel() {
+			options = null;
+			setVisible(false);
+		}
+
+		void save() {
+			setVisible(false);
+		}
+
+		public void itemStateChanged(ItemEvent e) {
+			calculateOptions();
+		}
+
+		void calculateOptions() {
+			boolean isPDF = false;
+			boolean canBeSavedIncrementally = false;
+			boolean isRedacted = false;
+
+			if (isPDF && !canBeSavedIncrementally)
+				incremental.setState(false);
+
+			if (highSecurity.getState()) {
+				incremental.setState(false);
+				prettyPrint.setState(false);
+				ascii.setState(false);
+				decompress.setState(false);
+				compress.setState(true);
+				compressImages.setState(false);
+				compressFonts.setState(false);
+				linearize.setState(false);
+				garbageCollect.setState(false);
+				cleanSyntax.setState(false);
+				sanitizeSyntax.setState(false);
+				encryption.select("None");
+				userPassword.setText("");
+				ownerPassword.setText("");
+			} else if (incremental.getState()) {
+				linearize.setState(false);
+				garbageCollect.setState(false);
+				cleanSyntax.setState(false);
+				sanitizeSyntax.setState(false);
+				encryption.select("Keep");
+				userPassword.setText("");
+				ownerPassword.setText("");
+			}
+
+			highSecurity.setEnabled(snapShot.getState() == false);
+			resolution.setEnabled(snapShot.getState() == false && highSecurity.getState() == true);
+			language.setEnabled(snapShot.getState() == false && highSecurity.getState() == true);
+			incremental.setEnabled(snapShot.getState() == false && highSecurity.getState() == false && isPDF && canBeSavedIncrementally);
+			prettyPrint.setEnabled(snapShot.getState() == false && highSecurity.getState() == false);
+			ascii.setEnabled(snapShot.getState() == false && highSecurity.getState() == false);
+			decompress.setEnabled(snapShot.getState() == false && highSecurity.getState() == false);
+			compress.setEnabled(snapShot.getState() == false && highSecurity.getState() == false);
+			compressImages.setEnabled(snapShot.getState() == false && highSecurity.getState() == false);
+			compressFonts.setEnabled(snapShot.getState() == false && highSecurity.getState() == false);
+			linearize.setEnabled(snapShot.getState() == false && highSecurity.getState() == false && incremental.getState() == false);
+			garbageCollect.setEnabled(snapShot.getState() == false && highSecurity.getState() == false && incremental.getState() == false);
+			cleanSyntax.setEnabled(snapShot.getState() == false && highSecurity.getState() == false && incremental.getState() == false);
+			sanitizeSyntax.setEnabled(snapShot.getState() == false && highSecurity.getState() == false && incremental.getState() == false);
+			encryption.setEnabled(snapShot.getState() == false && highSecurity.getState() == false && incremental.getState() == false);
+			userPassword.setEnabled(snapShot.getState() == false && highSecurity.getState() == false && incremental.getState() == false && encryption.getSelectedItem() != "Keep" && encryption.getSelectedItem() != "None");
+			ownerPassword.setEnabled(snapShot.getState() == false && highSecurity.getState() == false && incremental.getState() == false && encryption.getSelectedItem() != "Keep" && encryption.getSelectedItem() != "None");
+
+			if (incremental.getState()) {
+				garbageCollect.setState(false);
+				linearize.setState(false);
+				cleanSyntax.setState(false);
+				sanitizeSyntax.setState(false);
+				encryption.select("Keep");
+			}
+
+			StringBuilder opts = new StringBuilder();
+			if (highSecurity.getState()) {
+				opts.append(",compression=flate");
+				opts.append(",resolution=");
+				opts.append(resolution.getSelectedItem());
+				opts.append(",ocr-language=");
+				opts.append(language.getText());
+			} else {
+				if (decompress.getState()) opts.append(",decompress=yes");
+				if (compress.getState()) opts.append(",compress=yes");
+				if (compressFonts.getState()) opts.append(",compress-fonts=yes");
+				if (compressImages.getState()) opts.append(",compress-images=yes");
+				if (ascii.getState()) opts.append(",ascii=yes");
+				if (prettyPrint.getState()) opts.append(",pretty=yes");
+				if (linearize.getState()) opts.append(",linearize=yes");
+				if (cleanSyntax.getState()) opts.append(",clean=yes");
+				if (sanitizeSyntax.getState()) opts.append(",sanitize=yes");
+				if (encryption.getSelectedItem() == "None") opts.append(",decrypt=yes");
+				if (encryption.getSelectedItem() == "Keep") opts.append(",decrypt=no");
+				if (encryption.getSelectedItem() == "None") opts.append(",encrypt=no");
+				if (encryption.getSelectedItem() == "Keep") opts.append(",encrypt=keep");
+				if (encryption.getSelectedItem() == "RC4, 40bit") opts.append(",encrypt=rc4-40");
+				if (encryption.getSelectedItem() == "RC4, 128bit") opts.append(",encrypt=rc4-128");
+				if (encryption.getSelectedItem() == "AES, 128bit") opts.append(",encrypt=aes-128");
+				if (encryption.getSelectedItem() == "AES, 256bit") opts.append(",encrypt=aes-256");
+				if (userPassword.getText().length() > 0) {
+					opts.append(",user-password=");
+					opts.append(userPassword.getText());
+				}
+				if (ownerPassword.getText().length() > 0) {
+					opts.append(",owner-password=");
+					opts.append(ownerPassword.getText());
+				}
+				opts.append(",permissions=-1");
+				if (garbageCollect.getState() && isPDF && isRedacted)
+					opts.append(",garbage=yes");
+				else
+					opts.append(",garbage=compact");
+			}
+
+			if (opts.charAt(0) == ',')
+				opts.deleteCharAt(0);
+
+			options = opts.toString();
+		}
+
+		String getOptions() {
+			return options;
+		}
+	}
+
+	class Progressmeter extends Dialog implements ActionListener, KeyListener {
+		Label info = new Label("", Label.CENTER);
+		Button cancel = new Button("Cancel");
+		boolean cancelled = false;
+		boolean done = false;
+
+		public Progressmeter(Frame parent, String title, boolean modal, String initialText) {
+			super(parent, title, modal);
+
+			setLayout(new GridLayout(2, 1));
+
+			info.setText(initialText);
+			add(info);
+
+			cancel.addActionListener(this);
+			cancel.addKeyListener(this);
+			add(cancel);
+
+			pack();
+			setResizable(false);
+			cancel.requestFocusInWindow();
+		}
+
+		public void actionPerformed(ActionEvent e) {
+			if (e.getSource() == cancel)
+				cancel();
+		}
+
+		public void keyPressed(KeyEvent e) { }
+		public void keyReleased(KeyEvent e) { }
+
+		public void keyTyped(KeyEvent e) {
+			if (e.getKeyChar() == '\u001b')
+				cancel();
+		}
+
+		public void cancel() {
+			cancelled = true;
+		}
+
+		public void done() {
+			done = true;
+		}
+
+		public boolean progress(String text) {
+			info.setText(text);
+			return cancelled || done;
+		}
+	}
+
+	class OCRProgressmeter extends Progressmeter implements DocumentWriter.OCRListener {
+		int pages;
+
+		public OCRProgressmeter(Frame parent, String title, int pages) {
+			super(parent, title, true, "Progress: Page 65535/65535: 100%");
+			this.pages = pages;
+			progress(-1, 0);
+			setVisible(true);
+		}
+
+		public void done() {
+			super.done();
+			setVisible(false);
+			dispose();
+		}
+
+		public boolean progress(int page, int percent) {
+			StringBuilder text = new StringBuilder();
+
+			if (page >= 0 || pages >= 0) {
+				text.append("Page ");
+				if (page >= 0)
+					text.append(page + 1);
+				else
+					text.append("?");
+			}
+			if (pages >= 0) {
+				text.append("/");
+				text.append(pages);
+				text.append(": ");
+			}
+
+			text.append(percent);
+			text.append("%");
+
+			return progress(text.toString());
+		}
+	}
+
+	class RenderProgressmeter extends Progressmeter {
+		Cookie cookie;
+
+		public RenderProgressmeter(Frame parent, String title, Cookie cookie, final int update) {
+			super(parent, title, false, "Progress: 100%");
+			this.cookie = cookie;
+
+			(new Thread() {
+				public void run() {
+					try {
+						int slept = 0;
+						while (!progress(slept))
+						{
+							sleep(update);
+							slept += update;
+						}
+					} catch (InterruptedException e) {
+					}
+				}
+			}).start();
+		}
+
+		public void cancel() {
+			super.cancel();
+			cookie.abort();
+		}
+
+		public boolean progress(int slept) {
+			int progress = cookie.getProgress();
+			int max = cookie.getProgressMax();
+
+			if (max <= 0 && progress < 100)
+				max = 100;
+			else if (max <= 0 && progress > 100)
+			{
+				int v = progress;
+				max = 10;
+				while (v > 10)
+				{
+					v /= 10;
+					max *= 10;
+				}
+			}
+
+			if (progress >= max)
+				done = true;
+
+			int percent = (int) ((float) progress / max * 100.0f);
+
+			StringBuilder text = new StringBuilder();
+			text.append("Progress: ");
+			text.append(percent);
+			text.append("%");
+
+			if (slept > 0)
+				setVisible(true);
+
+			if (progress(text.toString()))
+			{
+				setVisible(false);
+				dispose();
+				return true;
+			}
+
+			return false;
+		}
+	}
+
+	public static void main(String[] args) {
+		String selectedPath;
+
+		if (args.length <= 0) {
+			FileDialog fileDialog = new FileDialog((Frame)null, "MuPDF Open File", FileDialog.LOAD);
+			fileDialog.setDirectory(System.getProperty("user.dir"));
+			fileDialog.setFilenameFilter(new FilenameFilter() {
+				public boolean accept(File dir, String name) {
+					return Document.recognize(name);
+				}
+			});
+			fileDialog.setVisible(true);
+			if (fileDialog.getFile() == null)
+				System.exit(0);
+			selectedPath = new StringBuffer(fileDialog.getDirectory()).append(File.separatorChar).append(fileDialog.getFile()).toString();
+			fileDialog.dispose();
+		} else {
+			selectedPath = args[0];
+		}
+
+		try {
+			Viewer app = new Viewer(selectedPath);
+			app.setVisible(true);
+		} catch (Exception e) {
+			messageBox(null, "MuPDF Error", "Cannot open \"" + selectedPath + "\": " + e.getMessage() + ".");
+			System.exit(1);
+		}
+	}
+
+	public float getRetinaScale() {
+		// first try Oracle's VM (we should also test for 1.7.0_40 or higher)
+		final String vendor = System.getProperty("java.vm.vendor");
+		boolean isOracle = vendor != null && vendor.toLowerCase().contains("Oracle".toLowerCase());
+		if (isOracle) {
+			GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
+			final GraphicsDevice device = env.getDefaultScreenDevice();
+			try {
+				Field field = device.getClass().getDeclaredField("scale");
+				if (field != null) {
+					field.setAccessible(true);
+					Object scale = field.get(device);
+					if (scale instanceof Integer && ((Integer)scale).intValue() == 2)
+						return 2.0f;
+				}
+			}
+			catch (Exception ignore) {
+			}
+			return 1.0f;
+		}
+
+		// try Apple VM
+		final Float scaleFactor = (Float)Toolkit.getDefaultToolkit().getDesktopProperty("apple.awt.contentScaleFactor");
+		if (scaleFactor != null && scaleFactor.intValue() == 2)
+			return 2.0f;
+
+		return 1.0f;
+	}
+
+	public int getScreenDPI() {
+		try {
+			return Toolkit.getDefaultToolkit().getScreenResolution();
+		} catch (HeadlessException e) {
+			return 72;
+		}
+	}
+
+	public Dimension getScreenSize() {
+		try {
+			return Toolkit.getDefaultToolkit().getScreenSize();
+		} catch (HeadlessException e) {
+			return new Dimension(1920, 1080);
+		}
+	}
+
+	protected static class OutlineItem {
+		protected String title;
+		protected String uri;
+		protected int page;
+		public OutlineItem(String title, String uri, int page) {
+			this.title = title;
+			this.uri = uri;
+			this.page = page;
+		}
+		public String toString() {
+			return title;
+		}
+	}
+
+	public String askPassword() {
+		return passwordDialog(null, "Password");
+	}
+	public void onChapterCountChange(int chapters) {
+		this.chapters = chapters;
+	}
+	public void onPageCountChange(int pages) {
+		this.pages = pages;
+		pageLabel.setText("/ " + pages);
+	}
+	public void onPageChange(Location page, int chapterNumber, int pageNumber, Rect bbox) {
+		this.location = page;
+		this.chapterNumber = chapterNumber;
+		this.pageNumber = pageNumber;
+		this.bbox = bbox;
+		if (pageNumber >= 0 && pageNumber < pages)
+			pageField.setText(String.valueOf(pageNumber + 1));
+		else
+			pageField.setText("");
+		render();
+	}
+	public void onReflowableChange(boolean reflowable) {
+		this.reflowable = reflowable;
+		fontIncButton.setEnabled(reflowable);
+		fontDecButton.setEnabled(reflowable);
+		fontSizeLabel.setEnabled(reflowable);
+	}
+	public void onLayoutChange(int width, int height, int em) {
+	}
+	public void onOutlineChange(ViewerCore.OutlineItem[] outline) {
+		boolean hadOutline = this.outline != null;
+		this.outline = outline;
+		outlineList.removeAll();
+		if (outline != null)
+			for (int i = 0; i < outline.length; i++)
+				outlineList.add(outline[i].title);
+		if (hadOutline)
+			outlinePanel.setVisible(outline != null);
+	}
+	public void onPageContentsChange(Pixmap pixmap, Rect[] links, String[] linkURIs, Quad[][] hits) {
+		this.pixmap = pixmap;
+		this.links = links;
+		this.linkURIs = linkURIs;
+		this.hits = hits;
+		redraw();
+		if (renderMeter != null)
+			renderMeter.done();
+	}
+	public void onSearchStart(Location startPage, Location finalPage, int direction, String needle) {
+		searchField.setEnabled(false);
+	}
+	public void onSearchPage(Location page, String needle) {
+		String text;
+		if (chapters > 1)
+			text = "Searching " + (page.chapter + 1) + "/" + chapters + "-" + page.page + "/" + pages;
+		else
+			text = "Searching " + page.page + "/" + pages;
+		searchStatus.setText(text);
+		searchStatusPanel.validate();
+	}
+	public void onSearchStop(String needle, Location page) {
+		searchField.setEnabled(true);
+		searchNextButton.setEnabled(true);
+		searchPrevButton.setEnabled(true);
+		if (page != null) {
+			doc.gotoLocation(page, null);
+			searchStatus.setText("");
+		} else if (needle != null)
+			searchStatus.setText("Search text not found.");
+		else
+			searchStatus.setText("");
+		searchStatusPanel.validate();
+		pageCanvas.requestFocusInWindow();
+	}
+	public void onSearchCancelled() {
+		searchField.setEnabled(true);
+		searchNextButton.setEnabled(true);
+		searchPrevButton.setEnabled(true);
+		searchStatus.setText("");
+		searchStatusPanel.validate();
+	}
+	public void onOutlineItemChange(int index) {
+		if (index == -1) {
+			int selected = outlineList.getSelectedIndex();
+			if (selected >= 0)
+				outlineList.deselect(selected);
+		} else {
+			outlineList.makeVisible(index);
+			outlineList.select(index);
+		}
+	}
+	public void onMetadataChange(String title, String author, String format, String encryption) {
+		this.title = title;
+		this.author = author;
+		this.format = format;
+		this.encryption = encryption;
+	}
+	public void onPermissionsChange(boolean print, boolean copy, boolean edit, boolean annotate, boolean form, boolean accessibility, boolean assemble, boolean printHq) {
+		this.print = print;
+		this.copy = copy;
+		this.edit = edit;
+		this.annotate = annotate;
+		this.form = form;
+		this.accessibility = accessibility;
+		this.assemble = assemble;
+		this.printHq = printHq;
+	}
+	public void onLinearizedChange(boolean linearized) {
+		this.linearized = linearized;
+	}
+	public void onUpdatesChange(int updates, int firstUpdate) {
+		this.updates = updates;
+		this.firstUpdate = firstUpdate;
+	}
+}