package org.expeditee.gio.javafx; import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; 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 com.sun.pdfview.PDFPage; import javafx.embed.swing.SwingFXUtils; import javafx.scene.image.PixelFormat; import javafx.scene.image.WritableImage; import java.lang.UnsupportedOperationException; public class JavaFXImageManager extends ImageManager { /** Singleton instance. */ private static JavaFXImageManager _instance; /** Singleton instantiator. */ public static JavaFXImageManager getInstance() { if (_instance == null) _instance = new JavaFXImageManager(); return _instance; } /** Mapping from image handles to actual internal images. */ private HashMap _imageMap; /** Constructor. */ private JavaFXImageManager() { // Initialise the map _imageMap = new HashMap(); } @Override public Image createImage(int width, int height, boolean hintUseAcceleratedMemory) { // Note: hintUseAcceleratedMemory is ignored. cts16 // Ensure width and height are sane if (width <= 0 || height <= 0) return null; // Create the image WritableImage jfxImage = new WritableImage(width, height); // Register and return the handle return register(jfxImage, false); } @Override public 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 image WritableImage jfxImage = new WritableImage(width, height); // Copy the pixel data to the image jfxImage.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getIntArgbInstance(), pixelData, 0, width); // Register and return the handle return register(jfxImage, false); } @Override public Image createImage(int width, int height, PDFPage page) { // TODO: Currently isn't implemented properly. cts16 return createImage(width, height); } /** Create a registration for an internal-type image. Not part of the general public interface. */ public Image createImage(javafx.scene.image.Image image) { // Make sure we have a valid image if (image == null || image.getWidth() == 0.0 || image.getHeight() == 0.0) return null; // Create a writable copy of the original javafx.scene.image.WritableImage writableImage = new WritableImage(image.getPixelReader(), (int) image.getWidth(), (int) image.getHeight()); // Register the image return register(writableImage, false); } @Override public Image createImage(byte[] rawImageData) { throw new UnsupportedOperationException(); } @Override public Image createImageAsCroppedCopy(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 javafx.scene.image.WritableImage jfxOrig = getInternalImage(orig); if (jfxOrig == 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 the cropped size javafx.scene.image.WritableImage jfxCropped = new WritableImage(jfxOrig.getPixelReader(), x, y, width, height); // Register and return the handle return register(jfxCropped, false); } @Override public Image getImage(URL url) { // JavaFX loads all images via a string url return getImage(url.toString()); } @Override public Image getImage(String filename) { // Make sure we have a filename if (filename == null) return null; // Make sure we include the file protocol for files if (!filename.startsWith("file")) { try { URL url = new URL("file", null, filename); filename = url.toString(); } catch (MalformedURLException e) { e.printStackTrace(); return null; } } // Load the file as read only javafx.scene.image.Image jfxReadOnlyImage; try { jfxReadOnlyImage = new javafx.scene.image.Image(filename, false); } catch (Exception e) { return null; } // Create the writable copy javafx.scene.image.WritableImage jfxImage = new javafx.scene.image.WritableImage(jfxReadOnlyImage.getPixelReader(), (int) jfxReadOnlyImage.getWidth(), (int) jfxReadOnlyImage.getHeight()); // Register and return the handle return register(jfxImage, false); } @Override public Image getImage(HttpURLConnection connection) throws IOException { // Make sure the connection is valid if (connection == null) return null; // Load the image as read only javafx.scene.image.Image jfxReadOnlyImage = new javafx.scene.image.Image(connection.getInputStream()); // Block until jfxReadOnlyImage has a valid width/height ensureLoaded(jfxReadOnlyImage); // Create the writable copy javafx.scene.image.WritableImage jfxImage = new javafx.scene.image.WritableImage(jfxReadOnlyImage.getPixelReader(), (int) jfxReadOnlyImage.getWidth(), (int) jfxReadOnlyImage.getHeight()); // Register and return the handle return register(jfxImage, false); } @Override public synchronized void releaseImage(org.expeditee.core.Image image) { // Make sure the handle corresponds to a valid JavaFX image javafx.scene.image.WritableImage jfxImage = getInternalImage(image); if (jfxImage == null) return; // Release and forget about the JavaFX image if (jfxImage != null) { _imageMap.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(Image image) { // Make sure the handle corresponds to a valid JavaFX image javafx.scene.image.WritableImage jfxImage = getInternalImage(image); if (jfxImage == null) return Image.INVALID_SIZE; // Return the width return (int) jfxImage.getWidth(); } @Override public int getHeight(Image image) { // Make sure the handle corresponds to a valid JavaFX image javafx.scene.image.WritableImage jfxImage = getInternalImage(image); if (jfxImage == null) return Image.INVALID_SIZE; // Return the height return (int) jfxImage.getHeight(); } @Override public Colour[] getPixels(Image image, int x, int y, int width, int height) { // Make sure we have a valid image javafx.scene.image.WritableImage jfxImage = getInternalImage(image); if (jfxImage == 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); if (!image.getBounds().completelyContains(selectedArea)) return null; // Create an array to get the pixel values int[] pixels = new int[width * height]; // Read the values of the desired pixels jfxImage.getPixelReader().getPixels(x, y, width, height, PixelFormat.getIntArgbInstance(), pixels, 0, width); // 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(Image image, int x, int y, Colour c) { // Make sure we have a valid image javafx.scene.image.WritableImage jfxImage = getInternalImage(image); if (jfxImage == null) return; // Make sure we are inside the image if (!image.getBounds().contains(x, y)) return; // Set the colour of the pixel jfxImage.getPixelWriter().setColor(x, y, JavaFXConversions.toJavaFXColor(c)); } @Override public boolean writeImageToDisk(Image image, String format, File file) throws IOException { // Validate parameters if (format == null || file == null) return false; // Make sure we have a valid image javafx.scene.image.Image jfxImage = getInternalImage(image); if (jfxImage == null) return false; // Create a Swing image (the internets said this is how you do it :P) // TODO: see if there is a Swing-less way of doing this. cts16 java.awt.image.BufferedImage swingImage = SwingFXUtils.fromFXImage(jfxImage, null); // Write the image to disk return ImageIO.write(swingImage, format, file); } /** Creates and returns a handle for the given JavaFX image to use for future reference. */ private synchronized Image register(javafx.scene.image.WritableImage jfxImage, boolean fromDisk) { // TODO: Add animators. cts16 // TODO: Only add animators for images we know are animated. cts16 Image image = Image.get(fromDisk); _imageMap.put(image.getHandle(), jfxImage); return image; } /** Gets the JavaFX image associated with the given handle, or null if none is. */ public synchronized javafx.scene.image.WritableImage getInternalImage(Image image) { if (!isImageValid(image)) return null; return _imageMap.get(image.getHandle()); } /** Blocks the calling thread until the given image is fully loaded. */ private void ensureLoaded(javafx.scene.image.Image jfxImage) { JavaFXMiscManager.waitUntilPropertyEquals(jfxImage.progressProperty(), 1.0); } }