All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tinymediamanager.thirdparty.ImageLoader Maven / Gradle / Ivy

The newest version!
package org.tinymediamanager.thirdparty;

/*
 * @(#)ImageLoader.java
 *
 * $Date$
 *
 * Copyright (c) 2011 by Jeremy Wood.
 * All rights reserved.
 *
 * The copyright of this software is owned by Jeremy Wood. 
 * You may not use, copy or modify this software, except in  
 * accordance with the license agreement you entered into with  
 * Jeremy Wood. For details see accompanying license terms.
 * 
 * This software is probably, but not necessarily, discussed here:
 * https://javagraphics.java.net/
 * 
 * That site should also contain the most recent official version
 * of this software.  (See the SVN repository for more details.)
 */

import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DirectColorModel;
import java.awt.image.ImageConsumer;
import java.awt.image.ImageProducer;
import java.awt.image.IndexColorModel;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Properties;

import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class can convert an abstract Image into a ARGB BufferedImage.
 * 

* It was written to replace the MediaTracker; on my Macs this class is faster and more efficient. It has come to my attention that on * Linux this may not be the case (see my blog for more * discussion.) *

* Also this class has the added advantage of always returning an RGB image. Using other methods (such as ImageIO) may return an arbitrary image type. * (And this class doesn't require a java.awt.Component to initialize; why does the MediaTracker do that? It's just a strange animal.) * */ public class ImageLoader { private static final Logger LOGGER = LoggerFactory.getLogger(ImageLoader.class); private static final DirectColorModel ARGBModel = (DirectColorModel) ColorModel.getRGBdefault(); private static final DirectColorModel RGBModel = new DirectColorModel(32, 0xff0000, 0xff00, 0xff, 0); public static BufferedImage createImage(URL url) { if (url == null) { throw new NullPointerException(); } try { return createImage(Toolkit.getDefaultToolkit().createImage(url), url.toString()); } catch (RuntimeException e) { LOGGER.debug("could not create image: {}", e.getMessage()); throw e; } } public static BufferedImage createImage(File file) { try { return createImage(Toolkit.getDefaultToolkit().createImage(file.getAbsolutePath()), file.getAbsolutePath()); } catch (RuntimeException e) { LOGGER.debug("could not create image: {}", e.getMessage()); throw e; } } /** * This returns an ARGB BufferedImage depicting the argument i. *

* Note that if i is already an ARGB BufferedImage, then it is immediately returned and this method does NOT duplicate it. * * @param i * the source image to create the buffered image of * @return an ARGB BufferedImage identical to the argument. */ public static BufferedImage createImage(Image i) { if (i instanceof BufferedImage) { BufferedImage bi = (BufferedImage) i; int type = bi.getType(); if (type == BufferedImage.TYPE_INT_ARGB) { return bi; } BufferedImage newImage = new BufferedImage(bi.getWidth(), bi.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = newImage.createGraphics(); g.drawImage(bi, 0, 0, null); g.dispose(); return newImage; } try { return createImage(i, null); } catch (RuntimeException e) { LOGGER.debug("could not create image: {}", e.getMessage()); throw e; } } protected static BufferedImage createImage(Image i, String description) { ImageLoader l = new ImageLoader(i.getSource(), null, description); return l.getImage(); } /** * This checks to see if two DirectColorModels are identical. Apparently the "equals" method in DirectColorModel doesn't really work. */ private static boolean equals(DirectColorModel d1, DirectColorModel d2) { if (d1.getAlphaMask() != d2.getAlphaMask()) { return false; } if (d1.getGreenMask() != d2.getGreenMask()) { return false; } if (d1.getRedMask() != d2.getRedMask()) { return false; } if (d1.getBlueMask() != d2.getBlueMask()) { return false; } if (d1.getColorSpace() != d2.getColorSpace()) { return false; } if (d1.isAlphaPremultiplied() != d2.isAlphaPremultiplied()) { return false; } if (d1.getTransferType() != d2.getTransferType()) { return false; } if (d1.getTransparency() != d2.getTransparency()) { return false; } return true; } InnerImageConsumer consumer; List listeners; ImageProducer producer; float progress = 0; String description; /** * This constructs an ImageLoader. As soon as an ImageLoader is constructed the ImageProducer is asked to start producing data. * (However constructing this object will not block waiting on the image data.) * * @param p * the source of this image * @param changeListener * an optional ChangeListener. This will be notified when a change occurs. It can be added here in the constructor because * loading the image begins immediately; depending on how ImageProducer.startProduction is implemented this may be a * blocking call so the ChangeListener needs to be added before the loading begins * @param description * an optional description that may be useful for debugging */ public ImageLoader(ImageProducer p, ChangeListener changeListener, String description) { producer = p; addChangeListener(changeListener); consumer = new InnerImageConsumer(); p.startProduction(consumer); this.description = description; } /** * Adds a ChangeListener to this loader. This listener will be notified either when the image is fully loaded/created, or when * getProgress() changes value. */ public void addChangeListener(ChangeListener l) { if (l == null) { return; } if (listeners == null) { listeners = new ArrayList<>(); } if (listeners.contains(l)) { return; } listeners.add(l); } /** * Removes a ChangeListener from this loader. */ public void removeChangeListener(ChangeListener l) { if (listeners == null) { return; } listeners.remove(l); } /** * Returns a float from [0,1] approximating how much of the image is loaded. If your image were a text document, this is basically telling * you where the text cursor was last placed. However the ImageProducer may make several different iterations over the image to deliver * the complete image, so it is completely possible that this value will range from 0 to 1 a few different times. *

* I wish this could be more precise, but I don't see how more precision is possible given the way the ImageProducer/ * ImageConsumer model works. *

* For the most part, this will be a straight-forward 1-pass system. If you limit yourself to certain types of images (like PNGs) this is probably * the case. */ public float getProgress() { return progress; } /** * This indicates whether this ImageLoader is finished. *

* Unlike getProgress(), this is guaranteed to be 100% accurate. */ public boolean isDone() { return finished; } /** * Returns the dimension of this image, or null if the dimensions are not yet known. */ public Dimension getSize() { if (size == null) { return null; } return new Dimension(size.width, size.height); } /** Fires all change listeners */ protected void fireChangeListeners() { if (listeners == null) { return; } for (ChangeListener l : listeners) { try { l.stateChanged(new ChangeEvent(this)); } catch (Exception e) { LOGGER.warn("failed to inform listener: {}", e.getMessage()); } } } /** * This blocks until the image has finished loading, an error occurs, or the operation has been cancelled. * * @return the ARGB image represented in the original ImageProducer, or null if the operation was cancelled. * @throws RuntimeException * if an error occurred while loading this image */ public BufferedImage getImage() { block(); if (status == ImageConsumer.IMAGEERROR) throw new RuntimeException("An error occurred."); if (status == ImageConsumer.IMAGEABORTED) throw new RuntimeException("The operation was aborted."); return dest; } private Thread waitingThread; private List waitingThreads; /** * Blocks this thread until the image is finished * */ private void block() { Thread t = Thread.currentThread(); int i = 0; synchronized (this) { if (waitingThreads == null) { // there is no list defined if (waitingThread == null) { waitingThread = t; } else { waitingThreads = new ArrayList<>(); waitingThreads.add(waitingThread); waitingThreads.add(t); i = 1; waitingThread = null; } } else { i = waitingThreads.size(); waitingThreads.add(t); } } while (!finished) { try { Thread.sleep(500); } catch (Exception e) { Thread.yield(); } } synchronized (this) { Thread.interrupted(); if (waitingThread == t) { waitingThread = null; } if (waitingThreads != null) { waitingThreads.remove(i); } } } private void unblock() { synchronized (this) { if (waitingThread != null) { waitingThread.interrupt(); } if (waitingThreads != null) { for (Thread t : waitingThreads) { t.interrupt(); } } } } boolean finished = false; int status; Dimension size; Properties properties; BufferedImage dest; class InnerImageConsumer implements ImageConsumer { public InnerImageConsumer() { } public void imageComplete(int completionStatus) { producer.removeConsumer(this); status = completionStatus; finished = true; unblock(); fireChangeListeners(); } int[] indexed; public void setColorModel(ColorModel cm) { try { lastCM = cm; indexed = null; if (cm instanceof IndexColorModel) { IndexColorModel i = (IndexColorModel) cm; if (i.getMapSize() <= 256) { indexed = new int[i.getMapSize()]; for (int a = 0; a < indexed.length; a++) { int r = i.getRed(a); int g = i.getGreen(a); int b = i.getBlue(a); indexed[a] = (255 << 24) + (r << 16) + (g << 8) + b; } int t = i.getTransparentPixel(); if (i.hasAlpha() && t >= 0 && t < indexed.length) { indexed[t] = 0; } } } } catch (RuntimeException | Error e) { LOGGER.debug("could not set color model: {}", e.getMessage()); throw e; } } private transient int[] row; public void setDimensions(int w, int h) { // try to cache the image file; we have up to 5 retries here if we hit the memory cap since we are // hitting the machine hard due to multi CPU image caching int retries = 5; do { try { if (w <= 0) throw new IllegalArgumentException("Width must be greater than zero. (" + w + ")"); if (h <= 0) throw new IllegalArgumentException("Height must be greater than zero. (" + h + ")"); if (size != null) { // eh? already exists? if (size.width == w && size.height == h) return; if (dest != null) { throw new RuntimeException("An image of " + (size.getWidth()) + "x" + size.getHeight() + " was already created. Illegal attempt to call setDimensions(" + w + "," + h + ")"); } } size = new Dimension(w, h); dest = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); row = new int[w]; fireChangeListeners(); break; } catch (OutOfMemoryError e) { // memory limit hit; give it another 500ms time to recover LOGGER.warn("hit memory cap: {}", e.getMessage()); size = null; dest = null; try { Thread.sleep(200); } catch (InterruptedException ignored) { Thread.interrupted(); } } catch (RuntimeException | Error e) { LOGGER.debug("could not set dimensions: {}", e.getMessage()); throw e; } retries--; } while (retries > 0); } public void setHints(int hints) { } public void setPixels(int x, int y, int w, int h, ColorModel cm, byte[] data, int offset, int scanSize) { try { if (size == null) throw new RuntimeException( "The dimensions of this image are not yet defined. Cannot write image data until the dimensions of the image are known."); if (cm == lastCM && indexed != null) { int argb; byte k = 0; int k2 = 0; for (int n = y; n < y + h; n++) { for (int m = x; m < x + w; m++) { k = data[(n - y) * scanSize + (m - x) + offset]; if (k >= 0) { k2 = k; } else { k2 = k + 256; } argb = indexed[k2]; row[m - x] = argb; } dest.getRaster().setDataElements(x, n, w, 1, row); } } else { int transIndex = (cm instanceof IndexColorModel) ? ((IndexColorModel) cm).getTransparentPixel() : -1; int argb; for (int n = y; n < y + h; n++) { for (int m = x; m < x + w; m++) { byte k = data[(n - y) * scanSize + (m - x) + offset]; int k2 = k & 0xff; if (k2 == transIndex) { argb = 0; } else { argb = cm.getRGB(k2); } row[m - x] = argb; } dest.getRaster().setDataElements(x, n, w, 1, row); } } setProgress(x + w, y + h); } catch (RuntimeException | Error e) { LOGGER.debug("could not set pixels: {}", e.getMessage()); throw e; } } private boolean lastCMwasRGB = false; private boolean lastCMwasARGB = false; ColorModel lastCM = null; public void setPixels(int x, int y, int w, int h, ColorModel cm, int[] data, int offset, int scanSize) { try { if (size == null) throw new RuntimeException( "The dimensions of this image are not yet defined. Cannot write image data until the dimensions of the image are known."); if (cm instanceof DirectColorModel) { // don't call "equals" all the time; that's just a waste // this is a little uglier, but it will save hundreds of method calls: boolean quickRGB = (cm == lastCM && lastCMwasRGB); boolean quickARGB = (cm == lastCM && lastCMwasARGB); DirectColorModel d = (DirectColorModel) cm; if (quickARGB || ((!quickRGB) && ImageLoader.equals(d, ARGBModel))) { for (int n = y; n < y + h; n++) { int k = (n - y) * scanSize - x + offset; System.arraycopy(data, k, row, 0, w); dest.getRaster().setDataElements(x, n, w, 1, row); } lastCMwasRGB = false; lastCMwasARGB = true; lastCM = cm; return; } else if (quickRGB || ImageLoader.equals(d, RGBModel)) { // same thing, but no alpha: for (int n = y; n < y + h; n++) { int k = (n - y) * scanSize - x + offset; System.arraycopy(data, k, row, 0, w); // add alpha to every pixel: for (int a = 0; a < w; a++) { row[a] = (row[a] & 0xffffff) + 0xff000000; } dest.getRaster().setDataElements(x, n, w, 1, row); } lastCMwasRGB = true; lastCMwasARGB = false; lastCM = cm; return; } } int argb; // If you know what your ColorModel is, // it'd be good to make a special-case loop // to deal with that data (like the if/then above), // because a million calls to getRGB() is NOT cheap. for (int n = y; n < y + h; n++) { for (int m = x; m < x + w; m++) { argb = cm.getRGB(data[(n - y) * scanSize + (m - x) + offset]); row[m - x] = argb; } dest.getRaster().setDataElements(x, n, w, 1, row); } lastCMwasRGB = false; lastCMwasARGB = false; lastCM = cm; setProgress(x + w, y + h); } catch (RuntimeException | Error e) { LOGGER.debug("could not set pixels: {}", e.getMessage()); throw e; } } public void setProperties(Hashtable p) { try { // TODO: one day integrate these // it wouldn't be hard to extend BufferedImage // and override the methods regarding // "getProperty" if you need to. if (properties == null) properties = new Properties(); properties.putAll(p); } catch (RuntimeException | Error e) { LOGGER.warn("could not set properties: {}", e.getMessage()); throw e; } } } private void setProgress(int x, int y) { progress = ((float) (y * size.width + x)) / ((float) (size.width * size.height)); fireChangeListeners(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy