package org.skylion.mangareader.util; import java.awt.AWTException; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Insets; import java.awt.MenuContainer; import java.awt.Point; import java.awt.RenderingHints; import java.awt.Robot; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.net.URL; import javax.imageio.ImageIO; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; /** * ImagePanel * This class extends JLabel allowing zooming and panning of an image contained within a JLabel. * @author Aaron Gokaslan (Skylion) */ public class ImagePanel extends JLabel implements MenuContainer, Serializable, SwingConstants {//Applicable interfaces /** * Auto-generated serial long */ private static final long serialVersionUID = 5026409875485764366L; private BufferedImage image; //The image you want to handle private double scale; //Scale factor private double x = 0, y = 0; //The image origin. private double startX, startY; //Temporary instance data used for panning private boolean isZoomed = false;//Determine is isZoomed /** * Constructor */ public ImagePanel(){ this((BufferedImage)null); } /** * Constructor with Text * @param text */ public ImagePanel(String text) { this(); setText(text); } @Deprecated public ImagePanel(Icon icon){ this(extractImageFromIcon(icon)); } /** * Constructor * @param text * @param HorizontalAlignment */ public ImagePanel(String text, int HorizontalAlignment) { this(); setText(text); this.setHorizontalAlignment(HorizontalAlignment); } @Deprecated public ImagePanel(Icon image, int HorizontalAlignment) { this(image); this.setHorizontalAlignment(HorizontalAlignment); } @Deprecated public ImagePanel(String text, Icon image, int HorizontalAlignment) { this(image); setText(text); this.setHorizontalAlignment(HorizontalAlignment); } /** * Constructor * @param file to get read the buffered image from. * @throws IOException if it cannot read the image */ public ImagePanel(File file) throws IOException{ this(ImageIO.read(file)); } /** * Constructor * @param url reads the image from URL. * @throws IOException if something goes wrong. */ public ImagePanel(URL url) throws IOException{ this(ImageIO.read(url)); } public ImagePanel(BufferedImage image) { super(); scale = 1.0; loadImage(image); this.setOpaque(true);//Required for background to display addMouseListeners(); this.setDoubleBuffered(true); startX = startY = x = y = 0; } /** * Adds mouse listeners to the image panel */ private void addMouseListeners(){ this.addMouseListener(new MouseAdapter(){ @Override public void mousePressed(MouseEvent me){ if(getImage()==null){ return; } if(SwingUtilities.isRightMouseButton(me)){//Zooms in and out if(!isZoomed){ setScale(scale*1.5); double dx = me.getX() - getWidth()/2; double dy = me.getY() - getHeight()/2; dx*=getZoomFactor(); dy*=getZoomFactor(); x-=dx; y-=dy; setDoubleBuffered(false); isZoomed = true; } else{ // Point p = me.getPoint(); // p.translate(-getWidth()/2, -getHeight()/2); // System.out.println(p); setScale(getPreferredScale()); setDoubleBuffered(true); x = y = 0; isZoomed = false; } } else if(isZoomed && SwingUtilities.isLeftMouseButton(me)){//Begins to zoom super.mousePressed(me); //Changes mouse cursor ImagePanel.this.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); //Gets instance data startX = me.getX(); startY = me.getY(); } } public void mouseReleased(MouseEvent me){ ImagePanel.this.setCursor(Cursor.getDefaultCursor());//Returns cursor } }); this.addMouseMotionListener(new MouseAdapter(){//This listener handles panning. @Override public void mouseDragged(MouseEvent me){ if(scale!=getPreferredScale() && SwingUtilities.isLeftMouseButton(me) && getImage()!=null){ setDoubleBuffered(false); //Calculates motion + acceleration double xDrift = me.getX() - startX; double yDrift = me.getY() - startY; if(isXValid(x+xDrift)){ x+=xDrift; startX = me.getX(); } if(isYValid(y+yDrift)){ y+=yDrift; startY = me.getY(); } repaint(); setDoubleBuffered(true); } } }); this.addMouseWheelListener(new MouseAdapter(){//This listener zooms using mouse wheel @Override public void mouseWheelMoved(MouseWheelEvent me){ if(getImage() == null){ return; } double scaleChange = me.getWheelRotation()/100d; if(!(getZoomFactor()<=.25 && scaleChange<0)){ setScale(getScale() + scaleChange); } double dx = me.getX() - getWidth()/2; double dy = me.getY() - getHeight()/2; dx/=getZoomFactor(); dy/=getZoomFactor(); if(!isXValid(x-dx) || !isYValid(y-dx)){ return; } x-=dx; y-=dy; try {//Makes the origin reset only happen once. Robot bot = new Robot(); Point loc = getLocationOnScreen(); loc.translate(getWidth()/2, getHeight()/2); bot.mouseMove(loc.x, loc.y); } catch (AWTException e) { e.printStackTrace(); } } }); this.addComponentListener(new ComponentAdapter(){//Resizes image when component resizes @Override public void componentResized(ComponentEvent ce){ if(!isZoomed){ setScale(getPreferredScale()); } } }); } /** * Determines if the value + the x value will be valid * @param x The value you want to pan to. * @return True if valid else false; */ private boolean isXValid(double x){ BufferedImage image = getImage(); double xImageCenter = getX() + image.getWidth()/2; return x+xImageCenter>=image.getMinX() && x+xImageCenter<=image.getWidth(); } /** * Determines if the value + the y value will be valid * @param y The value you want to pan to. * @return True if valid else false; */ private boolean isYValid(double y){ BufferedImage image = getImage(); double yImageCenter = getY() + image.getHeight()/2; return y+yImageCenter>=image.getMinY() && y+yImageCenter<=image.getHeight(); } @Override protected void paintComponent(Graphics g){ if(image == null){//Does nothing special if no image is specified super.paintComponent(g); return; } BufferedImage image = this.image;//Prevents the image from being painted twice. this.image = null; Graphics2D g2 = (Graphics2D)g; setRenderingHints(g2); super.paintComponent(g2); this.image = image; int w = getWidth(); int h = getHeight(); int imageWidth = image.getWidth(); int imageHeight = image.getHeight(); double x = (w - scale * imageWidth)/2; double y = (h - scale * imageHeight)/2; AffineTransform at = AffineTransform.getTranslateInstance(x,y); at.scale(scale, scale); at.translate(this.x, this.y); g2.drawRenderedImage(image, at); } /** * Sets Rendering Hints for the graphics object * @param g2 The Graphics2D to apply the rendering hints to */ private void setRenderingHints(Graphics2D g2){ g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); } /** * Sets the PreferredSize by modifying the image scale */ @Override public void setPreferredSize(Dimension d){ if(image == null){ super.setPreferredSize(d); } Dimension prefSize = this.getPreferredSize(); int h = (int)(d.height/(double)prefSize.height+.5); int w = (int)(d.width/(double)prefSize.width+.5); if(h>w){ setScale(h*scale); } else{ setScale(w*scale); } } /** * Sets the scale of the image * @param The new scale in percent that you want to set the iage to */ protected void setScale(double s) { if(s<=0){ return; } scale = s; repaint(); } /** * Overriden to convert icon into images. */ @Deprecated @Override public void setIcon(Icon icon){ loadImage(extractImageFromIcon(icon)); } /** * Returns null to prevent painting issues. * Use get image instead. */ @Deprecated @Override public Icon getIcon(){ if(image!=null){ return new ImageIcon(image); } return null;//Must return null for painting sake } /** * @return The current Scale factor (from the image's actual pixel size) */ protected double getScale(){ return scale; } /** * Return the preferred size based off of image scale (-1 */ @Override public Dimension getPreferredSize() { if(image == null){ return super.getPreferredSize(); } int w = (int)(scale * image.getWidth()); int h = (int)(scale * image.getHeight()); return new Dimension(w, h); } /** * @return Current image in image panel */ public BufferedImage getImage(){ return image; } /** * Sets the offset of the image from the Center * @param x The image offset * @param y The image offset */ protected void setImageOffset(int x, int y){ this.x = x; this.y = y; repaint(); } /** * Loads the image * @param image The current image */ public void loadImage(BufferedImage image){ this.image = image; if(image == null){ return; } setScale(this.getPreferredScale(image)); x = y = 0; repaint(); } /** * Calculates the ideal zoom factor to make the image fit into the JLabel * @return the ideal zoom factor based off of the image */ protected double getPreferredScale(){ return getPreferredScale(image); } /** * Calculates the current zoom factor (eg. X0.5, X2, X3, X4...) * @return the current zoom factor as a double. */ public double getZoomFactor(){ return getScale()/getPreferredScale(); } /** * Calculates the preferredScale based of the specified image * @param The image to calculate the preferred scale from * @return The preferred scale or negative 1 the image is null. */ protected double getPreferredScale(BufferedImage image){ if(image == null){ return -1; } int ih = image.getHeight(); int iw = image.getWidth(); Insets insets = this.getInsets(); int h, w; double scale = 1.0; if(this.getHeight() > 0 && this.getWidth() > 0){ h = this.getHeight() - insets.top - insets.bottom; w = this.getWidth() - insets.left - insets.right; } else{ h = this.getPreferredSize().height - insets.top - insets.bottom; w = this.getPreferredSize().width - insets.left - insets.right; } if(h/(double)w<ih/(double)iw){ scale *= h/(double)ih; } else{ scale *= w/(double)iw; } return scale; } /** * Converts an image to a BufferedImage * @param The image to extract the BufferedIMage * @return The extracted BufferedImage */ private static BufferedImage convertToBufferedImage(Image image){ if(image instanceof sun.awt.image.ToolkitImage){ return ((sun.awt.image.ToolkitImage) image).getBufferedImage(); } else if(image instanceof Image){ return (BufferedImage)image; } else{ return null; } } /** * Extracts a BufferedImage from an Icon * @param icon The icon to extract the image from * @return The extracted BufferedImage */ private static BufferedImage extractImageFromIcon(Icon icon){ if(icon == null || !(icon instanceof ImageIcon)){ return null; } Image image = (Image)((ImageIcon)icon).getImage(); if(image == null){ return null; } return convertToBufferedImage(image); } }