public abstract void saveAsImage(Image img, File target, String format)
throws IOException;
+ /**
+ * Scale a dimension.
+ *
+ * @param areaWidth
+ * the base width of the target dimension for snap sizes
+ * @param areaHeight
+ * the base height of the target dimension for snap sizes
+ * @param imageWidth
+ * the actual image width
+ * @param imageHeight
+ * the actual image height
+ * @param zoom
+ * the zoom factor, or -1 for snap size
+ * @param zoomSnapWidth
+ * if snap size, TRUE to snap to width (and FALSE, snap to
+ * height)
+ *
+ * @return the scaled size, width is [0] and height is [1] (minimum is 1x1)
+ */
+ protected static Integer[] scaleSize(int areaWidth, int areaHeight,
+ int imageWidth, int imageHeight, double zoom, boolean zoomSnapWidth) {
+ int width;
+ int height;
+ if (zoom > 0) {
+ width = (int) Math.round(imageWidth * zoom);
+ height = (int) Math.round(imageHeight * zoom);
+ } else {
+ if (zoomSnapWidth) {
+ width = areaWidth;
+ height = (int) Math.round(
+ (((double) areaWidth) / imageWidth) * imageHeight);
+ } else {
+ height = areaHeight;
+ width = (int) Math.round(
+ (((double) areaHeight) / imageHeight) * imageWidth);
+
+ }
+ }
+
+ if (width < 1)
+ width = 1;
+ if (height < 1)
+ height = 1;
+
+ return new Integer[] { width, height };
+ }
+
/**
* Return the EXIF transformation flag of this image if any.
*
}
if (!node.getChildren().isEmpty()) {
- // TODO (see things with icons included in viewer)
+ // TODO allow an image or ">", viewer
down = new JToggleButton(">");
final JPopupMenu popup = new JPopupMenu();
package be.nikiroo.utils.ui;
+ import java.awt.Dimension;
+ import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
+ import be.nikiroo.utils.IOUtils;
import be.nikiroo.utils.Image;
import be.nikiroo.utils.ImageUtils;
import be.nikiroo.utils.StringUtils;
* @author niki
*/
public class ImageUtilsAwt extends ImageUtils {
+ /**
+ * A rotation to perform on an image.
+ *
+ * @author niki
+ */
+ public enum Rotation {
+ /** No rotation */
+ NONE,
+ /** Rotate the image to the right */
+ RIGHT,
+ /** Rotate the image to the left */
+ LEFT,
+ /** Rotate the image by 180° */
+ UTURN
+ }
+
@Override
protected boolean check() {
// Will not work if ImageIO is not available
* in case of IO error
*/
public static BufferedImage fromImage(Image img) throws IOException {
+ return fromImage(img, Rotation.NONE);
+ }
+
+ /**
+ * Convert the given {@link Image} into a {@link BufferedImage} object,
+ * respecting the EXIF transformations if any.
+ *
+ * @param img
+ * the {@link Image}
+ * @param rotation
+ * the rotation to apply, if any (can be null, same as
+ * {@link Rotation#NONE})
+ *
+ * @return the {@link Image} object
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ public static BufferedImage fromImage(Image img, Rotation rotation)
+ throws IOException {
InputStream in = img.newInputStream();
BufferedImage image;
try {
String extra = "";
if (img.getSize() <= 2048) {
try {
- extra = ", content: "
- + new String(img.getData(), "UTF-8");
+ byte[] data = null;
+ InputStream inData = img.newInputStream();
+ try {
+ data = IOUtils.toByteArray(inData);
+ } finally {
+ inData.close();
+ }
+ extra = ", content: " + new String(data, "UTF-8");
} catch (Exception e) {
extra = ", content unavailable";
}
break;
}
+ if (rotation == null)
+ rotation = Rotation.NONE;
+
+ switch (rotation) {
+ case RIGHT:
+ if (affineTransform == null) {
+ affineTransform = new AffineTransform();
+ }
+ affineTransform.translate(height, 0);
+ affineTransform.rotate(Math.PI / 2);
+
+ int tmp = width;
+ width = height;
+ height = tmp;
+
+ break;
+ case LEFT:
+ if (affineTransform == null) {
+ affineTransform = new AffineTransform();
+ }
+ affineTransform.translate(0, width);
+ affineTransform.rotate(3 * Math.PI / 2);
+
+ int temp = width;
+ width = height;
+ height = temp;
+
+ break;
+ case UTURN:
+ if (affineTransform == null) {
+ affineTransform = new AffineTransform();
+ }
+ affineTransform.translate(width, height);
+ affineTransform.rotate(Math.PI);
+ break;
+ default:
+ break;
+ }
+
if (affineTransform != null) {
AffineTransformOp affineTransformOp = new AffineTransformOp(
affineTransform, AffineTransformOp.TYPE_BILINEAR);
return image;
}
+
+ /**
+ * Resize the given image.
+ *
+ * @param areaSize
+ * the base size of the target dimension for snap sizes
+ * @param image
+ * the image to resize
+ * @param zoom
+ * the zoom factor or -1 for snap size
+ * @param zoomSnapWidth
+ * if snap size, TRUE to snap to width (and FALSE, snap to
+ * height)
+ *
+ * @return a new, resized image
+ */
+ public static BufferedImage scaleImage(Dimension areaSize,
+ BufferedImage image, double zoom, boolean zoomSnapWidth) {
+ Integer scaledSize[] = scaleSize(areaSize.width, areaSize.height,
+ image.getWidth(), image.getHeight(), zoom, zoomSnapWidth);
+ int width = scaledSize[0];
+ int height = scaledSize[1];
+ BufferedImage resizedImage = new BufferedImage(width, height,
+ BufferedImage.TYPE_4BYTE_ABGR);
+ Graphics2D g = resizedImage.createGraphics();
+ try {
+ g.drawImage(image, 0, 0, width, height, null);
+ } finally {
+ g.dispose();
+ }
+
+ return resizedImage;
+ }
}
--- /dev/null
+ package be.nikiroo.utils.ui;
+
+ import java.awt.Dimension;
+ import java.awt.LayoutManager;
+ import java.awt.event.ActionEvent;
+ import java.awt.event.ActionListener;
+
+ import javax.swing.BoxLayout;
+ import javax.swing.Icon;
+ import javax.swing.JButton;
+ import javax.swing.JLabel;
+ import javax.swing.JTextField;
+
+ /**
+ * A Swing-based navigation bar, that displays first/previous/next/last page
+ * buttons.
+ *
+ * @author niki
+ */
+ public class NavBar extends ListenerPanel {
+ private static final long serialVersionUID = 1L;
+
+ /** The event that is fired on page change. */
+ public static final String PAGE_CHANGED = "page changed";
+
+ private JTextField page;
+ private JLabel maxPage;
+ private JLabel label;
+
+ private int index = 0;
+ private int min = 0;
+ private int max = 0;
+ private String extraLabel = null;
+
+ private JButton first;
+ private JButton previous;
+ private JButton next;
+ private JButton last;
+
+ /**
+ * Create a new navigation bar.
+ * <p>
+ * The minimum must be lower or equal to the maximum.
+ * <p>
+ * Note than a max of "-1" means "infinite".
+ *
+ * @param min
+ * the minimum page number (cannot be negative)
+ * @param max
+ * the maximum page number (cannot be lower than min, except if
+ * -1 (infinite))
+ *
+ * @throws IndexOutOfBoundsException
+ * if min > max and max is not "-1"
+ */
+ public NavBar(int min, int max) {
+ if (min > max && max != -1) {
+ throw new IndexOutOfBoundsException(
+ String.format("min (%d) > max (%d)", min, max));
+ }
+
+ LayoutManager layout = new BoxLayout(this, BoxLayout.X_AXIS);
+ setLayout(layout);
+
+ // Page navigation
+ first = new JButton();
+ first.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ first();
+ }
+ });
+
+ previous = new JButton();
+ previous.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ previous();
+ }
+ });
+
+ page = new JTextField(Integer.toString(min));
+ page.setPreferredSize(
+ new Dimension(new JButton("1234").getPreferredSize().width,
+ new JButton("dummy").getPreferredSize().height));
+ page.setMaximumSize(new Dimension(Integer.MAX_VALUE,
+ page.getPreferredSize().height));
+ page.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ try {
+ int pageNb = Integer.parseInt(page.getText());
+ if (pageNb < NavBar.this.min || pageNb > NavBar.this.max) {
+ throw new NumberFormatException("invalid");
+ }
+
+ if (setIndex(pageNb))
+ fireActionPerformed(PAGE_CHANGED);
+ } catch (NumberFormatException nfe) {
+ page.setText(Integer.toString(index));
+ }
+ }
+ });
+
+ maxPage = new JLabel("of " + max);
+
+ next = new JButton();
+ next.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ next();
+ }
+ });
+
+ last = new JButton();
+ last.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ last();
+ }
+ });
+
+ // Set the << < > >> "icons"
+ setIcons(null, null, null, null);
+
+ this.add(first);
+ this.add(previous);
+ this.add(new JLabel(" "));
+ this.add(page);
+ this.add(new JLabel(" "));
+ this.add(maxPage);
+ this.add(new JLabel(" "));
+ this.add(next);
+ this.add(last);
+
+ this.add(label = new JLabel(""));
+
+ this.min = min;
+ this.max = max;
+ this.index = min;
+
+ updateEnabled();
+ updateLabel();
+ fireActionPerformed(PAGE_CHANGED);
+ }
+
+ /**
+ * The current index, must be between {@link NavBar#min} and
+ * {@link NavBar#max}, both inclusive.
+ *
+ * @return the index
+ */
+ public int getIndex() {
+ return index;
+ }
+
+ /**
+ * The current index, should be between {@link NavBar#min} and
+ * {@link NavBar#max}, both inclusive.
+ *
+ * @param index
+ * the new index
+ *
+ * @return TRUE if the index changed, FALSE if not (either it was already at
+ * that value, or it is outside of the bounds set by
+ * {@link NavBar#min} and {@link NavBar#max})
+ */
+ public synchronized boolean setIndex(int index) {
+ if (index != this.index) {
+ if (index < min || (index > max && max != -1)) {
+ return false;
+ }
+
+ this.index = index;
+ updateLabel();
+ updateEnabled();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * The minimun page number. Cannot be negative.
+ *
+ * @return the min
+ */
+ public int getMin() {
+ return min;
+ }
+
+ /**
+ * The minimum page number. Cannot be negative.
+ * <p>
+ * May update the index if needed (if the index is < the new min).
+ * <p>
+ * Will also (always) update the label and enable/disable the required
+ * buttons.
+ *
+ * @param min
+ * the new min
+ */
+ public synchronized void setMin(int min) {
+ this.min = min;
+ if (index < min) {
+ index = min;
+ }
+
+ updateEnabled();
+ updateLabel();
+ }
+
+ /**
+ * The maximum page number. Cannot be lower than min, except if -1
+ * (infinite).
+ *
+ * @return the max
+ */
+ public int getMax() {
+ return max;
+ }
+
+ /**
+ * The maximum page number. Cannot be lower than min, except if -1
+ * (infinite).
+ * <p>
+ * May update the index if needed (if the index is > the new max).
+ * <p>
+ * Will also (always) update the label and enable/disable the required
+ * buttons.
+ *
+ * @param max
+ * the new max
+ */
+ public synchronized void setMax(int max) {
+ this.max = max;
+ if (index > max && max != -1) {
+ index = max;
+ }
+
+ maxPage.setText("of " + max);
+ updateEnabled();
+ updateLabel();
+ }
+
+ /**
+ * The current extra label to display.
+ *
+ * @return the current label
+ */
+ public String getExtraLabel() {
+ return extraLabel;
+ }
+
+ /**
+ * The current extra label to display.
+ *
+ * @param currentLabel
+ * the new current label
+ */
+ public void setExtraLabel(String currentLabel) {
+ this.extraLabel = currentLabel;
+ updateLabel();
+ }
+
+ /**
+ * Change the page to the next one.
+ *
+ * @return TRUE if it changed
+ */
+ public synchronized boolean next() {
+ if (setIndex(index + 1)) {
+ fireActionPerformed(PAGE_CHANGED);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Change the page to the previous one.
+ *
+ * @return TRUE if it changed
+ */
+ public synchronized boolean previous() {
+ if (setIndex(index - 1)) {
+ fireActionPerformed(PAGE_CHANGED);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Change the page to the first one.
+ *
+ * @return TRUE if it changed
+ */
+ public synchronized boolean first() {
+ if (setIndex(min)) {
+ fireActionPerformed(PAGE_CHANGED);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Change the page to the last one.
+ *
+ * @return TRUE if it changed
+ */
+ public synchronized boolean last() {
+ if (setIndex(max)) {
+ fireActionPerformed(PAGE_CHANGED);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Set icons for the buttons instead of square brackets.
+ * <p>
+ * Any NULL value will make the button use square brackets again.
+ *
+ * @param first
+ * the icon of the button "go to first page"
+ * @param previous
+ * the icon of the button "go to previous page"
+ * @param next
+ * the icon of the button "go to next page"
+ * @param last
+ * the icon of the button "go to last page"
+ */
+ public void setIcons(Icon first, Icon previous, Icon next, Icon last) {
+ this.first.setIcon(first);
+ this.first.setText(first == null ? "<<" : "");
+ this.previous.setIcon(previous);
+ this.previous.setText(previous == null ? "<" : "");
+ this.next.setIcon(next);
+ this.next.setText(next == null ? ">" : "");
+ this.last.setIcon(last);
+ this.last.setText(last == null ? ">>" : "");
+ }
+
+ /**
+ * Update the label displayed in the UI.
+ */
+ private void updateLabel() {
+ label.setText(getExtraLabel());
+ page.setText(Integer.toString(index));
+ }
+
+ /**
+ * Update the navigation buttons "enabled" state according to the current
+ * index value.
+ */
+ private synchronized void updateEnabled() {
+ first.setEnabled(index > min);
+ previous.setEnabled(index > min);
+ next.setEnabled(index < max || max == -1);
+ last.setEnabled(index < max || max == -1);
+ }
+ }
--- /dev/null
+ package be.nikiroo.utils.ui;
+
+ import java.awt.Window;
+
+ import javax.swing.JDialog;
+ import javax.swing.JLabel;
+ import javax.swing.SwingWorker;
+ import javax.swing.border.EmptyBorder;
+
+ import be.nikiroo.utils.Progress;
+ import be.nikiroo.utils.Progress.ProgressListener;
+
+ /**
+ * A small waiting dialog that will show only if more than X milliseconds passed
+ * before we dismiss it.
+ *
+ * @author niki
+ */
+ public class WaitingDialog extends JDialog {
+ private static final long serialVersionUID = 1L;
+
+ private boolean waitScreen;
+ private Object waitLock = new Object();
+
+ private Progress pg;
+ private ProgressListener pgl;
+
+ /**
+ * Create a new {@link WaitingDialog}.
+ *
+ * @param parent
+ * the parent/owner of this {@link WaitingDialog}
+ * @param delayMs
+ * the delay after which to show the dialog if it is still not
+ * dismiss (see {@link WaitingDialog#dismiss()})
+ */
+ public WaitingDialog(Window parent, long delayMs) {
+ this(parent, delayMs, null, null);
+ }
+
+ /**
+ * Create a new {@link WaitingDialog}.
+ *
+ * @param parent
+ * the parent/owner of this {@link WaitingDialog}
+ * @param delayMs
+ * the delay after which to show the dialog if it is still not
+ * dismiss (see {@link WaitingDialog#dismiss()})
+ * @param pg
+ * the {@link Progress} to listen on -- when it is
+ * {@link Progress#done()}, this {@link WaitingDialog} will
+ * automatically be dismissed as if
+ * {@link WaitingDialog#dismiss()} was called
+ */
+ public WaitingDialog(Window parent, long delayMs, Progress pg) {
+ this(parent, delayMs, pg, null);
+ }
+
+ /**
+ * Create a new {@link WaitingDialog}.
+ *
+ * @param parent
+ * the parent/owner of this {@link WaitingDialog}
+ * @param delayMs
+ * the delay after which to show the dialog if it is still not
+ * dismiss (see {@link WaitingDialog#dismiss()})
+ * @param waitingText
+ * a waiting text to display (note: you may want to subclass it
+ * for nicer UI)
+ */
+ public WaitingDialog(Window parent, long delayMs, String waitingText) {
+ this(parent, delayMs, null, waitingText);
+ }
+
+ /**
+ * Create a new {@link WaitingDialog}.
+ *
+ * @param parent
+ * the parent/owner of this {@link WaitingDialog}
+ * @param delayMs
+ * the delay after which to show the dialog if it is still not
+ * dismiss (see {@link WaitingDialog#dismiss()})
+ * @param pg
+ * the {@link Progress} to listen on -- when it is
+ * {@link Progress#done()}, this {@link WaitingDialog} will
+ * automatically be dismissed as if
+ * {@link WaitingDialog#dismiss()} was called
+ * @param waitingText
+ * a waiting text to display (note: you may want to subclass it
+ * for nicer UI)
+ */
+ public WaitingDialog(Window parent, long delayMs, Progress pg,
+ String waitingText) {
+ super(parent);
+
+ this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+
+ this.pg = pg;
+
+ if (waitingText != null) {
+ JLabel waitingTextLabel = new JLabel(waitingText);
+ this.add(waitingTextLabel);
+ waitingTextLabel.setBorder(new EmptyBorder(10, 10, 10, 10));
+ this.pack();
+ }
+
+ if (pg != null) {
+ pgl = new ProgressListener() {
+ @Override
+ public void progress(Progress progress, String name) {
+ if (WaitingDialog.this.pg.isDone()) {
+ // Must be done out of this thread (cannot remove a pgl
+ // from a running pgl)
+ new SwingWorker<Void, Void>() {
+ @Override
+ protected Void doInBackground() throws Exception {
+ return null;
+ }
+
+ @Override
+ public void done() {
+ dismiss();
+ }
+ }.execute();
+ }
+ }
+ };
+
+ pg.addProgressListener(pgl);
+
+ if (pg.isDone()) {
+ dismiss();
+ return;
+ }
+ }
+
+ final long delay = delayMs;
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException e) {
+ }
+
+ synchronized (waitLock) {
+ if (!waitScreen) {
+ waitScreen = true;
+ setVisible(true);
+ }
+ }
+ }
+ }).start();
+ }
+
+ /**
+ * Notify this {@link WaitingDialog} that the job is done, and dismiss it if
+ * it was already showing on screen (or never show it if it was not).
+ * <p>
+ * Will also dispose the {@link WaitingDialog}.
+ */
+ public void dismiss() {
+ synchronized (waitLock) {
+ if (waitScreen) {
+ setVisible(false);
+ }
+ waitScreen = true;
+ }
+
+ if (pg != null) {
+ pg.removeProgressListener(pgl);
+ }
+
+ dispose();
+ }
+ }