package org.expeditee.gio.swing; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.image.BufferedImage; import java.awt.image.CropImageFilter; import java.awt.image.FilteredImageSource; import java.awt.image.ImageObserver; import java.awt.image.MemoryImageSource; import java.awt.image.PixelGrabber; import java.awt.image.VolatileImage; import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashMap; import javax.imageio.ImageIO; import org.expeditee.core.Colour; import org.expeditee.core.Image; import org.expeditee.core.bounds.AxisAlignedBoxBounds; import org.expeditee.gio.ImageManager; import org.expeditee.gui.DisplayController; import com.sun.pdfview.PDFPage; /** * The Swing version of the image manager. Mappings are maintained through * the Image handle number so that handles can be garbage-collected i.e. this * manager doesn't hold any reference to the handle object itself. Image.finalize() * tells this manager when the handle is being collected so the mapping can be * removed. * * @author cts16 */ public class SwingImageManager extends ImageManager { /** Singleton instance. */ private static SwingImageManager _instance; /** Singleton instantiator. */ public static SwingImageManager getInstance() { if (_instance == null) _instance = new SwingImageManager(); return _instance; } /** Mapping from image handles to actual internal images. */ private HashMap _imageMap; /** Mapping from image handles to image animators (for animated images e.g. GIFs). */ private HashMap _animatorMap; /** Constructor. */ private SwingImageManager() { // Initialise the maps _imageMap = new HashMap(); _animatorMap = new HashMap(); } @Override public org.expeditee.core.Image createImage(int width, int height, boolean hintUseAcceleratedMemory) { // Ensure width and height are sane if (width <= 0 || height <= 0) return null; java.awt.Image swingImage; if (hintUseAcceleratedMemory) { GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); swingImage = ge.getDefaultScreenDevice().getDefaultConfiguration().createCompatibleVolatileImage(width, height); } else { swingImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); } return register(swingImage, false); } @Override public org.expeditee.core.Image createImage(int width, int height, int[] pixelData) { // Ensure width and height are sane if (width <= 0 || height <= 0) return null; // Make sure the caller has provided enough pixel data if (pixelData == null || pixelData.length < (width * height)) return null; // Create the Swing image java.awt.Image swingImage; MemoryImageSource memory_image = new MemoryImageSource(width, height, pixelData, 0, width); swingImage = Toolkit.getDefaultToolkit().createImage(memory_image); // Register the image and return its handle return register(swingImage, false); } @Override public org.expeditee.core.Image createImage(int width, int height, PDFPage page) { // Ensure width and height are sane if (width <= 0 || height <= 0) return null; // Ensure we have a valid PDFPage if (page == null) return null; // Get the Swing image of the page java.awt.Image swingImage = page.getImage(width, height, new Rectangle(0, 0, (int)page.getBBox().getWidth(), (int)page.getBBox().getHeight()), null, true, true); // Register it and return the handle return register(swingImage, false); } @Override public org.expeditee.core.Image createImage(byte[] rawImageData) { // Get the image from the given file java.awt.Image swingImage = Toolkit.getDefaultToolkit().createImage(rawImageData); if (swingImage == null) return null; // Register and return the handle return register(swingImage, true); } /** * Registers the given swing image with the Expeditee image system. Not available from the abstract interface. * * @param image * The image to register. * * @return The internal image handle. */ public org.expeditee.core.Image createImage(java.awt.Image image) { return register(image, false); } @Override public org.expeditee.core.Image createImageAsCroppedCopy(org.expeditee.core.Image orig, int x, int y, int width, int height) { // Ensure width and height are sane if (width <= 0 || height <= 0) return null; // Make sure the original is a valid image java.awt.Image swingOrig = getInternalImage(orig); if (swingOrig == null) return null; // Make sure the given coordinate are within the bounds of the image AxisAlignedBoxBounds selectedArea = new AxisAlignedBoxBounds(x, y, width, height); if (!orig.getBounds().completelyContains(selectedArea)) return null; // Create a new image that is a cropped version of the original java.awt.Image swingImage = Toolkit.getDefaultToolkit().createImage( new FilteredImageSource(swingOrig.getSource(), new CropImageFilter(x, y, width, height) ) ); // Register and return the handle return register(swingImage, false); } @Override public org.expeditee.core.Image getImage(URL url) { // Make sure url is valid if (url == null) return null; // Get the image from the given URL java.awt.Image swingImage = Toolkit.getDefaultToolkit().createImage(url); if (swingImage == null) return null; // Register and return the handle return register(swingImage, true); } @Override public org.expeditee.core.Image getImage(String filename) { // Make sure the filename is valid if (filename == null) return null; // Get the image from the given file java.awt.Image swingImage = Toolkit.getDefaultToolkit().createImage(filename); if (swingImage == null) return null; // Register and return the handle return register(swingImage, true); } @Override public org.expeditee.core.Image getImage(HttpURLConnection connection) throws IOException { // Make sure the connection is valid if (connection == null) return null; // Spoofing a widely accepted User Agent, since some sites refuse to serve non-webbrowser clients connection.setRequestProperty("User-Agent", "Mozilla/5.0"); // Read the image from the connection BufferedImage swingImage = ImageIO.read(connection.getInputStream()); if (swingImage == null) return null; // Register and return the handle return register(swingImage, false); } @Override public synchronized void releaseImage(org.expeditee.core.Image image) { // Make sure the handle corresponds to a valid Swing image java.awt.Image swingImage = getInternalImage(image); if (swingImage == null) return; // Release and forget about the Swing image swingImage.flush(); _imageMap.remove(image.getHandle()); // If this image is animated, stop the animator SelfAnimatingImageObserver obs = getAnimator(image); if (obs != null) { obs.deactivate(); _animatorMap.remove(image.getHandle()); } } @Override public synchronized boolean isImageValid(Image image) { // Null image is invalid if (image == null) return false; // Image is valid if it's in the map return _imageMap.containsKey(image.getHandle()); } @Override public int getWidth(org.expeditee.core.Image image) { // Make sure the image is valid java.awt.Image swingImage = getInternalImage(image); if (swingImage == null) return Image.INVALID_SIZE; // Create a blocking observer so we don't return without an answer BlockingImageObserver obs = new BlockingImageObserver(ImageObserver.WIDTH); // Try and get the width now if it's available int width = swingImage.getWidth(obs); // If it's not available yet, block until it is if (width == Image.INVALID_SIZE) { obs.attemptWait(); width = obs.width; } return width; } @Override public int getHeight(org.expeditee.core.Image image) { // Make sure the image is valid java.awt.Image swingImage = getInternalImage(image); if (swingImage == null) return Image.INVALID_SIZE; // Create a blocking observer so we don't return without an answer BlockingImageObserver obs = new BlockingImageObserver(ImageObserver.HEIGHT); // Try and get the height now if it's available int height = swingImage.getHeight(obs); // If it's not available yet, block until it is if (height == Image.INVALID_SIZE) { obs.attemptWait(); height = obs.height; } return height; } @Override public Colour[] getPixels(org.expeditee.core.Image image, int x, int y, int width, int height) { // Make sure we have a valid image java.awt.Image swingImage = getInternalImage(image); if (swingImage == null) return null; // Make sure width and height are reasonable if (width <= 0 || height <= 0) return null; // Make sure the given coordinate are within the bounds of the image AxisAlignedBoxBounds selectedArea = new AxisAlignedBoxBounds(x, y, width, height); AxisAlignedBoxBounds bounds = image.getBounds(); if (!bounds.completelyContains(selectedArea)) { return null; } // Create an pixel grabber to get the pixel values int[] pixels = new int[width * height]; PixelGrabber grabber = new PixelGrabber(swingImage, x, y, width, height, pixels, 0, width); // Block until we've grabbed the pixels boolean pixelsGrabbed = false; while(!pixelsGrabbed) { try { grabber.grabPixels(0); pixelsGrabbed = true; } catch (InterruptedException e) { } } // Return the colour of the grabbed pixel Colour[] ret = new Colour[width * height]; for (int i = 0; i < (width * height); i++) { ret[i] = Colour.fromARGB32BitPacked(pixels[i]); } return ret; } @Override public void setPixel(org.expeditee.core.Image image, int x, int y, Colour c) { // Make sure we have a valid image java.awt.Image swingImage = getInternalImage(image); if (swingImage == null) return; // Make sure the given coordinate is inside the image if (!image.getBounds().contains(x, y)) return; // Draw to the pixel Graphics g = swingImage.getGraphics(); g.setColor(SwingConversions.toSwingColor(c)); g.fillRect(x, y, 1, 1); } @Override public boolean writeImageToDisk(org.expeditee.core.Image image, String format, File file) throws IOException { // Validate parameters if (format == null || file == null) return false; // Make sure we have a valid image java.awt.Image swingImage = getInternalImage(image); if (swingImage == null) return false; // Write the image to disk if (swingImage instanceof BufferedImage) { return ImageIO.write((BufferedImage) swingImage, format, file); } else { assert(swingImage instanceof VolatileImage); return ImageIO.write(((VolatileImage) swingImage).getSnapshot(), format, file); } } /** Gets the graphics context for drawing on this image. */ public Graphics2D getImageGraphics(Image image) { // Make sure we have a valid image java.awt.Image swingImage = getInternalImage(image); if (swingImage == null) return null; // Get the graphics context for the image try { if (swingImage instanceof VolatileImage) { return ((VolatileImage) swingImage).createGraphics(); } else { assert (swingImage instanceof BufferedImage); return ((BufferedImage) swingImage).createGraphics(); } } catch (Exception e) { e.printStackTrace(System.out); return null; } } /** Gets the Swing image associated with the given handle, or null if none is. */ public synchronized java.awt.Image getInternalImage(org.expeditee.core.Image image) { // Make sure we have a valid image if (!isImageValid(image)) return null; // Return the internal image return _imageMap.get(image.getHandle()); } /** Creates and returns a handle for the given Swing image to use for future reference. */ private synchronized org.expeditee.core.Image register(java.awt.Image swingImage, boolean fromDisk) { // TODO: Add animators. cts16 // TODO: Only add animators for images we know are animated. cts16 // Create a new handle for the image org.expeditee.core.Image image = Image.get(fromDisk); // Put the handle and image in the map _imageMap.put(image.getHandle(), swingImage); // Return the handle return image; } /** * Gets an animator for the given image. Will reuse an old one if it exists, * or create one if not. */ public synchronized SelfAnimatingImageObserver getAnimator(org.expeditee.core.Image image) { SelfAnimatingImageObserver obs = _animatorMap.get(image.getHandle()); if (obs == null) { obs = new SelfAnimatingImageObserver(); _animatorMap.put(image.getHandle(), obs); } return _animatorMap.get(image.getHandle()); } /** TODO: Comment. cts16 */ public java.awt.image.BufferedImage getBufferedImageForImage(org.expeditee.core.Image image) { if (image == null) return null; java.awt.Image swingImage = getInternalImage(image); if (swingImage instanceof VolatileImage) { return ((VolatileImage) swingImage).getSnapshot(); } // The follow uses an internal sun class. // // SwingImageManager.java:427: warning: ToolkitImage is internal proprietary API and may be removed in a future release // // Back in the days of JDK8 this was OK to use, but now a hidden/internal class // While the following code does perform a runtime type-check first, it does result in the above warning, and // given that the class is internal, it should be getting return directly, thus exposig the class // -David. /*else if (swingImage instanceof sun.awt.image.ToolkitImage) { return ((sun.awt.image.ToolkitImage) swingImage).getBufferedImage(); } */ else { assert(swingImage instanceof BufferedImage); return (BufferedImage) swingImage; } } /** * Image observer which blocks the calling thread until image is fully loaded. */ public static class BlockingImageObserver implements ImageObserver { private int _waitingOn; private boolean _done = false; public int x, y, width, height; public BlockingImageObserver() { this(ImageObserver.ALLBITS); } public BlockingImageObserver(int waitingOn) { _waitingOn = waitingOn; } @Override public boolean imageUpdate(java.awt.Image img, int infoflags, int x, int y, int width, int height) { if ((infoflags & _waitingOn) == _waitingOn) { this.x = x; this.y = y; this.width = width; this.height = height; synchronized (this) { _done = true; notify(); } return true; } return false; } public void attemptWait() { synchronized (this) { while (!_done) { try { wait(); } catch (InterruptedException e) { } } } } } /** * Image observer which refreshes the display to show a new frame from an animated image. */ public static class SelfAnimatingImageObserver implements ImageObserver { private boolean _active; public SelfAnimatingImageObserver() { _active = true; } public void activate() { _active = true; } public void deactivate() { _active = false; } @Override public boolean imageUpdate(java.awt.Image img, int infoflags, int x, int y, int width, int height) { if (!_active) return false; DisplayController.requestRefresh(false); return true; } } /** * Image observer which refreshes the display once an image finishes loading. */ public static class LoadCompletedImageObserver implements ImageObserver { @Override public boolean imageUpdate(java.awt.Image img, int infoflags, int x, int y, int width, int height) { if ((infoflags & ImageObserver.ALLBITS) != 0) { DisplayController.requestRefresh(false); return false; } return true; } } }