/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may * obtain a copy of the License at * * http://www.osedu.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS IS" * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing * permissions and limitations under the License. */ package tufts.vue; import tufts.Util; import tufts.DocDump; import static tufts.vue.Resource.*; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; import java.lang.ref.*; import java.net.URL; import java.net.URI; import java.net.URLConnection; import java.io.*; import java.awt.Image; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Transparency; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import javax.swing.ImageIcon; import javax.imageio.*; import javax.imageio.metadata.*; import javax.imageio.event.*; import javax.imageio.stream.*; import javax.xml.xpath.*; import org.w3c.dom.NodeList; /** * * Handle the loading of images in background threads, making callbacks to deliver * results to multiple listeners that can be added at any time during the image fetch, * and caching (memory and disk) with a URI key, using a HashMap with SoftReference's * for the BufferedImage's so if we run low on memory they just drop out of the cache. * * @version $Revision: 1.85 $ / $Date: 2010-02-03 19:17:40 $ / $Author: mike $ * @author Scott Fraize */ public class Images { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(Images.class); private static final boolean ALLOW_HIGH_QUALITY_ICONS = true; private static volatile int LOW_MEMORY_COUNT = 0; // Setting DELAYED_ICONS to true allows maps with lots of images paint something for // each image much faster the first time they're loaded under plentiful memory // conditions, but cause horrible thrashing under low memory conditions, and it // would take even more complexity than we've already got to fix that, so for now // we're just going with immediately created icons. static final boolean DELAYED_ICONS = false; public static void setLowMemory(Object cause) { synchronized (Images.class) { if (LOW_MEMORY_COUNT == 0) { Log.info(Util.TERM_PURPLE + "entering low-memory conditions, cause=" + Util.tags(cause) + Util.TERM_CLEAR); } else { if (DEBUG.Enabled) Log.debug(Util.TERM_PURPLE + "setLowMemory " + LOW_MEMORY_COUNT + ": " + Util.TERM_CLEAR + Util.tags(cause)); } final boolean first = (LOW_MEMORY_COUNT == 0); LOW_MEMORY_COUNT++; ProcessingPool.shrinkIfPossible(first); } // we do this out side of the sync just in case, as // below will obtain it's own sync //TaskQueue.flushCachingRequests(); // needs testing // TODO: either allow the above flushing, or skip the tasks when popped off queue and force // callbacks to listeners with an error. At this point, we're getting into issues of // coherency with ImageRep loading states, and it's getting so complex that this is the // point to stop and wait for a day when we can do a greenfield impl w/out all the // backward-compat Images API's, and just keeps ImageRef's themselves in the cache, // possibly even with their own prev/next task-queue pointers for fast jumping & shuffling. } public static int lowMemoryCount() { return LOW_MEMORY_COUNT; } public static boolean lowMemoryConditions() { return LOW_MEMORY_COUNT > 0; } public static VueAction ClearCacheAction = new VueAction("Empty Image Cache") { public void act() { RawCache.clear(); } }; /** * Calls to Images.getImage must pass in a Listener to get results. * The first argument to all the callbacks is the original object * passed in to getImage as the imageSRC. */ public interface Listener { /** If image is already cached, this will NOT be called -- is only called from an image loading thread. * If sourceSize is non-null, this is an icon, and that represents the full-size */ void gotImageSize(Object imageSrc, int w, int h, long byteSize, int[] sourceSize); /** If byte-tracking is enabled on the input source, this will be called periodically during loading */ void gotImageProgress(Object imageSrc, long bytesSoFar, float percentSoFar); /** Will be called immediately in same thread if image cached, later in different thread if not. */ //void gotImage(Object imageSrc, Image image, int w, int h); void gotImage(Object imageSrc, Handle handle); /** If there is an exception or problem loading the image, this will be called */ void gotImageError(Object imageSrc, String msg); // /** intended to ultimately replace all the below calls */ // void gotImageData(Object key, Progress p); } private static final Object LOAD_CACHE = "CACHE"; private static final Object LOAD_NORMAL = "PAINT"; private static final Object LOAD_IMMEDIATE = "PRINT"; /** * Fetch the given image with meta-data. If it's cached, listener.gotImage is called back immediately * in the current thread. If not, the image is fetched asynchronously, and the * callbacks are made later from a special image loading thread. * * @param imageSRC - anything that might be converted to an image: a Resource, File, URL, InputStream, etc. * * @param listener - the Images.Listener to callback. If null, the result * of the call would be only to ensure the given image is cached. * * @return the Image if immediately available, null if the listener will be called back. **/ public static Handle getImageHandle(Object imageSRC, Images.Listener listener) { return getImage(imageSRC, listener, LOAD_NORMAL); } public static Handle getImageImmediately(Object imageSRC, Images.Listener listener) { return getImage(imageSRC, listener, LOAD_IMMEDIATE); } public static Handle cacheImage(Object imageSRC, Images.Listener listener) { return getImage(imageSRC, listener, LOAD_CACHE); } public static Image getImage(Object imageSRC, Images.Listener listener) { return handleToImage(getImage(imageSRC, listener, LOAD_NORMAL)); } /** * Fetch the given image. If it's cached, listener.gotImage is called back immediately * in the current thread. If not, the image is fetched asynchronously, and the * callbacks are made later from a special image loading thread. * * @param imageSRC - anything that might be converted to an image: a Resource, File, URL, InputStream, etc. * * @param listener - the Images.Listener to callback. If null, the result * of the call would be only to ensure the given image is cached. * * @param immediate - if the requested content is not already loading, * it will be immediately loaded in the current thread. * * @return the Image if immediately available, null if the listener will be called back. **/ private static Handle getImage(Object imageSRC, Images.Listener listener, Object when) { try { return getCachedOrCallbackOnLoad(imageSRC, listener, when); } catch (Throwable t) { if (DEBUG.IMAGE) tufts.Util.printStackTrace(t); if (listener != null) listener.gotImageError(imageSRC, t.toString()); } return null; } // /** SYNCHRONOUSLY retrive an image from the given data source: e.g., a Resource, File, URL, InputStream, etc */ // public static Image getImage(Object imageData) // { // try { // return getCachedOrCallbackOnLoad(imageData, null); // } catch (Throwable t) { // Log.error("getImage " + imageData, t); // return null; // } // } public static void loadDiskCache() { File dir = getCacheDirectory(); if (dir == null) return; Log.debug("listing disk cache..."); File[] files = dir.listFiles(); Log.debug("listing disk cache: done; entries=" + files.length); synchronized (RawCache) { for (int i = 0; i < files.length; i++) { File file = files[i]; String name = file.getName(); if (name.charAt(0) == '.') continue; //out("found cache file " + file); URI key = null; try { key = cacheFileNameToKey(name); if (DEBUG.IMAGE && DEBUG.META) out("made cache key: " + key); if (key != null) { RawCache.put(key, new CacheEntry(file)); //RefCache.put(key, file); } } catch (Throwable t) { Log.error("failed to load cache with [" + name + "]; key=" + key); } } } } /** @return the cache file for the given resource, or null if none exists */ public static File findCacheFile(Resource r) { final ImageSource imageSRC = ImageSource.create(r); if (imageSRC.key != null) { final Object entry = RawCache.get(imageSRC.key); if (entry instanceof CacheEntry) { return ((CacheEntry)entry).file; } else { Log.warn("Cache is loading, no cache file yet for " + r); } } Log.warn("Failed to find cache file for " + r); return null; } // todo: really, ImageCacheEntry v.s. Loader cache entries, tho they don't // currently have a common super-class. private static class CacheEntry { private final Reference<Image> ref; private final Map<String,?> data; private final File file; // Loader loader; // todo: add loader here so we can always have CacheEntry's in the cache, and // so file is information always available, tho that could be extracted from the // Loader ImageSource? No -- that could be any file, not just a disk-cache entry. // This new complexity arising out of having icons in the cache -- we may just // want a separate icon cache. /** image should only be null for startup init with existing cache files */ CacheEntry(Image image, File cacheFile, Map<String,?> props) { if (image == null && cacheFile == null) throw new IllegalArgumentException("CacheEntry: at least one of image or file must be non null"); if (image != null) this.ref = new SoftReference(image); else this.ref = null; this.file = cacheFile; this.data = props; if (DEBUG.IMAGE) out("new " + this); } CacheEntry(Image image, File cacheFile) { this(image, cacheFile, Collections.EMPTY_MAP); } CacheEntry(Handle handle, File cacheFile) { this(handle.image, cacheFile, handle.data); } // for startup disk entries: note that props can't change (is final), so this // entry must be replaced once the image is loaded. CacheEntry(File cacheFile) { this(null, cacheFile, Collections.EMPTY_MAP); } boolean isPreloadedDiskEntry() { return ref == null; } Image getCachedImage() { // if don't even have a ref, this was for an init-time persistent cache file if (ref == null) return null; Image image = ref.get(); // will be null if was cleared if (image == null) { if (DEBUG.Enabled && file != null) out("GC'd: " + file); return null; } else return image; } Handle getHandle() { //return Handle.create(getCachedImage(), data); Image i = getCachedImage(); if (i != null) return new Handle(i, data); else return null; } File getFile() { return file; } void clear() { if (ref != null) ref.clear(); } public String toString() { return "CacheEntry[" + tag(getCachedImage()) + "; file=" + file + "]"; } } private static final CacheMap RawCache = new CacheMap(); //private static final NewCache RefCache = new NewCache(); //private static final NewCache<URI,ImageRef> RefCache = new NewCache(); // private static class NewCache { // private final Map<URI,ImageRef> map = new ConcurrentHashMap(); // // can ultimately refactor access code to make all transactions explicit // // so don't need to be synchronized here and can rely on ConcurrentHashMap // // for simple ops, but leaving synchronized for now as our existing // // cache transactions may depend on it. // synchronized ImageRef get(URI key) { // return map.get(key); // } // synchronized boolean containsKey(URI key) { // return map.containsKey(key); // } // synchronized ImageRef put(URI key, ImageRef value) { // return map.put(key, value); // } // synchronized ImageRef put(URI key, File file) { // //return map.put(key, value); // //return map.put(key, ImageRef.create(file)); // return null; // } // synchronized ImageRef remove(URI key) { // return map.remove(key); // } // synchronized void clear() { // throw new UnsupportedOperationException("unimplemented"); // } // } //=================================================================================================== // This would all be much simpler if the cache contained ImageRef's. They could contain // whatever reps are available, and be the place where the full size was always recorded // (possibly in the size of it's full-rep, left in an UNLOADED state, but with size // recorded. In that case, the _full rep may be able to be made final). The only // high-level extra complexity would be that ImageRef's would then need to keep a list of // listeners (LWImage's), which would also want to be flushed when LWImage's leave the // model (and re-add if they re-join the model). Also tho, w/out extra work, any update to // an ImageRef (e.g., full-rep-has-arrived), would trigger repaints to all LWImage's, even // if their happy displaying just an icon image. That's probably a good trade-off to make // tho -- it's not often that you have a map with the same image repeated anyway (well, may // presentations when slide icons are turned on), and it's extra repaints that will only // happen once -- when the full rep fully arrives. // // Also, either all existing API's would need to change to work w/ImageRef's, or we'd need // to keep things backward compat. That probably wouldn't be that much of an issue. // Okay, only two places use the image api: PreviewPane & ResourceIcon. And only PreviewPane // makes any use of the tracking the original update source (in case we've switched to a new // preview after results of an old preview finally arrived). // // The big change is that the cache would now contain entries for multiple reps (full + icons). // Maybe this isn't an issue at all tho? Currently, this lets full reps and icon reps // exist separately in the cache, and for them to be GC's separately. If the cache contained // ImageRef's, nobody would actually request icon reps -- ImageRef is the only thing that does // that. Normal requestors only request the original rep -- it's just sometimes they'll get // the icon rep instead. //=================================================================================================== /* * Not all HashMap methods covered: only safe to use * the ones explicity implemented here. */ private static class CacheMap extends HashMap { public synchronized Object get(Object key) { return super.get(key); } public synchronized boolean containsKey(Object key) { return super.containsKey(key); } public synchronized Object put(Object key, Object value) { return super.put(key, value); } public synchronized Object remove(Object key) { return super.remove(key); } // for now, only clears memory cache public synchronized void clear() { final Iterator i = values().iterator(); while (i.hasNext()) { Object entry = i.next(); // may be a Loader: todo: may want to kill thread if it is Especially: // if we go off line, Loaders created immediately after that (or during) // tend to hang forever. Loaders created once the OS knows we're // offline will usually fail immediately with "no route to host", but // even after going back online, and other images load, the originally // hung Loader's won't die... So at least an image-cache should kill // them. if (entry instanceof CacheEntry) { CacheEntry ce = (CacheEntry) entry; ce.clear(); if (ce.getFile() == null) i.remove(); } else { // Interrupt may not be good enough: if blocked on non-async IO // (non-channel IO, e.g., "regular"), this can have no // effect. Turns out using stop doesn't help even in this // case. if (entry instanceof LoadThread) { Log.info("STOPPING THREAD ENTRY " + entry); ((LoadThread)entry).stop(); } else { //if (entry instanceof Loader) { Log.warn("LEAVING TO RUN OUT TASK ENTRY " + entry); // if it was a Future, we could attempt to de-queue it } //((LoadThread)entry).stop(); //((Loader)entry).interrupt(); } } //super.clear(); } } /** * flush any cache BufferedImages we have for the give file: future requests * will force the image data to be reloaded from the file (useful if we know * the file has changed on disk). */ public static void flushCache(File file) { final URI key = makeKey(file); flushEntry(key, "image"); final URI iconKey = ImageSource.makeIconKey(key, 128); // TODO: sync icon key 128 size with size in ImageRef, or better yet, search // cache for all cache keys of any size (tho we only have one size for now) if (flushEntry(iconKey, "ic128")) { File iconFile = new File(getCacheDirectory(), keyToCacheFileName(iconKey)); Log.info("looking for cache file " + iconFile); if (iconFile.exists()) { Log.info(" deleting cache file " + iconFile); iconFile.delete(); } } } private static boolean flushEntry(Object key, String debug) { final Object entry = RawCache.remove(key); if (entry != null) { Log.info(Util.TERM_RED + "flushed cache " + debug + " entry: " + key + "; " + entry + Util.TERM_CLEAR); } else { Log.info("failed to find cache entry for key: " + Util.tags(key)); } return entry != null; } // private static URI makeKey(URL u) { // try { // if ("file".equals(u.getProtocol())) { // return Resource.makeURI(u); // } else { // return new URI(u.getProtocol(), // u.getUserInfo(), // u.getHost(), // u.getPort(), // //u.getAuthority(), // u.getPath(), // u.getQuery(), // u.getRef()).normalize(); // } // } catch (Throwable t) { // Util.printStackTrace(t, "can't make URI cache key from URL " + u); // } // return null; // } static URI makeKey(File file) { try { return file.toURI().normalize(); } catch (Throwable t) { Util.printStackTrace(t, "can't make URI cache key from file " + file); } return null; } static String keyToCacheFileName(URI key) //throws java.io.UnsupportedEncodingException { try { //return key.toASCIIString(); return java.net.URLEncoder.encode(key.toString(), "UTF-8"); } catch (UnsupportedEncodingException e) { Log.error("transforming key to cache-file name: " + key, e); } return null; } private static URI cacheFileNameToKey(String name) { try { return new URI(java.net.URLDecoder.decode(name, "UTF-8")); //return new URL(java.net.URLDecoder.decode(name, "UTF-8")); } catch (Throwable t) { if (DEBUG.Enabled) tufts.Util.printStackTrace(t); return null; } } /** * Using a relay system, as opposed to say a list of listeners maintained by the * Loader, allows the image loading code to not care if there is a single listener * or multiple listeners, which is handy in the case where the result is cached and * we don't even create a loader: we just callback the listener immediately in the * same thread. But when a Loader is created, it can create ListenerRelay's to * relay results down the chain, starting with it's special LoaderRelayer to relay * partial results to listeners added in the middle of an image fetch, and again, * the image loading code doesn't need to know about this: it just has a single * listener. * * Performance-wise, there is rarely ever more than a single relayer object * created (covering two listeners for the same image load). * * This is also a handy place for diagnostics. */ private static class ListenerRelay implements Listener { protected final Listener head; protected Listener tail; ListenerRelay(Listener l0, Listener l1) { // if head is null, nobody is listening at the start, but listeners may be added later to tail this.head = l0; this.tail = l1; } ListenerRelay(Listener l0) { this(l0, null); } // public void gotImageUpdate(Object key, Progress p) { // if (head != null) relayUpdate(head, key, p); // if (tail != null) relayUpdate(tail, key, p); // } // protected final void relayUpdate(Listener l, Object key, Progress p) { // try { // if (DEBUG.IMAGE && l instanceof ListenerRelay == false) // out("relay UPDATE of " + key + " to " + tag(l)); // l.gotImageUpdate(key, p); // } catch (Throwable t) { // Log.error("relaying UPDATE " + key + " to " + Util.tags(l), t); // } // } public void gotImageSize(Object src, int w, int h, long bytes, int[] sourceSize) { relaySize(head, src, w, h, bytes, sourceSize); relaySize(tail, src, w, h, bytes, sourceSize); } public void gotImageProgress(Object src, long bytes, float pct) { relayProgress(head, src, bytes, pct); relayProgress(tail, src, bytes, pct); } public void gotImage(Object src, Handle handle) { relayImage(head, src, handle); relayImage(tail, src, handle); } public void gotImageError(Object src, String msg) { relayError(head, src, msg); relayError(tail, src, msg); } protected final void relaySize(Listener l, Object src, int w, int h, long bytes, int[] sourceSize) { if (l != null) { try { if (DEBUG.IMAGE && l instanceof ListenerRelay == false) out(Util.TERM_CYAN + "relay SIZE " + w + "x" + h + " ss=" + Util.tags(sourceSize) + " to " + tag(l) + Util.TERM_CLEAR); l.gotImageSize(src, w, h, bytes, sourceSize); } catch (Throwable t) { Log.error("relaying size to " + Util.tags(l), t); } } } protected final void relayProgress(Listener l, Object src, long bytes, float pct) { if (l != null) { try { if (DEBUG.IMAGE && DEBUG.META && l instanceof ListenerRelay == false) out(String.format("relay PROGRESS %.2f %5d to %s", pct, bytes, tag(l))); l.gotImageProgress(src, bytes, pct); } catch (Throwable t) { Log.error("relaying progress to " + Util.tags(l), t); } } } protected final void relayImage(Listener l, Object src, Handle handle) { if (l != null) { try { if (DEBUG.IMAGE && l instanceof ListenerRelay == false) out(Util.TERM_CYAN + "relay IMAGE to " + tag(l) + Util.TERM_CLEAR); l.gotImage(src, handle); } catch (Throwable t) { Log.error("relaying image to " + Util.tags(l), t); } } } protected final void relayError(Listener l, Object src, String msg) { if (l != null) { try { if (DEBUG.IMAGE && l instanceof ListenerRelay == false) out("relay ERROR to " + tag(head) + "; is=" + src); l.gotImageError(src, msg); } catch (Throwable t) { Log.error("relaying error to " + Util.tags(l), t); } } } boolean hasListener(Listener l) { if (head == l || tail == l) return true; else if (tail instanceof ListenerRelay) return ((ListenerRelay)tail).hasListener(l); else return false; } protected boolean addListener(Listener newListener) { synchronized (this) { if (hasListener(newListener)) { if (DEBUG.IMAGE) out(getClass().getSimpleName() + " redundant listener: " + tag(newListener)); return false; } if (tail == null) { this.tail = newListener; } else { this.tail = new ListenerRelay(tail, newListener); } return true; } // Note: WE CAN DEADLOCK if deliverPartialResults is in the sync. E.g. -- // trying moving around raw images as they're loading / icon generating. // TODO: this really should be in the sync, as well as all the above // Listener API methods -- normally we'll be in the AWT thread, and if a // loader modifies the partial results while this call is being made, they // may be incoherent, and not all partial data may delivered. The lock is // inherently dangerous tho, in that anything may generally happen during // client code callbacks, including calls back into this API. //deliverPartialResults(newListener); } } /** * Track what's been delivered, to send to listeners that are added when * partial results have already been delivered. */ // note: would be simpler to have a single ImageResult/ImageProgress object that // slowly accumulates it's results, and is reported to a single gotImageData call, // with an added event type argument (size/progress/image/icon/error) An ImageRep // might be tempting to serve this purpose, but that doesn't really make sense: the // ImageRep doesn't need to know things like byteSize or bytesSoFar. This Progress // class is an an attempt to that, tho we'd need to change all the Listener API // impls to make use of it -- cleanup for another day. /*public*/ static class Progress { // public static final String SIZE = "IMG_SIZE"; // public static final String SOURCE_SIZE = "IMG_SOURCE_SIZE"; // public static final String PROGRESS = "IMG_BYTE_PROGRESS"; // public static final String IMAGE = "IMG_RENDERABLE"; // public static final String ERROR = "IMG_ERROR"; protected final ImageSource imageSRC; protected Handle handle; protected int width = -1; protected int height = -1; protected int[] sourceSize; protected long byteSize; protected long bytesSoFar; protected String errorMsg; private Progress(ImageSource is) { imageSRC = is; } public ImageSource source() { return imageSRC; } public Image image() { return handle == null ? null : handle.image; } public int width() { return width; } public int height() { return height; } public long byteSize() { return byteSize; } public long bytesSoFar() { return bytesSoFar; } public String errorMessage() { return errorMsg; } } private static final class CachingRelayer extends Progress implements Listener { final ListenerRelay chain; CachingRelayer(ImageSource is, Listener firstListener) { //super(firstListener, null); super(is); chain = new ListenerRelay(firstListener); } // public void gotImageData(Object key, Progress p) { // chain.gotImageUpdate(key, p); // } public synchronized void gotImageSize(Object imageSrc, int w, int h, long byteSize, int[] sourceSize) { this.width = w; this.height = h; this.byteSize = byteSize; if (sourceSize != null) this.sourceSize = sourceSize; chain.gotImageSize(imageSrc, w, h, byteSize, sourceSize); } public synchronized void gotImageProgress(Object imageSrc, long bytesSoFar, float _p) { this.bytesSoFar = bytesSoFar; // incoming percent is empty -- we fill it here // Note that the underlying raw stream may send us byte progress reports before // the image size is known. This is a bug -- the byteSize is known // before this and should be recored here. The # of bytes read before // the image size is known is usually negligable tho. For now we just don't report // the progress until we have a byteSize. if (byteSize > 0) chain.gotImageProgress(imageSrc, bytesSoFar, percentProgress()); } public synchronized void gotImage(Object imageSrc, Handle handle) { //this.image = image; this.handle = handle; chain.gotImage(imageSrc, handle); } public synchronized void gotImageError(Object imageSrc, String msg) { this.errorMsg = msg; chain.gotImageError(imageSrc, msg); } private float percentProgress() { // // note: bytes can arrive from the raw stream before the im // final float x = (float) bytesSoFar / (float) byteSize; // Log.debug("bytesSoFar " + bytesSoFar + " / bytes " + byteSize + " = " + x); if (byteSize > 0) return (float) bytesSoFar / (float) byteSize; else return 0; } void addListener(Listener newListener) { if (chain.addListener(newListener)) deliverPartialResults(newListener); } void flushForGC() { //chain.imageSRC = null; // is now final //image = null; // this is the most important // chain = null; // is final handle = null; } /** * Deliver any results we've already got. It's possible for this to happen * even after we have all our results, if the image completed between the * time we found the Loader in the cache, and the time the requestor was * added as a listener. */ private void deliverPartialResults(Listener l) { if (DEBUG.IMAGE) out("DELIVERING PARTIAL RESULTS TO: " + tag(l)); if (width > 0) chain.relaySize(l, imageSRC.original, width, height, byteSize, sourceSize); if (bytesSoFar > 0) chain.relayProgress(l, imageSRC.original, bytesSoFar, percentProgress()); if (handle != null) chain.relayImage(l, imageSRC.original, handle); if (errorMsg != null) { if (handle != null) Log.warn("had both image and error: " + errorMsg + "; for " + l); chain.relayError(l, imageSRC.original, errorMsg); } if (DEBUG.IMAGE) out(" DELIVERED PARTIAL RESULTS TO: " + tag(l)); } } // private static Image getCachedOrCallbackOnLoad(Object imageSource, Images.Listener listener) // throws java.io.IOException, java.lang.InterruptedException // { // return getCachedOrCallbackOnLoad(imageSource, listener, false); // } /** * @return Image if cached or listener is null, otherwise makes callbacks to the listener from * a new thread. Note: specifying both a listener, and immediate=true is untested. */ private static Handle getCachedOrCallbackOnLoad (final Object imageSource, final Images.Listener listener, final Object when) throws java.io.IOException, java.lang.InterruptedException { if (imageSource instanceof Image) { if (DEBUG.Enabled) Log.info("image source was an instance of Image", new Throwable(Util.tags(imageSource))); final Image image = (Image) imageSource; final int w, h; if (image instanceof BufferedImage) { BufferedImage bi = (BufferedImage) imageSource; w = bi.getWidth(); h = bi.getHeight(); } else { w = image.getWidth(null); h = image.getHeight(null); } final Handle handle = new Handle(image); if (listener != null) listener.gotImage(imageSource, // same as image handle); return handle; } final ImageSource imageSRC = ImageSource.create(imageSource); //if (DEBUG.IMAGE) System.out.println("-------------------------------------------------------"); //Log.debug("fetching image source " + imageSRC + " for " + tag(listener)); if (DEBUG.IMAGE||DEBUG.WORK) Log.debug("fetching for " + Util.tags(when) + " to listener " + Util.tag(listener) + "; " + imageSRC); final Object cacheEntry = getCachedOrKickLoad(imageSRC, listener, when); // Possibly: if nothing was found in the cache and an ICON for the // given source exists in the cache, return that. Either that // or change all Images callers to request an ImageRef, which // will handle that for us. if (cacheEntry == LOADER_JUST_STARTED) { // if this is the START of a new load, the caller has already been attached // as a listener, and the results will be coming via callback. return null; } Handle handle = null; if (cacheEntry instanceof Loader) { // Another request has already put a Loader into the cache -- have // the listener observe the existing loader. final Loader loader = (Loader) cacheEntry; final Image loaderImage = loader.getLoaderImage(); if (loaderImage != null) { // This can happen if a loader has completed, but hasn't left the cache yet. We // can just return the image immediately w/out delivering partial-result callbacks // (or in this case, it would be full-result callbacks). We're ignoring any sync // issues on the call to loader.getImage(), because even if we see null when it's // really there, the results will still be delivered by our fully synced partial // results delivery, and we don't need any more sync issues to test. return new Handle(loaderImage); } if (when == LOAD_NORMAL) loader.raisePriority(); // TODO: if this is a "regular priority" request, and we find a loader in the cache // that has NOT already been started, then the task queue(s) may need modification. // If the existing task is in the caching queue, it needs to be moved (to the // front) of the regular loading queue. If the existing task is in the loading // queue, it needs to be moved (LIFO style) to the front of the loading queue. if (listener != null) { //if (DEBUG.IMAGE) out("Adding us as listener to existing Loader"); loader.addListener(listener); if (when != LOAD_IMMEDIATE) { // normal case: called has been added as a listener to an existing // load -- just return. return null; } } // We had no listener, so we can only run synchronous & wait on existing loader thread // to die: We can't have a cache-lock when we do this. // Note: this can be dangerous -- multiple non-listener requests for the same // loading content will quickly have all image processing threads hung waiting on // the same load -- do we still need this? Generally speaking, image requests // w/out listeners are not safe. For now, we'll only allow this if this was an // immediate requrest. // Note: our print code (ImageRef rendering in a print-quality DrawContext) makes // use of non-listener requests regularly to ensure full-resolution image data is // loaded before rendering, but each request is going to come in via a single // thread, so we shouldn't have to worry about contention issues -- however, // multiple print jobs running at the same time in different threads could cause a // problem. Actually, no -- if an image ALREADY has an active loader, and then we // get a print request, we will need to take advantage of the join below. if (when != LOAD_IMMEDIATE) { // No listener, no cache entry, no result. Caller simply // gets null and doesn't know why, even tho the image // may now be loading. Log.warn("no listener for non-cached content: caller in the dark as to the presence of image content: " + imageSRC); return null; } Log.info("Joining " + tag(loader) + "..."); loader.join(); Log.info("Join of " + tag(loader) + " completed, cache has filled."); // Note we get here only in one rare case: there was an entry in the // cache that was already loading on another thread, and somebody new // requested the image that did NOT have a listener, so we joined the // existing thread and waited for it to finish (with no listener, we // have to run synchronous). // So now that we've waited, we should be guaranteed to have a full // Image result in the cache at this point. // Note: theoretically, the GC could have cleared our SoftReference // betwen loading the cache and now. We can't lock the cache while // we're waiting on the join tho. // cachedImage = ((CacheEntry)RawCache.get(imageSRC.key)).getCachedImage(); // if (cachedImage == null) // Log.warn("Zealous GC: image tossed immediately " + imageSRC); handle = ((CacheEntry)RawCache.get(imageSRC.key)).getHandle(); if (handle == null) Log.warn("Zealous GC: image tossed immediately " + imageSRC); } else if (cacheEntry instanceof Handle) { handle = (Handle) cacheEntry; } else if (cacheEntry instanceof Image) { Log.warn("DEPRECATED USE OF CACHE ENTRY -- USE HANDLE; " + imageSRC, new Throwable("HERE")); //cachedImage = (Image) cacheEntry; handle = new Handle((Image) cacheEntry); } else { if (DEBUG.IMAGE) Log.warn("unhandled fetch result: " + Util.tags(cacheEntry), new Throwable("HERE")); else Log.warn("unhandled fetch result: " + Util.tags(cacheEntry)); // if we have no listener, it's reasonable for the getCachedOrKickLoad to return null, // tho wouldn't it be better just to return one anyway, and allow quiet async caching? // later requests could still hook up to the loader } if (handle != null && listener != null) { // THE IMAGE WAS IN THE CACHE: immediately callback the listener with the result // if (handle.data.size() > 0) // listener.gotImageUpdate(imageSRC.original, handle.data); listener.gotImage(imageSRC.original, handle); } // if (cachedImage != null) { // if (listener != null) { // // THE IMAGE WAS IN THE CACHE: immediately callback the listener with the result // listener.gotImage(imageSRC.original, // cachedImage, // null); // } // //return cachedImage; // } return handle; //return cachedImage; // [no longer needed: request can be immediate] // // We had no image, and no Loader was started: this should // // only happen if there was no listener for the Loader, tho we // // allow the sync load to go ahead just in case. (Could get // // here due to an over-zealous GC). // if (listener != null) // Log.warn("had a listener, but no Loader created: backup synchronous loading for " + imageSRC); // if (DEBUG.IMAGE) out("synchronous load of " + imageSRC); // // load the image and don't return until we have it // return loadImageAndCache(imageSRC, listener); } private static final Object LOADER_JUST_STARTED = "<image-loader-created>"; /** * Although ThreadPoolExecutors have an API to allow resizing, the resize does not * appear to take effect until all tasks are completed and the pool has has run to * idle. This class wraps an ExecutorService (a ThreadPoolExecutor), and allows it * to be immediately shut down and all of it's tasks transferred to a new, smaller * pool at any time. This needs to happen ASAP if we start getting * OutOfMemoryError's. */ private static class ImmediatelyReducablePool implements Runnable { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(ImmediatelyReducablePool.class); private Thread _shutdownThread; private ThreadPoolExecutor _pool; private ExecutorService _poolInShutdown; //private List<Runnable> _deferredTasks; private int _nextSmallerPoolSize; ImmediatelyReducablePool(int startSize) { if (startSize < 1) startSize = 1; _pool = createThreadPool(startSize); if (startSize > 1) { //_nextSmallerPoolSize = startSize / 2; // for now, any EOM will ramp us straight down to a single processing thread _nextSmallerPoolSize = 1; } else { _nextSmallerPoolSize = 0; } } public void run() { Log.info("reducer started"); ExecutorService oldPool = null; for (;;) { synchronized (this) { if (oldPool == _poolInShutdown) Log.error("should never happen: re-waiting on the same pool " + oldPool); oldPool = _poolInShutdown; } //--------------------------------- waitForTermination(oldPool); //--------------------------------- synchronized (this) { _poolInShutdown = null; createAndLoadNewPool(_nextSmallerPoolSize); Log.info(Util.TERM_YELLOW + "pool resized to " + _nextSmallerPoolSize + Util.TERM_CLEAR); if (_nextSmallerPoolSize <= 1) { _nextSmallerPoolSize = 0; _shutdownThread = null; // allow GC //_deferredTasks = null; // allow GC break; // no more resizes possible } else { reduceNextPoolSize(); Log.info("next pool size: " + _nextSmallerPoolSize); try { Log.info("resizer sleeping..."); wait(); Log.info("resizer woke"); } catch (InterruptedException e) { Log.error("interrupted " + this, e); } } } } Log.info("resizer terminating, pool at size = 1, no further shrinkages possible"); } private void reduceNextPoolSize() { if (_nextSmallerPoolSize > 1) _nextSmallerPoolSize /= 2; else _nextSmallerPoolSize = 1; } private synchronized void forkAndWaitForTermination(ExecutorService pool) { _poolInShutdown = pool; if (_shutdownThread == null) { // if we never run out of memory, we'll never need to start this thread _shutdownThread = new Thread(this, "PoolReducer"); _shutdownThread.start(); } else { notify(); } } private void waitForTermination(final ExecutorService poolInShutdown) { Log.info("awaitTermination..."); try { if (poolInShutdown.awaitTermination(60L, TimeUnit.SECONDS)) { Log.info("pool terminated gracefully: " + poolInShutdown); } else { Log.warn("pool shutdown timed out: " + poolInShutdown); } } catch (InterruptedException e) { Log.error("awaiting termination", e); } } private synchronized void createAndLoadNewPool(int size) { if (size < 1) { Log.error("createAndLoadNewPool, bad size " + size); size = 1; } _pool = createThreadPool(size); // loadTasks(_deferredTasks); // _deferredTasks = null; } private synchronized void loadTasks(Collection<Runnable> tasks) { Log.info("RESUBMIT: " + Util.tags(tasks)); for (Runnable r : tasks) { //_pool.submit(r); // java 1.6 impl _pool.execute(r); // java 1.5 impl } } private synchronized void addTask(Task r) { if (_pool == null) { Log.info("deferred " + r); //_deferredTasks.add(r); TaskQueue.queueTask(r); } else { // _pool.submit(r); // java 1.6 impl -- (note: creates FutureTask's internally) _pool.execute(r); // java 1.5 impl } } /** re-load the queue as the priority mechanism has changed -- any IconTasks need to be sorted to the front */ private synchronized void resortQueue() { Log.info("skipping queue resort: self-managed"); // final BlockingQueue q = _pool.getQueue(); // if (q.size() > 1) { // Log.info("resorting queue " + Util.tags(q)); // final Collection<Runnable> qlist = new ArrayList(q.size()); // q.drainTo(qlist); // loadTasks(qlist); // } else { // Log.info("queue doesn't need re-sorting: " + Util.tags(q)); // } } public synchronized void shrinkIfPossible(boolean firstLowMemory) { if (_nextSmallerPoolSize < 1) { if (DELAYED_ICONS && firstLowMemory) resortQueue(); return; } if (_pool != null) { // if EOM's stack up, we may try and shrink while // already waiting for a shrink, and _pool will be null // // We no longer need to drain the queue, as we self-manage it now // final List<Runnable> dequeued; // if (false) { // dequeued = new ArrayList(); // TaskQueue.drainTo(dequeued); // Log.debug("shutdown..."); // _pool.shutdown(); // } else { // Log.debug("shutdownNow..."); // dequeued = _pool.shutdownNow(); // } // Log.info("back from shutdown, drained=" + Util.tags(dequeued)); // if (DEBUG.Enabled) Util.dump(dequeued); // if (_deferredTasks == null) { // _deferredTasks = new ArrayList(dequeued); // } else { // _deferredTasks.addAll(dequeued); // } Log.debug("shutdownNow..."); _pool.shutdownNow(); final ExecutorService oldPool = _pool; _pool = null; forkAndWaitForTermination(oldPool); // note: will not need to re-sort queue: is automatically resorted when // _deferredTasks is re-loaded } else { reduceNextPoolSize(); if (DELAYED_ICONS && firstLowMemory) resortQueue(); Log.warn("stacked OutOfMemoryError conditions: next pool size reduced to " + _nextSmallerPoolSize); } } } private static final Runnable NOOP = new Runnable() { volatile long count = 0; public void run() { if (DEBUG.Enabled) { Log.debug(Util.TERM_PURPLE + "NOOP RUNS " + count + Util.TERM_CLEAR); count++; } } @Override public String toString() { return "NOOP"; } }; private static final String PRI_HIGH = "HIGH"; private static final String PRI_NORM = "NORM"; private static final String PRI_LOW = "LAST"; // ThreadPoolExectuor impl requires BlockingQueue methods: // offer, remove, isEmpty, poll, poll(time,timeUnit), take, drainTo(Collection), iterator, // remainingCapacity, size // it does NOT need (so, it needs just about everything) // add, contains // Primarily, we need to override TAKE/poll and OFFER to feed ThreadPoolExectuor properly private static final class TaskQueue<E> extends SynchronousQueue<E> { // An implementaiton that maximally adressed performance would record the canvas object // drawn to (e.g., MapViewer object: the full-screen v.s. standard map instances, etc) for // each desired representation, and an API call for the application to report the current // priority canvas (e.g., when a MapViewer gets application focus). Or perhaps an API call // to report when the focal has changed or narrowed, so we should only then pay attention // re-prioritizing due to repeated paint requests. In any case, Within the priority canvas // items, the most recently desired reps would take priority (e.g., the most recently // requested content to be drawn to the full-screen viewer when in presentation mode has // priority). As ImageRep's won't re-poll the cache (call getImage) if their alreadly // loading (and that would be messy to enforce) this would require coordination with the // ImageRef's desired reps each time they're drawn. This would probably be most simply // done by changing ImageRef's to singleton instances that are stored in the cache. All // that is to prevent excess queue thrashing (near full rotations on each paint), tho in // practice that would only actually happen when there are lots of high-res full-scale // images trying to draw at once, which is fundamentally limited by the pixel resoution of // the screen. // For icon tasks, runs as FIFO (would be better as a JDK1.6 ArrayDeque, as this never needs shuffling) private final LinkedList<E> q1 = new LinkedList(); // For normal priorities, runs as LIFO, and requires re-shuffling: private final LinkedList<E> q2 = new LinkedList(); // For low priorities, runs as FIFO (also better as ArrayDeque) private final LinkedList<E> q3 = new LinkedList(); // The total queue can be seen as three regions arranged linearly in order of priority, // left to right, to be fed to the TPE (ThreadPoolExecutor): // // TPE <- [FIFO priorities (icon generation)] <= [LIFO paint requests] <= [FIFO cache requests] private static void debug(String s) { Log.info(Util.TERM_GREEN + s + Util.TERM_CLEAR); } private void dump() { Log.debug("TaskQueue:"); Util.dump(q1); Util.dump(q2); Util.dump(q3); } // Actually, we leave this as the SynchronousQueue default, which is to always report as empty. // @Override public synchronized boolean isEmpty() { // return q0.size() + q1.size() + q2.size() < 1; // } @Override public E take() throws InterruptedException { E o = popNext(); if (o != null) { if (DEBUG.IMAGE) debug("take provides " + o); return o; } else { // There are currently no outstanding tasks. if (DEBUG.IMAGE) debug("take is waiting on offer..."); // SynchronousQueue super.take() will put this thread to sleep, and it won't wake // until another thread makes a call to super.offer(Runnable). The codepath for // that call is via ThreadPoolExecutor.execute(Runnable), which will call into our // BlockingQueue to offer new tasks. We don't just call offer ourselves when a new // image request comes in -- we want the TPE to do that. return super.take(); } } @Override public boolean offer(E e) { // offer(E) This is called by ThreadPoolExecutor in execute() -- we run through that so // we can take advantage of it's thread startup/restart code. // All we do is add it to our self-managed queue -- we don't immediately feed it to // the TPE, as we may have other more important tasks outstanding. queueTask(e); // Ideally, this would offer up a PEEK, and if accepted, THEN pop the peek. We always // want to return true in any case -- we're unbounded. queueTask could also return the task // queued in the special case that it's the only item in the queue, which is a common condition. // Doing it that way would at least leave the total real queue-size check inside the same sync. // CRUCIAL: super.offer(E) is what signals the blocking run thread in // ThreadPoolExecutor to wake and look for more tasks. We do not transfer a task to be // executed -- it's just a wake-up signal to call us back for another take(). The NOOP // task we hand it will in fact run, just quickly and with no result. /*final boolean wasAccepted = */ super.offer((E)NOOP); // Note that we DO NOT want to use put() or some other superclass impl that would let // us block here until the offer succeeds, as by the time the TPE is ready for another // take(), our self-managed queue may well have something more important to offer up. return true; } @Override public E poll() { // This will be called by SyncQueue to DRAIN us if the TPE is shutting down. // We never want or need to be drained, so we always return null. debug(getClass() + ".poll() returning null"); return null; // final E o = getNext(); // /*if (DEBUG.IMAGE)*/ debug("local poll " + o); // if (o != null) { // return o; // } else { // /*if (DEBUG.IMAGE)*/ debug("super poll " + o); // return super.poll(); // } } private void queueTask(E task) { if (task instanceof Task) { // typing in this class is currently a bit hacked-up final Object pri = ((Task)task).getPriority(); if (DEBUG.IMAGE) debug("QUEUE(" + pri + "): " + task); queueTaskAtPriority(task, pri); } else { Log.error("cannot queue: " + Util.tags(task)); } } synchronized void queueTaskAtPriority(E task, Object pri) { if (pri == PRI_HIGH) { q1.addLast(task); } else if (pri == PRI_NORM) { q2.addFirst(task); // push front for LIFO } else { //if (pri == PRI_LOW) q3.addLast(task); // If this is an ICON we're wanting to pre-cache, they should take // priority over any full-reps. Right now, we don't have a bit for // that, but could hack it in by recognizing the "i128.png" extension. // To handle this properly, there should be a another FIFO queue // inserted before the current LAST priorities (pre-caching of full-reps) // for the pre-caching of the ICON reps. // We could just do a q3.addFirst if this is an icon-key, tho that would // change those to LIFO order. Note that this is ONLY important when // opening multiple maps at once tho, which users pretty much are never // going to do (can only do that from the command line). Actually, no // this could happen if there are a bunch of images on a slide that are // not on a map, so they never get that first paint request that will // request the icon. Not common, but it could happen. In these funny // cases, if we don't handle this, the full-rep could be cached first. // It really just means that the full-rep will paint in place of the // icon rep until it loads. Chances are, pre-caching will have loaded // both anyway by the time users would see any of this tho. } // typing in this class is currently a bit hacked-up: ((Task)task).setPriority(pri); } /** @return the next highest priority processing task */ private synchronized E popNext() { // Ideally, we would mark Tasks as RUNNING right here -- as soon as it's de-queued. if (q1.size() > 0) return q1.removeFirst(); // pop front (was inserted FIFO) else if (q2.size() > 0) return q2.removeFirst(); // pop front (was inserted LIFO) else return q3.poll(); // pop front (was inserted FIFO), but return null if empty } void raiseTaskPriority(Task task) { //debug("RAISE-PRIORITY: " + task); final Object pri = task.getPriority(); if (pri == PRI_LOW) { jumpQueue(q3, task, PRI_NORM); } else if (pri == PRI_NORM) { jumpQueue(q2, task, PRI_NORM); } } // this needs testing: could be dangerous: even tho we're only // flushing caching requests, the Loader's can still be in // the RawCache, and we never want the possibility of a task // being left orphaned there, as future requests for that image // will think it's queued to be loaded, when that will never // happen. // synchronized void flushCachingRequests() { // // note: these task will still be in the RawCache, so // // they may later be found there and attempt to be re-prioritized... // if (q3.size() > 0) { // if (DEBUG.Enabled) Log.debug("flusing caching queue " + Util.tags(q3)); // q3.clear(); // } // } synchronized void jumpQueue(List src, Task task, Object newPri) { if (DEBUG.Enabled) { if (DEBUG.IMAGE) dump(); debug("jumpQueue to " + newPri + " for: " + task); } if (!src.remove(task)) { //if (!lowMemoryConditions() || task.isRunning()) { // really: is running, or has run if (true) { debug("queue did not contain: " + task + "\n\t was not in queue: " + Util.tags(src) + "\n\trequested new priority: " + newPri //+ "\n\tThis task will not be re-queued." ); if (DEBUG.Enabled) dump(); //return; // allow re-queue for now: worst case, it re-runs } // if we're low-memory, this can happen as a result // of flushing cache requests -- we allow the re-queuing //if (DEBUG.Enabled) debug("re-queue loMem orphan: " + task); } queueTaskAtPriority((E)task, newPri); if (DEBUG.IMAGE) dump(); } // Ideally, these caching tasks would only ever consume a single thread (low CPU // usage, especially during a presentation), and if we transition to a // low-memory state, all outstanding pre-cache requests should be flushed. // We should at least be able to demote outstanding pre-caches if the content // is no longer needed -- e.g., you're fast-paging through a presentation in // low-memory conditions, and you only need previews until you settle on where // you want to be. The way to handle this is actually to promote more // recent duplicate requests. } private static final TaskQueue<Runnable> TaskQueue = new TaskQueue(); private static final int ImageThreadPriority; private static final ThreadFactory ImageThreadFactory = new ThreadFactory() { private int count = 0; public Thread newThread(Runnable r) { final Thread it = new Thread(r); try { it.setPriority(ImageThreadPriority); } catch (Throwable t) { Log.warn("failed to set image thread priority to " + ImageThreadPriority, t); } count++; if (DEBUG.Enabled) it.setName("IPX-" + count); else it.setName("imageProcessor-" + count); if (DEBUG.Enabled) Log.debug("created thread: " + it); return it; } }; private static final ImmediatelyReducablePool ProcessingPool; static { final int cores = Runtime.getRuntime().availableProcessors() - 1; final int useCores; if (DEBUG.SINGLE_THREAD) { if (cores == 1) useCores = 2; else useCores = 1; } else { useCores = cores; } // rough test: on a 2-core laptop, our use-case came in at 1min v.s. 1:30min w/all cores in use // (all icons being generated) int priority = 1; // should be lowest try { priority = Thread.MIN_PRIORITY + Thread.NORM_PRIORITY / 2; // in case NORM_PRIORITY access fails -- there was some case of this happening in Applets? } catch (Throwable t) {} ImageThreadPriority = priority; // for thread factory ProcessingPool = new ImmediatelyReducablePool(useCores); } private static ThreadPoolExecutor createThreadPool(int nThreads) { final ThreadPoolExecutor pool; pool = new PriorityThreadPool(nThreads); Log.info("created thread pool: " + Util.tags(pool) + "; maxSize=" + pool.getMaximumPoolSize()); return pool; } private static final class PriorityThreadPool extends ThreadPoolExecutor { private int lowMemoryRepaints; PriorityThreadPool(int nThreads) { super(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, (BlockingQueue<Runnable>) TaskQueue, ImageThreadFactory); } // /** This method was added to AbstractExecutorService in Java 1.6 -- is not available in java 1.5 */ // @Override protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { // if (runnable instanceof RunnableFuture) { // if (DEBUG.Enabled) Log.debug("resubmit " + runnable); // return (RunnableFuture) runnable; // for re-submits // } else { // if (DEBUG.IMAGE) Log.debug("newTaskFor runnable " + Util.tags(runnable)); // return new PriorityTask((Loader)runnable); // } // } // /** This method was added to AbstractExecutorService in Java 1.6 -- is not available in java 1.5 */ // @Override protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { // throw new UnsupportedOperationException("newTaskFor Callable; " + callable); // } @Override public Future<?> submit(Runnable task) { throw new Error("use execute; " + task); } @Override public <T> Future<T> submit(Runnable task, T result) { throw new Error("use execute; " + task); } @Override public <T> Future<T> submit(Callable<T> task) { throw new Error("use execute; " + task); } // private boolean first = true; // @Override public void execute(Runnable runnable) { // if (runnable instanceof Loader) { // if (DEBUG.Enabled) Log.debug("new-task " + runnable); // if (first) { // //super.execute(new PriorityTask((Loader)runnable)); // super.execute(runnable); // first = false; // } else { // //TaskQueue.addFirst(new PriorityTask((Loader)runnable)); // TaskQueue.addFirst(runnable); // // if caching task, do addLast... // } // } else { // if (DEBUG.Enabled) Log.debug("resubmit " + runnable); // if (first) { // super.execute(runnable); // //first = false; // } else { // TaskQueue.addFirst(runnable); // } // } // } @Override public void execute(Runnable runnable) { if (DEBUG.IMAGE||DEBUG.WORK) Log.debug("submit " + runnable); super.execute(runnable); // if (runnable instanceof Loader) { // if (DEBUG.Enabled) Log.debug("new-task " + runnable); // super.execute(new PriorityTask((Loader)runnable)); // } else { // if (DEBUG.Enabled) Log.debug("resubmit " + runnable); // super.execute(runnable); // } } @Override protected void afterExecute(Runnable r, Throwable t) { if (r == NOOP) return; // note: don't toString the task -- members flushed for GC, so you'll get NPE //if (DEBUG.IMAGE) Log.debug("AFTER-EXECUTE " + Util.tags(r) + "; ex=" + t); // Only allow a few of these, otherwise we can get continuous looping failures if // memory becomes full enough. This is mainly needed for recovery from the first // low-memory failure. //if (DEBUG.Enabled) Util.dump(Cache); if (lowMemoryRepaints < 3 && lowMemoryConditions() && !isShutdown() && getQueue().isEmpty()) { java.awt.Component v = VUE.getActiveViewer(); if (v != null) { Log.info("LOW-MEMORY-REPAINT " + lowMemoryRepaints); v.repaint(); lowMemoryRepaints++; } } } } // what was the problem with making the Loader a FutureTask itself? static abstract class Loader implements Runnable { CachingRelayer relay; ImageSource imageSRC; volatile boolean started; Loader(ImageSource _imageSRC, Listener firstRelay) { imageSRC = _imageSRC; relay = new CachingRelayer(imageSRC, firstRelay); } void setPriority(Object key) {} Object getPriority() { return "-n/a"; } void raisePriority() {} final Image getLoaderImage() { return relay.image(); } public void join() throws InterruptedException { if (DEBUG.Enabled) Log.debug(this + " has been joined..."); Thread.currentThread().join(); } public final void run() { started = true; try { runToResult(); } catch (OutOfMemoryError eom) { setLowMemory(eom); Log.error("Uncaught EOM running " + this, eom); } catch (Throwable t) { Log.error("Uncaught Exception running " + this, t); } } // Mark as having been completed / potentially assist garbage collector void dispose() { // assist GC -- may help it run slightly faster: imageSRC = null; relay.flushForGC(); relay = null; } final boolean isRunning() { return started; } /** @return an Image that's already been put into the cache */ private final Handle runToResult() { if (DEBUG.IMAGE) debugMark(">>>>>"); final Handle handle = produceResult(); if (DEBUG.IMAGE) { debugMark("<<<<<"); TaskQueue.dump(); } dispose(); return handle; } private void debugMark(String s) { Log.debug(Util.TERM_YELLOW + s + "---" + getClass().getSimpleName() + "-" + getPriority() + "---" + imageSRC.debugName() + "-----------------------------------------------------------------------------" + Util.TERM_CLEAR ); } /** @return an Image that's already been put into the cache */ Handle produceResult() { if (DEBUG.IMAGE || DEBUG.THREAD) out("loadAndCache kick: " + imageSRC); Handle h = loadImageAndCache(imageSRC, relay); if (DEBUG.IMAGE || DEBUG.THREAD) out("loadAndCache return: " + h); return h; } public void addListener(Listener newListener) { if (DEBUG.IMAGE) out(this + " addListener " + tag(newListener)); relay.addListener(newListener); } //public int getPriority() { return 5; } @Override public String toString() { try { return String.format("%s[%s%s:%s ->%s]", getClass().getSimpleName(), isRunning() ? "<RUNNING> " : "", getPriority(), imageSRC == null ? "null-imageSRC" : imageSRC.debugName(), relay == null ? "null-relay" : relay.chain.head); } catch (Throwable t) { return getClass().getSimpleName() + "[" + t + "]"; } } } private static class Task extends Loader { static Loader create(ImageSource imageSRC, Listener listener, Object when) { final Loader loader; if (imageSRC.key == null) { if (DEBUG.IMAGE||DEBUG.WORK) Util.printStackTrace("attempting to load cache w/null key: " + imageSRC); else Log.warn("attempting to load cache w/null key: " + imageSRC); } // It's okay if listener is null: a CachingRelayer will still be created, in // the Loader, and listeners can be added later. // note: if imageSRC is a file, and it doesn't exist, we could fail-fast // (perhaps if immediate is requested?), rather than wait for the error to // be reported via Listener.gotImageError, tho that's the standard API for // now. // ISSUE: if we use a Deque to deal with priorities (regular requests LIFO'd up front, // caching requests at end), we still have the problem of ensuring that IconTasks take // priority over everything else. if (imageSRC.mayBlockIndefinitely()) { // run in a completely separate thread outside the thread pool: loader = new LoadThread(imageSRC, listener); } else if (imageSRC.isImageSourceForIcon()) { // highest-priority tasks: loader = new IconTask(imageSRC, listener); } else if (when == LOAD_CACHE) { loader = new Task(imageSRC, listener, PRI_LOW); // if (DEBUG.Enabled && listener != null) { // Log.warn("cache tasks don't normally want listeners: " + loader); // } } else { // regular task: loader = new Task(imageSRC, listener, PRI_NORM); } return loader; } Object priority; private Task(ImageSource is, Listener relay, Object pri) { super(is, relay); priority = pri; if (DEBUG.IMAGE && relay == null) Log.debug(this + "; nobody currently listening: image may be quietly cached: " + imageSRC); } void raisePriority() { if (!isRunning()) TaskQueue.raiseTaskPriority(this); } synchronized void setPriority(Object key) { priority = key; } synchronized Object getPriority() { return priority; } } /** a task to generate an icon */ private static final class IconTask extends Task { IconTask(ImageSource is, Listener relay) { super(is, relay, PRI_HIGH); if (DEBUG.Enabled && relay == null) Log.debug(this + "; nobody currently listening: image may be quietly cached: " + imageSRC); } @Override Handle produceResult() { //if (DEBUG.IMAGE || DEBUG.THREAD) out("ICON-TASK for " + imageSRC + " kicked off"); // At this point, there's a cache entry containing this IconTask, so no // other icon requestor in another thread will attempt to create an icon -- // it will simply be chained up to the results of this Loader waiting for // callback. return createAndCacheIcon(relay, imageSRC); } } /** a marker class */ private static final class ImgThread extends Thread { ImgThread(Runnable r, String name) { super(r, name); } } /** * A thread for loading a single image. Images.Listener results are delievered * from this thread (unless the image was already cached). */ private static final class LoadThread extends Loader { private static int LoaderCount = 0; private final Thread thread; /** * @param src must be any valid src *except* a Resource * @param resource - if this is tied to a resource to update with meta-data after loading */ LoadThread(ImageSource imageSRC, Listener firstRelay) { super(imageSRC, firstRelay); thread = new ImgThread(this, String.format("ImgLoader-%02d", LoaderCount++)); thread.setDaemon(true); thread.setPriority(Thread.MIN_PRIORITY); } public void join() throws InterruptedException { thread.join(); } public void start() { //if (DEBUG.IMAGE) Log.debug("======================================================="); thread.start(); } public void stop() { thread.stop(); } } /** * This method has side effects to the cache. At the end of this call, we know * there is *something* in the cache for the given imageSRC.key -- either we found * the image already there, or we found a Loader thread already started there, or we * put a new Loader into the cache and kicked it off, unless it was an immediate request, * (an anusual but supported case) in which case first the Loader will have been put in the * cache, and then the final result before we return. * * This method also deals with cache cleanup: if an entry is found to be * empty (it has no disk file, and it's image has been GC'd), the entry * is removed. * * @return If an Image, we had the image in the cache immediately availble. If a * Loader thread object, the image is loading, and we should become and additional * listener (if there is a listener given), or wait for the Loader to die to get * it's results. If the special value IMAGE_LOADER_STARTED is returned, there is * nothing to do be done for now -- just wait for the callbacks to the listener if * one was provided. */ private static Object getCachedOrKickLoad(ImageSource imageSRC, Images.Listener listener, Object when) { Loader loader = null; //------------------------------------------------------- // OBTAIN CACHE LOCK //------------------------------------------------------- synchronized (RawCache) { final Object entry = getCacheContentsWithAutoFlush(imageSRC); // if anything in the Cache, immediately return it. Could // be the desired image, or an existing loader. if (entry != null) { // TODO: if this is a "regular priority" request, and we find a loader in the cache // that has NOT already been started, then the task queue(s) may need modification. // If the existing task is in the caching queue, it needs to be moved (to the // front) of the regular loading queue. If the existing task is in the loading // queue, it needs to be moved (LIFO style) to the front of the loading queue. return entry; // cache lock released } else { // Nothing was in the cache. Create a task for loading // the image, which will then normally be run in another thread. // the task immediately in the current thread and return the // result. loader = Task.create(imageSRC, listener, when); // Note that even if we've been requested to run synchronously (no listener or // "immediate" is true) , we still want a Loader created that can have listeners // added later, and have it marked in the cache, in case subsequent asynchronous // requests come in for this image, or even future immediate requests, which then // won't be honored: they'll get a callback when the previous immediate load // finishes. markCacheAsLoading(imageSRC.key, loader); } } //------------------------------------------------------- // CACHE LOCK IS RELEASED //------------------------------------------------------- if (when == LOAD_IMMEDIATE) { // It's crucial that this NOT be run in a Cache-lock, or // every other image thread will soon hang until this is // done, including the AWT thread if anything requests // image data. // runToResult should always return an image that's been loaded into the cache return loader.runToResult(); } else { kickTask(loader); return LOADER_JUST_STARTED; } } // private static Loader createTask(ImageSource imageSRC, Listener listener, Object when) // { // final Loader loader; // if (imageSRC.key == null) { // if (DEBUG.Enabled) // Util.printStackTrace("attempting to load cache w/null key: " + imageSRC); // else // Log.warn("attempting to load cache w/null key: " + imageSRC); // } // // It's okay if listener is null: a CachingRelayer will still be created, in // // the Loader, and listeners can be added later. // // note: if imageSRC is a file, and it doesn't exist, we could fail-fast // // (perhaps if immediate is requested?), rather than wait for the error to // // be reported via Listener.gotImageError, tho that's the standard API for // // now. // // ISSUE: if we use a Deque to deal with priorities (regular requests LIFO'd up front, // // caching requests at end), we still have the problem of ensuring that IconTasks take // // priority over everything else. // if (imageSRC.mayBlockIndefinitely()) { // // run in a completely separate thread outside the thread pool: // loader = new LoadThread(imageSRC, listener); // } else if (imageSRC.isImageSourceForIcon()) { // // highest-priority tasks: // loader = new IconTask(imageSRC, listener); // } else if (when == LOAD_CACHE) { // loader = new CacheTask(imageSRC, listener); // // if (DEBUG.Enabled && listener != null) { // // Log.warn("cache tasks don't normally want listeners: " + loader); // // } // } else { // // regular task: // loader = new LoadTask(imageSRC, listener); // } // return loader; // } private static void markCacheAsLoading(URI key, Loader loader) { final Object old = RawCache.put(key, loader); // todo: under what conditiions is this normal? if (old != null && old instanceof CacheEntry && !((CacheEntry)old).isPreloadedDiskEntry()) { Log.warn("blew away existing cache content:\n\told: " + Util.tags(old) + "\n\tfor: " + Util.tags(loader)); } } private static void kickTask(Loader loader) { if (loader instanceof LoadThread) { // could submit to a special pool or some future fancy non-blocking NIO multi-stream handler ((LoadThread)loader).start(); } else { ProcessingPool.addTask((Task)loader); } } /** * Get the cache contents for the given source (either an Image or * a Loader), and update the cache entry if needed. This should * only be called from within a Cache lock. Will return null if * no cache entry was found, or one was found but it's contents * had been garbage collected. In the latter case, the empty * entry will be flushed from the cache. */ private static Object getCacheContentsWithAutoFlush(ImageSource imageSRC) { if (imageSRC.key == null) return null; final Object entry = RawCache.get(imageSRC.key); if (entry == null) return null; if (DEBUG.IMAGE) out("found cache entry for key " + tag(imageSRC.key) + ": " + entry); if (entry instanceof Loader) { if (DEBUG.IMAGE) out("Image is loading into the cache via already existing Loader..."); // For ideal LIFO handling of image requests, we should at this point // find out if the Loader is already running in the thread pool or not. If // it's NOT running, we should advance it's priority to the front of // the queue since it's been requested again. Unless this request // is for low-priority PRE-CACHING, in which case we'd need not // make any change -- the image is already loading with a high priority. return entry; } // Entry is not a Loader, so it must be a regular CacheEntry We still may not // have an image tho: it may have be been garbage collected, or the entry may // actually be for a file on disk. final CacheEntry ce = (CacheEntry) entry; final Image cachedImage = ce.getCachedImage(); // if we have the image, we're done (it was loaded this runtime, and not GC'd) // if not, either it was GC'd, or it's a cache file entry from the persistent // cache -- in either case, there is a file on disk -- mark it in the imageSRC, // and the loader will notice it and use it. boolean emptyEntry = true; if (cachedImage != null) { emptyEntry = false; } else if (ce.getFile() != null) { if (ce.file.canRead()) { imageSRC.setCacheFile(ce.file); // Note: imageSRC side effect emptyEntry = false; } else Log.warn("cache file no longer available: " + ce.file); } if (emptyEntry) { // there is a cache entry with no image OR file: this could only // happen if the disk cache is not operating, and the memory // image was garbage collected: we need to remove this entry // from the cache completely and start from scratch: if (DEBUG.IMAGE) out("REMOVING FROM CACHE: " + imageSRC); RawCache.remove(imageSRC.key); } if (cachedImage == null) return null; else return new Handle(cachedImage, ce.data); //return cachedImage; // If cachedImage is null at this point, there was an entry in the cache, but it was of no use: // That happens in the following cases: // (1) We had the image, but is was GC'd -- we're going back to the cache file // (2) We had the image, but is was GC'd -- original was on disk: go back to that // (3) We had the image, but is was GC'd, and disk cache not working: reload original from network // (4) We never had the image, but it is in disk cache: go get it // (5) unlikely case case of zealous GC: reload original // Note that original image sources that were on disk are NOT moved // to the disk cache, and CacheEntry.file should always be null for those. } private static final String NO_READABLE_FOUND = "No Readable"; /** @return true if there's a cache entry for the given key. The entry may be in any state: * an unloaded pre-registered disk cache entry, a loaded cache entry, or a loader in progress. */ public static boolean hasCacheEntry(URI cacheKey) { return cacheKey != null && RawCache.get(cacheKey) != null; } private static Handle createAndCacheIcon(Listener listener, ImageSource iconSource) { final Image hardImage; boolean badReadable = false; if (iconSource.readable instanceof Image) { hardImage = (Image) iconSource.readable; // lose hard reference to the original image so it can be GC'd, // and be sure do this before we're start creating the icon, so if we // EOM or error it's already been cleared. THIS IS CRUCIAL -- // if we don't do this, recovering from OutOfMemoryError's when // generating icons is virtually impossible. iconSource.readable = null; //======================================================================================== // TODO: OUTSTANDING PROBLEM: // If we hit EOM creating this icon, the icon-source readable is cleared, and // thus the icon ImageRep goes permanently bad -- it can't reconsititute. // Yet the icon ImageSource (can) has a ref to the full ImageRep, which it // could reconstitute, and then generate the image from, but then // the Icon ImageRep would need to listen first for the full callbacks, // then the the icon callbacks. We could just try NOT clearing it here, // and let the ImageRep clear it only if it gets the image, but then we // can run lower on memory by leaving this hard-ref around... Need to run tests. // // The best fix for this probably includes going all the way to keeping singleton // ImageRef's in the cache, perhaps only allow icons to be obtained through their // ImageRef. //======================================================================================== } else if (iconSource.readable instanceof ImageRep) { hardImage = ((ImageRep) iconSource.readable).image(); } else { badReadable = true; hardImage = null; } if (badReadable) throw new Error("iconSource had no image content in readable: " + iconSource); if (hardImage == null) { Log.warn("hard image GC'd before icon creation, forcing low-memory conditions (" + iconSource + ")"); setLowMemory("GC-wanted-data"); // REMOVE THE FAILED LOADER FROM THE CACHE RawCache.remove(iconSource.key); // todo: generally handle caching in our caller based on return value? // todo: nobody to catch this error and deliver gotImageError! //throw new OutOfMemoryError("full-rep was GC'd: forcing low memory conditions"); if (listener != null) { listener.gotImageError(iconSource, OUT_OF_MEMORY); return null; } } final Dimension originalSize = new Dimension(hardImage.getWidth(null), hardImage.getHeight(null)); final Handle iconHandle = createIcon(hardImage, iconSource.iconSize); if (listener != null) { // note that we could init the handle with data containing he size of the source image, // but that info is only useful when we do NOT already have the full rep loaded, // and we only get here if we just created an icon from the full rep, so it's okay to leave // the image data blank in the delivered handle. That data shouldn't even need to go into // the cache this runtime? It should probably go there anyway just to be safe. listener.gotImage(iconSource, iconHandle); } File cacheFile = null; try { cacheFile = makePermanentCacheFile(iconSource.key); } catch (Throwable t) { Log.error("creating cache file for " + iconSource, t); } // if for any reason the disk cache has failed, we can still create the CacheEntry with a null file RawCache.put(iconSource.key, new CacheEntry(iconHandle, cacheFile)); // TODO: Make sure size data is in icon cache entry to be consistent with state of cache on re-init. // Some code is actually sensitive to this. Oh -- wait -- maybe the problem is that it is NOT // in after it's been loaded to the disk cache? if (cacheFile != null && iconHandle.image != null) cacheIconToDisk(iconSource.key, (RenderedImage) iconHandle.image, cacheFile, originalSize); return iconHandle; } private static boolean cacheIconToDisk(URI iconKey, RenderedImage image, File cacheFile, Dimension originalSize) { final Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png"); ImageWriter writer = null; int count = 1; while (writers.hasNext()) { ImageWriter i = writers.next(); if (writer == null) writer = i; if (count > 1 || (DEBUG.IMAGE && DEBUG.IO)) { Log.debug("FOUND WRITER #" + count + "for PNG: " + Util.tags(i)); } count++; } try { //File cacheFile = makePermanentCacheFile(iconKey); if (DEBUG.IMAGE||DEBUG.IO) Log.debug("writing " + cacheFile); if (writer != null) { writeWithComments(writer, image, cacheFile, originalSize); } else { Log.warn("unable to find writer for PNG; " + cacheFile); ImageIO.write(image, "png", cacheFile); } if (DEBUG.IMAGE||DEBUG.IO) Log.debug(" wrote " + cacheFile); return true; } catch (Throwable t) { Log.error("writing icon cache file " + iconKey, t); return false; } } /** see com.sun.imageio.plugins.png.PNGMetadata */ private static final String PNG_META_DATA = "javax_imageio_png_1.0"; private static final String GENERIC_META_DATA = IIOMetadataFormatImpl.standardMetadataFormatName; //private static final String VUE_META_DATA = GENERIC_META_DATA; private static final String VUE_IMAGE_META_KEY = "VueData"; private static final String VUE_IMAGE_META_VALUE_PREFIX = "@(#)VUE-DATA/"; private static void writeWithComments(ImageWriter writer, RenderedImage image, File file, Dimension originalSize) throws IOException { if (DEBUG.Enabled||DEBUG.IMAGE||DEBUG.IO) Log.debug("WRITER WRITE " + writer + "; " + file); //ImageWriteParam param = writer.getDefaultWriteParam(); //Log.debug("DEFAULT WRITE PARAM: " + Util.tags(param)); final ImageTypeSpecifier image_type = new ImageTypeSpecifier(image); final IIOMetadata meta_data = writer.getDefaultImageMetadata(image_type, null); // Log.debug("GOT META-DATA: " + Util.tags(meta_data)); if (DEBUG.IMAGE && DEBUG.IO) { Log.debug(" FORMAT-NAMES: " + Arrays.asList(meta_data.getMetadataFormatNames())); Log.debug("NATIVE-FORMAT: " + meta_data.getNativeMetadataFormatName()); } final org.w3c.dom.Node metaTree = meta_data.getAsTree(GENERIC_META_DATA); addData(metaTree, "cacheFile", file.getName()); addData(metaTree, "sourceSize", String.format("%d,%d", originalSize.width, originalSize.height)); meta_data.mergeTree(GENERIC_META_DATA, metaTree); if (DEBUG.IO) { //Log.debug("TREE: " + Util.tags(tree)); //Log.debug("ATTRIBS: " + Util.tags(tree.getAttributes())); //Log.debug("CHILDREN: " + Util.tags(tree.getChildNodes())); //DocDump.dump(meta_data.getAsTree(GENERIC_META_DATA)); DocDump.dump(meta_data.getAsTree(PNG_META_DATA)); } final IIOImage io_image = new IIOImage(image, Collections.EMPTY_LIST, meta_data); final ImageOutputStream output = ImageIO.createImageOutputStream(file); writer.setOutput(output); writer.write(io_image); output.close(); } private static void addData (final org.w3c.dom.Node tree, final String key, final String value) throws IOException { // note: we add a trailing newline for /usr/bin/what final String comment = String.format("%s%s: %s\n", VUE_IMAGE_META_VALUE_PREFIX, key, value); final String keyword = VUE_IMAGE_META_KEY + "/" + key; // note: the keyword data is currently redundant (ignored by us when loading icons we've written out), // but its presence is required by the ImageIO meta-data API. final IIOMetadataNode text; final IIOMetadataNode textRoot; if (DEBUG.IO||DEBUG.IMAGE) Log.debug("addData; " + keyword + "=" + comment); if (false) { // use PNG-specific meta-data -- simpler, but can only be used with PNG's text = new IIOMetadataNode("tEXtEntry"); // PNG name text.setAttribute("keyword", keyword); text.setAttribute("value", comment); textRoot = new IIOMetadataNode("tEXt"); // PNG name } else { //----------------------------------------------------------------------------- // The most generic method -- we should be able to use this with image // formats beyond PNG, tho that's untested. The meta-data format in the tree // (root node) should be IOMetadataFormatImpl.standardMetadataFormatName // ("javax_imageio_1.0"). // ----------------------------------------------------------------------------- text = new IIOMetadataNode("TextEntry"); // generic name text.setAttribute("keyword", keyword); text.setAttribute("value", comment); // required, but ignored in PNGMetadata: text.setAttribute("encoding", "ISO-8859-1"); // required, but ignored in PNGMetadata as long as 1st 255 chars all ascii: text.setAttribute("language", ""); // apparently safest: empty means undefined text.setAttribute("compression", "none"); textRoot = new IIOMetadataNode("Text"); // generic name } textRoot.appendChild(text); tree.appendChild(textRoot); } private static Map<String,Object> extractVueImageMetaData(IIOMetadata metaData) { //final org.w3c.dom.Node metaTree final IIOMetadataNode metaTree = (IIOMetadataNode) metaData.getAsTree(GENERIC_META_DATA); if (DEBUG.IMAGE && DEBUG.IO) { Log.debug(" FORMAT-NAMES: " + Arrays.asList(metaData.getMetadataFormatNames())); Log.debug("GOT META-DATA: " + Util.tags(metaData) + " for format " + GENERIC_META_DATA); DocDump.dump(metaTree); //DocDump.dump(data.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName)); //DocDump.dump(data.getAsTree(PNG_FORMAT_NAME)); } return extractVueImageMetaData(metaTree); } private static Map<String,Object> extractVueImageMetaData(IIOMetadataNode metaTree) { Map result = Collections.EMPTY_MAP; for (org.w3c.dom.Node n : Util.iterable(metaTree.getElementsByTagName("TextEntry"))) { //Log.debug("FOUND TEXT NODE " + Util.tags(n) + " " + Util.tags(n.getAttributes())); if (DEBUG.IO && DEBUG.DATA) Log.debug("FOUND TEXT NODE " + Util.tags(n)); //Log.debug("FOUND TEXT " + n.getTextContent()); for (org.w3c.dom.Node a : Util.iterable(n.getAttributes())) { if (DEBUG.IO && DEBUG.DATA) Log.debug("FOUND TEXT ATTRIB " + Util.tags(a)); String text = a.getNodeValue(); // we don't bother to check for the keyword=value attribute -- it's all in the value anyway if (text != null && text.startsWith(VUE_IMAGE_META_VALUE_PREFIX)) { final String s, key, value; s = text.substring(VUE_IMAGE_META_VALUE_PREFIX.length()); key = s.substring(0, s.indexOf(':')); value = s.substring(key.length()+2, s.length()-1); if (DEBUG.IO || DEBUG.DATA) Log.debug(String.format("VUE-DATA: key=%s value=%s", key, Util.tags(value))); if ("cacheFile".equals(key)) { // skip this one -- is not currently needed, and clutters up debug bigtime continue; } if (result == Collections.EMPTY_MAP) result = new HashMap(4); result.put(key, value); } } } return result; } // Our test w/NODESET works in XML-INGEST w/NY-TIMES RSS feed, but not here... // XPath xpath = XPathFactory.newInstance().newXPath(); // //String expression = "javax_imageio_1.0" + "/Text/TextEntry/VueMetaData"; // //String expression = "/javax_imageio_1.0" + "/Text"; // //String expression = "/javax_imageio_1.0" + "/Text/TextEntry"; // String expression = "/Text"; // //String expression = "/Text/TextEntry"; // Log.debug("Extracting " + Util.tags(expression) + " from " + Util.tags(metaTree)); // // First, obtain the element as a node. // NodeList nodeSet = (NodeList) xpath.evaluate(expression, // //Object nodeSet = xpath.evaluate(expression, // metaTree, // XPathConstants.NODESET); // NODE will only do 1 node // //Log.debug(" Node: " + nodeValue); // Log.debug("NodeList: " + Util.tags(nodeSet) + "; size=" + nodeSet.getLength()); // //Log.debug("NodeResult: " + Util.tags(nodeSet)); public static Object getCacheLock() { return RawCache; } static class ImageException extends Exception { ImageException(String s) { super(s); }} static class DataException extends ImageException { DataException(String s) { super(s); }} public static final String OUT_OF_MEMORY = "Out of memory"; /** An wrapper for readAndCreateImage that deals with exceptions, and puts successful results in the cache */ // note: we don't actually need to know the second arg is a ListenerRelay -- it could just be a // Listener, tho it never is -- this is for clarity -- remember that the callbacks may be // happening to a chain of pending listeners, not just one. private static Handle loadImageAndCache(final ImageSource imageSRC, final Listener relay) { Handle imageData = null; if (imageSRC.resource != null) imageSRC.resource.getProperties().holdChanges(); try { imageData = readImageInAvailableMemory(imageSRC, relay); } catch (Throwable t) { if (DEBUG.IMAGE) Util.printStackTrace(t); RawCache.remove(imageSRC.key); if (relay != null) { String msg; boolean dumpTrace = false; if (t instanceof java.io.FileNotFoundException) { msg = "Not Found: " + t.getMessage(); } else if (t instanceof java.net.UnknownHostException) { msg = "Unknown Host: " + t.getMessage(); } else if (t instanceof java.lang.IllegalArgumentException && t.getMessage().startsWith("LUT has improper length")) { // known java bug: many small PNG images fail to read (effects thumbshots) msg = null; // don't bother to report an error } else if (t instanceof OutOfMemoryError) { setLowMemory(t); msg = OUT_OF_MEMORY; } else if (t instanceof ImageException) { msg = (t.getMessage() == NO_READABLE_FOUND ? null : t.getMessage()); } else if (t instanceof ThreadDeath) { msg = "interrupted"; } else if (t.getMessage() != null && t.getMessage().length() > 0) { msg = t.getMessage(); dumpTrace = true; } else { msg = t.toString(); dumpTrace = true; } // this is the one place we deliver caught exceptions // during image loading: relay.gotImageError(imageSRC.original, msg); if (dumpTrace) Log.warn(imageSRC + ":", t); else Log.warn(imageSRC + ": " + t); } if (imageSRC.resource != null) imageSRC.resource.getProperties().releaseChanges(); } if (relay != null && imageData != null) { relay.gotImage(imageSRC.original, imageData); } //----------------------------------------------------------------------------- // If we were to auto-generate icons in Images, this would be the place to do it. //----------------------------------------------------------------------------- // TODO opt: if this item was loaded from the disk cache, we're needlessly // replacing the existing CacheEntry with a new one, instead of // updating the old with the new in-memory image buffer. if (imageSRC.useCacheFile()) { if (imageData != null) { File permanentCacheFile = null; if (imageSRC.hasCacheFile()) permanentCacheFile = ensurePermanentCacheFile(imageSRC.getCacheFile()); if (DEBUG.IMAGE) out("getting cache lock for storing result; " + imageSRC); RawCache.put(imageSRC.key, new CacheEntry(imageData, permanentCacheFile)); // If the cache file has moved from tmp to permanent, we'd need to do this to keep imageSRC // current, tho at the moment this is a bit overkill as we should no longer need imageSRC // at this point, but just in case it wants to update something such as a Resource with // a reference to the right cache file, we do this here. if (permanentCacheFile != null) imageSRC.setCacheFile(permanentCacheFile); // if (imageSRC.resource != null && permanentCacheFile != null) // imageSRC.resource.setCacheFile(permanentCacheFile); } } else { if (imageData.image != null) RawCache.put(imageSRC.key, new CacheEntry(imageData, null)); //RawCache.put(imageSRC.key, new CacheEntry(image, null)); } return imageData; } private static Handle readImageInAvailableMemory(ImageSource imageSRC, Listener listener) throws ImageException { //Image image = null; Handle imageData = null; do { try { imageData = readAndCreateImage(imageSRC, listener); } catch (OutOfMemoryError eom) { setLowMemory(eom); Log.warn(Util.TERM_YELLOW + "out of memory reading " + imageSRC.readable + ": " + eom + Util.TERM_CLEAR); if (Thread.currentThread() instanceof ImgThread) { Log.info("reader sleeping & retrying..."); try { // Todo: sleep until all other image load threads have finished, or, // ideally, until there aren't any running (those still running are // blocked). May actually just be easiest to to abort and re-submit this // to entirely new execution queue, or could just stick it on the end of // the existing queue, tho if this is a network IO thread, we still want to // run it later in it's own thread, so it could just be a task the // re-creates a new LoadThread Thread.currentThread().sleep(1000 * 45); } catch (InterruptedException e) { Log.warn("reader interrupted"); } Log.info(Util.TERM_GREEN + "reader re-awakened, retrying... " + imageSRC.readable + Util.TERM_CLEAR); } else { // if instanceof POOLTHREAD // we must be running in the thread-pool accessing local disk // (or, e.g., we could be in a print job thread processing immediate requests) if (DEBUG.Enabled) Log.info("EOM; consider re-kicking a LoadTask for " + imageSRC); //e.g., somehing like: kickLoad(imageSRC, listener); throw eom; } } catch (Throwable t) { throw new ImageException("reader failed: " + t.toString()); } } while (imageData == null); if (DEBUG.IMAGE) out("readAndCreateImage got " + Util.tags(imageData)); return imageData; } // todo: this probably wants to move to a resource impl class private static void setDateValue(Resource r, String name, long value) { if (value > 0) r.setProperty(name, new java.util.Date(value).toString()); // todo: set raw value for compares, but allow prop displayer to convert it? // or put a raw Date object in there? } // todo: this probably wants to move to a resource impl class private static void setResourceMetaData(Resource r, java.net.URLConnection uc) { // r.getProperties().holdChanges(); // try { long len = uc.getContentLength(); //r.setProperty("url.contentLength", len); r.setProperty(CONTENT_SIZE, len); r.setByteSize(len); // todo: update later from cache file size for correctness String ct = uc.getContentType(); //r.setProperty("url.contentType", ct); r.setProperty(CONTENT_TYPE, ct); if (DEBUG.Enabled && ct != null && !ct.toLowerCase().startsWith("image")) { Log.warn("NON-IMAGE CONTENT TYPE [" + ct + "]; " + r); } setDateValue(r, "URL.expires", uc.getExpiration()); //setDateValue(r, "url.date", uc.getDate()); setDateValue(r, CONTENT_ASOF, uc.getDate()); // should probably ignore this an generate ourselves //setDateValue(r, "url.lastModified", uc.getLastModified()); setDateValue(r, CONTENT_MODIFIED, uc.getLastModified()); // } catch (Throwable t) { // Util.printStackTrace(t); // } finally { // r.getProperties().releaseChanges(); // } } // todo: this probably wants to move to a resource impl class private static void setResourceMetaData(Resource r, File f) { // r.getProperties().holdChanges(); // try { //r.setProperty("file.size", f.length()); r.setProperty(CONTENT_SIZE, f.length()); //setDateValue(r, "file.lastModified", f.lastModified()); setDateValue(r, CONTENT_MODIFIED, f.lastModified()); //r.setProperty(CONTENT_TYPE, java.net.URLConnection.guessContentTypeFromName(f.getName())); // todo: also URLConnection.guessContentTypeFromStream (could use in FileBackedImageInputStream) // } finally { // r.getProperties().releaseChanges(); // } } private static File makePermanentCacheFile(URI key) throws java.io.UnsupportedEncodingException { return makeCacheFile(key, false); } private static File makeTmpCacheFile(URI key) throws java.io.UnsupportedEncodingException { return makeCacheFile(key, true); } /** @param temporary -- for temporary cache files that have yet to complete (e.g., not all data has arrived) */ private static File makeCacheFile(URI key, boolean temporary) throws java.io.UnsupportedEncodingException { final String cacheName; if (temporary) cacheName = "." + keyToCacheFileName(key); else cacheName = keyToCacheFileName(key); File cacheDir = getCacheDirectory(); File file = null; if (cacheDir != null) { file = new File(getCacheDirectory(), cacheName); try { if (!file.createNewFile()) { if (DEBUG.IO) Log.debug("cache file already exists: " + file); } } catch (java.io.IOException e) { String msg = "can't create cache file: " + file; if (DEBUG.Enabled) Util.printStackTrace(e, msg); else Log.warn(msg, e); return null; } if (!file.canWrite()) { Log.warn("can't write cache file: " + file); return null; } if (DEBUG.IMAGE) out("got cache file " + file); } return file; } private static File ensurePermanentCacheFile(File file) { try { // chop off the initial "." to make permanent // If doesn't start with a dot, this is one of our existing cache // files: nothing to do. String tmpName = file.getName(); String permanentName; if (tmpName.charAt(0) == '.') { permanentName = tmpName.substring(1); } else { // it's already permanent: we must have loaded this from our cache originally return file; } File permanentFile = new File(file.getParentFile(), permanentName); if (file.renameTo(permanentFile)) { Log.debug("new perm cache file: " + permanentFile); return permanentFile; } } catch (Throwable t) { tufts.Util.printStackTrace(t, "Unable to create permanent cache file from tmp " + file); } return null; } private static File CacheDir; private static File getCacheDirectory() { if (CacheDir == null) { File dir = VueUtil.getDefaultUserFolder(); CacheDir = new File(dir, "cache"); if (!CacheDir.exists()) { Log.debug("creating cache directory: " + CacheDir); if (!CacheDir.mkdir()) Log.warn("couldn't create cache directory " + CacheDir); } else if (!CacheDir.isDirectory()) { Log.warn("couldn't create cache directory (is a file) " + CacheDir); return CacheDir = null; } Log.debug("Got cache directory: " + CacheDir); } return CacheDir; } // private static final class ImageProps extends HashMap<String,?> { // ImageProps() { // super(6); // } // } public static final class Handle { public final Image image; final Map<String,?> data; // note: shouldn't expose this globally as modifiable Handle(Image i, Map<String,?> data) { if (i == null) throw new NullPointerException("null image"); if (data == null) throw new NullPointerException("null data"); this.image = i; this.data = data; } Handle(Image i) { this(i, Collections.EMPTY_MAP); } private Handle() { image = null; data = Collections.EMPTY_MAP; } public static Handle emptyInstance() { return new Handle(); } @Override public String toString() { return Util.tags(image) + data; } } private static Image handleToImage(Handle h) { return h == null ? null : h.image; } //private static Handle NULL_HANDLE = new Handle(); /** * @param imageSRC - see ImageSource ("anything" that we can get an image data stream from) * @param listener - an Images.Listener: if non-null, will be issued a callback for when the size is first obtained * @return the loaded image, or null if none found */ private static boolean FirstFailure = true; private static Handle readAndCreateImage(ImageSource imageSRC, Images.Listener listener) throws java.io.IOException, ImageException { if (DEBUG.IMAGE) out("trying: " + imageSRC); InputStream urlStream = null; // if we create one, we need to keep this ref to close it later File tmpCacheFile = null; // if we create a tmp cache file, it will be put here int dataSize = -1; if (DEBUG.IMAGE && imageSRC.resource != null) { imageSRC.resource.setDebugProperty("image.read", imageSRC.readable); } if (imageSRC.hasCacheFile()) { // just point us at the cache file: ImageIO will create the input stream imageSRC.readable = imageSRC.getCacheFile(); if (DEBUG.IMAGE && imageSRC.resource != null) { imageSRC.resource.setDebugProperty("image.cache", imageSRC.getCacheFile()); out("reading cache file: " + imageSRC.getCacheFile()); } // note: can get away with this because imageSRC.resource will // be null if this is for a preview icon, so don't need to worry // about getting wrong size todo: a hack anyway -- include // in clean-up of meta-data setting if (imageSRC.resource != null) { imageSRC.resource.setProperty(CONTENT_SIZE, imageSRC.getCacheFile().length()); // java has no creation date for Files! Well, last modified good enough... setDateValue(imageSRC.resource, CONTENT_ASOF, imageSRC.getCacheFile().lastModified()); } } else if (imageSRC.readable instanceof java.net.URL) { final URL url = (URL) imageSRC.readable; int tries = 0; boolean success = false; final boolean debug = DEBUG.IMAGE || DEBUG.IO; do { final URLConnection conn = UrlAuthentication.getAuthenticatedConnection(url); urlStream = conn.getInputStream(); if (imageSRC.resource != null) { dataSize = conn.getContentLength(); try { setResourceMetaData(imageSRC.resource, conn); } catch (Throwable t) { // Don't fail if a problem with meta data: still give // a chance for the content to work... Util.printStackTrace(t, "URLConnection Meta Data Failure"); //imageSRC.resource.setProperty("MetaDataFailure", t.toString()); } } if (!imageSRC.useCacheFile()) { imageSRC.readable = urlStream; success = true; } else { tmpCacheFile = makeTmpCacheFile(imageSRC.key); imageSRC.setCacheFile(tmpCacheFile); // will be made permanent if no errors if (imageSRC.hasCacheFile()) { try { imageSRC.readable = new FileBackedImageInputStream(urlStream, tmpCacheFile, listener); success = true; } catch (Images.DataException e) { Log.warn(imageSRC + ": " + e); if (++tries > 1) { final String msg = "Try #" + tries + ": " + e; // if (DEBUG.Enabled) // Util.printStackTrace(msg); // else Log.warn(msg); throw e; } else { Log.info("second try for " + imageSRC); urlStream.close(); } // try the reconnect one more time } } else { // unable to create cache file: read directly from the stream Log.warn("Failed to create cache file " + tmpCacheFile); imageSRC.readable = urlStream; success = true; } } } while (!success && tries < 2); } else if (imageSRC.readable instanceof java.io.File) { if (DEBUG.IMAGE) Log.debug("Loading local file " + imageSRC.readable); if (imageSRC.resource != null) setResourceMetaData(imageSRC.resource, (File) imageSRC.readable); } if (imageSRC.resource != null) { // in case any held changes //if (DEBUG.DR) imageSRC.resource.setDebugProperty("readsrc", Util.tags(imageSRC.readable)); imageSRC.resource.getProperties().releaseChanges(); } final ImageInputStream inputStream; if (imageSRC.readable instanceof ImageInputStream) { inputStream = (ImageInputStream) imageSRC.readable; } else if (imageSRC.readable != null) { //if (DEBUG.IMAGE) out("ImageIO converting " + tag(imageSRC.readable) + " to InputStream..."); inputStream = ImageIO.createImageInputStream(imageSRC.readable); } else { throw new ImageException(NO_READABLE_FOUND); //Log.warn("not readable: " + imageSRC); } if (DEBUG.IMAGE) out("Got ImageInputStream " + inputStream); if (inputStream == null) throw new ImageException("Can't Access [" + imageSRC.readable + "]"); // e,g., local file permission denied ImageReader reader = getDecoder(inputStream, imageSRC); if (reader == null) { badStream: { if (FirstFailure) { // This FirstFailure code was an attempt to deal with what is now handled // via DataException, but it's not a bad idea to keep it around. FirstFailure = false; Log.warn("No reader found: first failure, rescanning for codecs: " + imageSRC); // TODO: okay, problem appears to be with the URLConnection / stream? Is only // getting us tiny amount of bytes the first time... if (DEBUG.Enabled) tufts.Util.printStackTrace("first failure: " + imageSRC); ImageIO.scanForPlugins(); reader = getDecoder(inputStream, imageSRC); if (reader != null) break badStream; } if (DEBUG.IMAGE) out("NO IMAGE READER FOUND FOR " + imageSRC); throw new ImageException("Unreadable Image Stream"); } } if (DEBUG.IMAGE) out("Chosen ImageReader for stream: " + reader + "; format=" + reader.getFormatName()); //reader.addIIOReadProgressListener(new ReadListener()); //out("added progress listener"); final boolean allowSeekBack = false; final boolean ignoreMetadata; if (imageSRC.isDiskCacheEntry()) { ignoreMetadata = false; } else { // Note that basic meta-data is still present on the resulting image when we // say to ignore it, tho extra meta-data we've added won't be there. This // may just be because it's creating it from scratch for the newly built // image instead of reading it from the on-disk image -- not sure. ignoreMetadata = true; } reader.setInput(inputStream, allowSeekBack, ignoreMetadata); if (DEBUG.IMAGE) out("Input for reader set to " + inputStream); if (DEBUG.IMAGE) out("Getting size..."); int w = reader.getWidth(0); int h = reader.getHeight(0); if (DEBUG.IMAGE) out("ImageReader got size " + w + "x" + h); if (w == 0 || h == 0) throw new ImageException("invalid size: width=" + w + "; height=" + h); if (imageSRC.resource != null) { if (DEBUG.IMAGE || DEBUG.THREAD || DEBUG.RESOURCE) out("setting resource image.* meta-data for " + imageSRC.resource); imageSRC.resource.getProperties().holdChanges(); imageSRC.resource.setProperty(Resource.IMAGE_WIDTH, Integer.toString(w)); imageSRC.resource.setProperty(Resource.IMAGE_HEIGHT, Integer.toString(h)); imageSRC.resource.setProperty(Resource.IMAGE_FORMAT, reader.getFormatName()); imageSRC.resource.setCached(true); // Note: If MetaDataPane is not carefully coded, this call // can lead to a DEADLOCK against the AWT thread (e.g., // entering PropertyMap.removeListener) imageSRC.resource.getProperties().releaseChanges(); } if (listener != null) { if (DEBUG.IMAGE) out("Sending size to " + tag(listener)); listener.gotImageSize(imageSRC.original, w, h, dataSize, null); } // FYI, if fetch meta-data, will need to trap exceptions here, as if there are // any problems or inconsistencies with it, we'll get an exception, even if the // image is totally readable. //out("meta-data: " + reader.getImageMetadata(0)); //----------------------------------------------------------------------------- // Now read the image, creating the BufferedImage (or otherwise) // // Todo performance: using Toolkit.getImage on MacOSX gets us OSXImages, instead // of the BufferedImages which we get from ImageIO, which are presumably // non-writeable, and may perform better / be cached at the OS level. This of // course would only work for the original java image types: GIF, JPG, and PNG. //----------------------------------------------------------------------------- if (DEBUG.IMAGE || DEBUG.IO) out("reading " + imageSRC + "; " + reader + "..."); Image image = null; Map<String,Object> imageData = Collections.EMPTY_MAP; Throwable exception = null; try { image = reader.read(0); if (DEBUG.Enabled) out(" got " + imageSRC + "."); //testImageInspect(reader, image, imageSRC); //======================================================================================== //======================================================================================== //======================================================================================== // IF LISTENER IS NON-NULL, then fetch full-size, only to make a callback // analogue to gotImageSize. Tho would be better to stuff this info into // the cache, AS, OH, CRAP, WE NEED TO STUFF THIS INFO INTO THE CACHE.... // Subsequent callers, after load, are still going to need the full pixel // size. //======================================================================================== //======================================================================================== //======================================================================================== if (listener != null && !ignoreMetadata) { // look for meta-data we may have written with the image: // Note that arbitrary image meta-data created by god knows what app may // at any time be considered "bad" according to java, and cause an // exception to be thrown here. E.g., reading a random JPG: // "javax.imageio.IIOException: ICC APP2 encountered without prior // JFIF!" thrown from com.sun.imageio.plugins.jpeg.JPEGMetadata. // If we successfully wrote the meta-data ourselves, we should be pretty // darn safe tho. try { // note: the meta-data should have been cached by the reader -- it's // normally stored & read first final IIOMetadata data = reader.getImageMetadata(0); imageData = extractVueImageMetaData(data); String sourceSize = (String) imageData.get("sourceSize"); if (sourceSize != null) { if (DEBUG.IO) Log.debug("FOUND SOURCE SIZE[" + sourceSize + "]"); String[] dim = sourceSize.split(","); int sw = Integer.parseInt(dim[0]); int sh = Integer.parseInt(dim[1]); if (DEBUG.IO || DEBUG.IMAGE) Log.debug(String.format("parsed meta-data source size: %dx%d", sw, sh)); //imageData.put("sourceWidth", w); //imageData.put("sourceHeight", h); //listener.gotImageUpdate(Progress.SOURCE_SIZE, null); // NEED TO PASS IMAGE WIDTH/HEIGHT int[] srcSize = new int[2]; srcSize[0] = sw; srcSize[1] = sh; imageData.put("sourcePixels", srcSize); // we could deliver this, but we'll eventually get it with the image //listener.gotImageSize(imageSRC.original, w, h, dataSize, srcSize); } else { Log.warn("failed to find sourceSize in meta-data: " + imageData + " for " + imageSRC); } } catch (Throwable t) { Log.error("loading meta-data for " + imageSRC, t); } } } catch (Throwable t) { exception = t; } finally { reader.reset(); inputStream.close(); if (urlStream != null) urlStream.close(); } if (exception instanceof OutOfMemoryError) { setLowMemory(exception); throw (Error) exception; } else if (exception != null) { throw new ImageException("reader.read(0) failure: " + exception); } return new Handle(image, imageData); } private void testImageInspect(ImageReader reader, Image image, ImageSource imageSRC) { try { int thumbs = reader.getNumThumbnails(0); if (thumbs > 0) Log.info("thumbs: " + thumbs + "; " + imageSRC); } catch (Throwable t) { Log.debug("getNumThumbnails", t); } if (DEBUG.WORK && image != null) { String[] tryProps = new String[] { "name", "title", "description", "comment" }; for (int i = 0; i < tryProps.length; i++) { Object p = image.getProperty(tryProps[i], null); if (p != null && p != java.awt.Image.UndefinedProperty) System.err.println("FOUND PROPERTY " + tryProps[i] + "=" + p); } } } static Handle createIcon(Image source, int maxSide) { final Handle icon; // if (USE_SCALED_INSTANCE) { // return producePlatformIcon(source, maxSide); // } else { // return produceDrawnIcon(source, maxSide); // } if (DEBUG.Enabled) Log.debug("image pre-scaled: " + Util.tags(source) + " -> toMaxSide " + maxSide); icon = produceDrawnIcon(source, maxSide); if (DEBUG.Enabled) Log.debug("icon post-scaled: " + icon); return icon; } public static java.awt.Dimension fitInto(int maxSide, int srcW, int srcH) { int width, height; if (srcW > srcH) { width = maxSide; height = srcH * maxSide / srcW; // todo: ensure precision in above, and round to nearest EVEN value (for memory alignment) } else { height = maxSide; width = srcW * maxSide / srcH; // todo: ensure precision in above, and round to nearest EVEN value (for memory alignment) } //Log.debug(String.format("%dx%d -> (%d) %dx%d", srcW, srcH, maxSide, width, height)); return new java.awt.Dimension(width, height); } public static tufts.vue.Size fitInto(float maxSide, int[] srcSize) { return fitInto(maxSide, srcSize[0], srcSize[1]); } public static tufts.vue.Size fitInto(float maxSide, Size s) { return fitInto(maxSide, s.width, s.height); } public static tufts.vue.Size fitInto(float maxSide, float srcW, float srcH) { final float width, height; if (srcW < 1 || srcH < 1) { Log.error("fitInto " + maxSide + ": bad source size " + srcW + "x" + srcH); // make sure we never generate NaN results if (srcW < 1) srcW = 16; if (srcH < 1) srcH = 16; } if (srcW > srcH) { width = maxSide; height = srcH * maxSide / srcW; } else { height = maxSide; width = srcW * maxSide / srcH; } return new tufts.vue.Size(width, height); } private static Handle produceDrawnIcon(Image source, int maxSide) { final java.awt.Dimension size = fitInto(maxSide, source.getWidth(null), source.getHeight(null)); final Image iconSource; int transparency; if (source instanceof BufferedImage) transparency = ((BufferedImage)source).getTransparency(); else transparency = Transparency.OPAQUE; if (ALLOW_HIGH_QUALITY_ICONS /*&& DrawContext.isImageQualityRequested()*/) { // this is clever: we can still make use of the high-quality image // smoothing (tho it's still quite slow) by copying out the quality // image data then immediately flusing the created image. And with our // old 1GB busting use-case we're still down at 82MB after GC cool-down. // Amazing. iconSource = source.getScaledInstance(size.width, size.height, Image.SCALE_SMOOTH); if (iconSource instanceof sun.awt.image.ToolkitImage) { // will probably always be a ToolkitImage // generally neeeded in case the the image has alpha, so transparency will be Transparency.TRANSLUCENT // This will normally have already been pulled from the BufferedImage source, but just in // case the source wasn't a BufferedImage: transparency = ((sun.awt.image.ToolkitImage)iconSource).getColorModel().getTransparency(); } // note: there are supposed to be faster methods available for generating // similar quality that involve multi-pass down-scaling that have been // documented, tho the benchmarks don't likely include a comparison to // the most recent Mac OS X Java 1.6 implementation, that may use // CoreImage underneath. See: // http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html } else { iconSource = source; } final Image icon = tufts.vue.gui.GUI.getDeviceConfigForWindow(null) .createCompatibleImage(size.width, size.height, transparency); final Graphics2D g = (Graphics2D) icon.getGraphics(); if (iconSource == source) { // The source is the original raw image -- we'll be down-scaling during the // drawImage, so quality is going to be low. Note: setImageQuality is not much // help (if any?) as to quality in Java 1.6 (used to make a big difference in java // 1.5), but we try anyway. DrawContext.setImageQuality(g, true); } g.drawImage(iconSource, 0, 0, size.width, size.height, null); if (iconSource != source) iconSource.flush(); // crucial to release the memory consumed return new Handle(icon); // note: could load with source size info, but not needed in this case } private static Image producePlatformIcon(Image source, int maxSide) { final Dimension size = fitInto(maxSide, source.getWidth(null), source.getHeight(null)); final Image icon; // Note that on Mac OS X this returns an apple.awt.OSXImage's, and does so // IMMEDIATELY, which means they will later hang the paint thread the first time // they're painted (and your entire application) as the image production // (scaling/smoothing) takes place in lazy fashion at the last possible moment. // Note that whatever's returned will also probably always be an instance of // sun.awt.image.ToolkitImage (as OSXImage is), but how the various scaling // hints are handled, when/if they're lazy evaluated, etc is going to vary by // platform. // SCALE_SMOOTH produces dramatically higher quality than SCALE_FAST on Mac, // but is significantly slower icon = source.getScaledInstance(size.width, size.height, Image.SCALE_SMOOTH); // On Mac OSX, immediately drawing the image to a scratch buffer will force // the loading (and the actual scaling) of the apple.awt.OSXImage // if (false) { // // (would we have wanted to render icon, not _image?) // ScratchGraphics.drawImage(source, 0, 0, size.width, size.height, null); // } // This doesn't appear to be having a big impact on memory -- OSXImage is // apparently holding a reference to the underlying content. Even if all // images are read & scaled sequentially, we run out of memory (bumping // against 1GB using our test case). return icon; } // private static final Image ScratchImage; // private static final Graphics2D ScratchGraphics; // static { // if (USE_SCALED_INSTANCE) { // ScratchImage = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB); // ScratchGraphics = (Graphics2D) ScratchImage.getGraphics(); // } else { // ScratchImage = null; // ScratchGraphics = null; // } // } public static void dumpImage(Image image, String debug) { Object g; try { g = image.getGraphics(); } catch (Throwable t) { g = t; } Object s; try { s = image.getSource(); } catch (Throwable t) { s = t; } Log.debug("Image " + debug + ": " + Util.tags(image) + "\n\t size: " + image.getWidth(null) + "x" + image.getHeight(null) + "\n\t source: " + Util.tags(s) // + "\n\tgraphics: " + Util.tags(g) // + "\n\taccelPri: " + image.getAccelerationPriority() // + "\n\t caps: " + image.getCapabilities(null) ); //Util.dump(image.getClass().getMethods()); } private static ImageReader getDecoder(ImageInputStream istream, ImageSource imageSRC) { java.util.Iterator iri = ImageIO.getImageReaders(istream); ImageReader reader = null; int idx = 0; while (iri.hasNext()) { final ImageReader ir = (ImageReader) iri.next(); String formatName; try { formatName = ir.getFormatName(); } catch (Throwable t) { formatName = "[" + t + "]"; } if (reader == null) { reader = ir; } // else if ("ico".equalsIgnoreCase(formatName)) { // try { // if (imageSRC.key.toString().toLowerCase().endsWith(".ico")) { // if (DEBUG.IMAGE) out("CHOOSING ICO READER FOR .ICO"); // reader = ir; // } // } catch (Throwable t) { // Log.error("getDecoder: " + imageSRC + " " + t); // } // } if (DEBUG.IMAGE) { out("\t found ImageReader #" + idx + ": " + Util.tags(ir) + "; format=" + formatName + "; provider=" + Util.tags(ir.getOriginatingProvider())); } idx++; } return reader; } // nl.ikarus.nxt.priv.imageio.icoreader.lib.ICOReaderSpi.registerIcoReader(); // not needed: is self registering or SPI providing // nl.ikarus.nxt.priv.imageio.icoreader.lib.ICOReader [this reader, from ICOReader-1.04.jar, appears to mostly work] /* Apparently, not all decoders actually report to the listeners, (e.g., TIFF), so we're not using this for now */ private static class ReadListener implements IIOReadProgressListener { public void sequenceStarted(ImageReader source, int minIndex) { out("sequenceStarted; minIndex="+minIndex); } public void sequenceComplete(ImageReader source) { out("sequenceComplete"); } public void imageStarted(ImageReader source, int imageIndex) { out("imageStarted; imageIndex="+imageIndex); } public void imageProgress(ImageReader source, float pct) { out("imageProgress; "+(int)(pct + 0.5f) + "%"); } public void imageComplete(ImageReader source) { out("imageComplete"); } public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex){} public void thumbnailProgress(ImageReader source, float percentageDone) {} public void thumbnailComplete(ImageReader source) {} public void readAborted(ImageReader source) { out("readAborted"); } } private static String tag(Object o) { if (o instanceof java.awt.Component) return tufts.vue.gui.GUI.name(o); else if (o instanceof LWComponent) return o.toString(); //return ((LWComponent)o).getDiagnosticLabel(); else return Util.tags(o); // String s = Util.tag(o); // s += "["; // if (o instanceof Thread) { // s += ((Thread)o).getName(); // } else if (o instanceof BufferedImage) { // BufferedImage bi = (BufferedImage) o; // s += bi.getWidth() + "x" + bi.getHeight(); // } else if (o != null) // s += o.toString(); // return s + "]"; } private static void out(Object o) { Log.debug(o); //Log.debug((o==null?"null":o.toString())); /* String s = "Images " + (""+System.currentTimeMillis()).substring(8); s += " [" + Thread.currentThread().getName() + "]"; System.err.println(s + " " + (o==null?"null":o.toString())); */ } /* private static void copyStreamToFile(InputStream in, File file) throws java.io.IOException { ByteBuffer buf = ByteBuffer.allocate(2048); FileOutputStream fout = new FileOutputStream(file); FileChannel fcout = fout.getChannel(); ReadableByteChannel chin = Channels.newChannel(in); while (true) { buf.clear(); int r = chin.read(buf); //out("read " + r + " bytes"); if (DEBUG.IMAGE) System.err.print(r + "; "); if (r == -1) break; buf.flip(); fcout.write(buf); } fcout.close(); chin.close(); if (DEBUG.IMAGE) out("\nFILLED " + file); } private static File cacheURLContent(URL url, InputStream in) throws java.io.IOException { File file = getCacheFile(url); copyStreamToFile(in, file); return file; } */ /* static { // ImageIO file caching is a runtime-only scheme for allowing // file streams to seek backwards: nothing to with a presistant store. // It's also on by default. javax.imageio.ImageIO.setUseCache(true); javax.imageio.ImageIO.setCacheDirectory(new java.io.File("/tmp")); } */ public static void main(String args[]) throws Exception { // GUI init required for fully loading all image codecs (tiff gets left behind otherwise) // Ah: the TIFF reader in Java 1.5 apparently comes from the UI library: // [Loaded com.sun.imageio.plugins.tiff.TIFFImageReader // from /System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Classes/ui.jar] VUE.init(args); System.out.println(java.util.Arrays.asList(javax.imageio.ImageIO.getReaderFormatNames())); System.out.println(java.util.Arrays.asList(javax.imageio.ImageIO.getReaderMIMETypes())); String filename = args[0]; java.io.File file = null; java.net.URL url = null; Object imageSRC; if (args[0].startsWith("http:") || args[0].startsWith("file:")) imageSRC = url = new java.net.URL(filename); else imageSRC = file = new java.io.File(filename); DEBUG.IMAGE=true; ////getImage(imageSRC, new LWImage()); new Impl no longer an Images.Listener //loadImage(imageSRC, null); /* ImageInputStream iis = ImageIO.createImageInputStream(imageSRC); out("Got ImageInputStream " + iis); java.util.Iterator i = ImageIO.getImageReaders(iis); ImageReader IR = null; int idx = 0; while (i.hasNext()) { ImageReader ir = (ImageReader) i.next(); if (IR == null) IR = ir; out("\tfound ImageReader #" + idx + " " + ir); idx++; } if (IR == null) { out("NO IMAGE READER FOUND FOR " + imageSRC); if (file == null) System.err.println("ImageIO.read got: " + ImageIO.read(url)); else System.err.println("ImageIO.read got: " + ImageIO.read(file)); //System.out.println("Reading " + file); System.exit(0); } out("Chosen ImageReader for stream " + IR + " formatName=" + IR.getFormatName()); IR.addIIOReadProgressListener(new ReadListener()); out("added progress listener"); out("Reading " + IR); IR.setInput(iis); out("Input for reader set to " + iis); //out("meta-data: " + IR.getImageMetadata(0)); out("Getting size..."); int w = IR.getWidth(0); int h = IR.getHeight(0); out("ImageReader got size " + w + "x" + h); BufferedImage bi = IR.read(0); out("ImageReader.read(0) got " + bi); */ /* The below code requires the JAI libraries: // JAI (Java Advandced Imaging) libraries /System/Library/Java/Extensions/jai_core.jar /System/Library/Java/Extensions/jai_codec.jar Using this code below will also get us decoding .fpx images, tho we would need to convert it from the resulting RenderedImage / PlanarImage */ /* try { // Use the ImageCodec APIs com.sun.media.jai.codec.SeekableStream stream = new com.sun.media.jai.codec.FileSeekableStream(filename); String[] names = com.sun.media.jai.codec.ImageCodec.getDecoderNames(stream); System.out.println("ImageCodec API's found decoders: " + java.util.Arrays.asList(names)); com.sun.media.jai.codec.ImageDecoder dec = com.sun.media.jai.codec.ImageCodec.createImageDecoder(names[0], stream, null); java.awt.image.RenderedImage im = dec.decodeAsRenderedImage(); System.out.println("ImageCodec API's got RenderedImage: " + im); Object image = javax.media.jai.PlanarImage.wrapRenderedImage(im); System.out.println("ImageCodec API's got PlanarImage: " + image); } catch (Exception e) { e.printStackTrace(); } // We're not magically getting any new codec's added to ImageIO after the above code // finds the .fpx codec... System.out.println(java.util.Arrays.asList(javax.imageio.ImageIO.getReaderFormatNames())); System.out.println(java.util.Arrays.asList(javax.imageio.ImageIO.getReaderMIMETypes())); */ } } /** * An implementation of <code>ImageInputStream</code> that gets its * input from a regular <code>InputStream</code>. As the data * is read, is it backed by a File for seeking backward. * */ class FileBackedImageInputStream extends ImageInputStreamImpl { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(FileBackedImageInputStream.class); private static final int BUFFER_LENGTH = 2048; private final RandomAccessFile cache; private final byte[] streamBuf = new byte[BUFFER_LENGTH]; private final InputStream stream; private long length = 0L; private boolean foundEOF = false; private final Images.Listener listener; private final File file; // todo opt: pass in content size up front to init RAF to full size at start /** * @param stream - image data stream -- will be closed when reading is done * @param file - file to write incoming data to to use as cache */ public FileBackedImageInputStream(InputStream stream, File file, Images.Listener listener) throws IOException, Images.ImageException { if (stream == null || file == null) throw new IllegalArgumentException("FileBackedImageInputStream: stream or file is null"); this.stream = stream; this.cache = new RandomAccessFile(file, "rw"); this.file = file; this.listener = listener; this.cache.setLength(0); // in case already there if (true) { final byte[] testBuf = new byte[64]; final int got = read(testBuf); super.seek(0); // put as back at the start final String contentHead = new String(testBuf, 0, got, "US-ASCII"); if (DEBUG.IMAGE) { Log.debug(String.format("ContentHead; got=%d; streamPos=%d; length=%d [%s]", got, streamPos, length, contentHead.trim())); // For inspecting largeer content-head's in log debug stream: // compress strings of newlines/whitespaces into single newlines: //contentHead = contentHead.replaceAll("(\\n\\s*)(\\n\\s*)+", "\n"); // or just compress all whitespace down to single spaces: //contentHead = contentHead.replaceAll("\\s+", " "); //Log.debug("CONTENT-HEAD:\n" + contentHead + "\n-------"); } final String trimmed = contentHead.trim(); final String matcher = trimmed.substring(0, Math.min(16, trimmed.length())).toUpperCase(); if (matcher.startsWith("<HTML>") || matcher.startsWith("<!DOCTYPE")) { if (DEBUG.IMAGE) Log.warn("Stream " + stream + " contains HTML, not image data: [" + contentHead.trim() + "]"); else Log.info("Stream contains HTML, not image data: see cache file for HTML sample:\n\t" + file); // DEBUG: we force this readUntil to get more info on the streams that are starting // with <HTML> every once in a while: we can be sure to have a cache file with a bit // of data in it we can inspect afterwords. readUntil(BUFFER_LENGTH); if (DEBUG.IMAGE) { byte[] buf = new byte[BUFFER_LENGTH]; int n = read(buf); super.seek(0); Log.debug("Cache contents:\n"); System.out.println(new String(buf, 0, n, "US-ASCII")); } close(); throw new Images.DataException("Content is HTML, not image data"); } else { readUntil(BUFFER_LENGTH); // debug: same reason as above } // setting this at the end will prevent any gotImageProgress callbacks to any listener // until we at know it's not an HTML data stream //this.listener = listener; } } /* public void seek(long pos) throws IOException { System.err.println("SEEK " + pos); super.seek(pos); } */ /** * Ensures that at least <code>pos</code> bytes are cached, * or the end of the source is reached. The return value * is equal to the smaller of <code>pos</code> and the * length of the source file. */ private long readUntil(long pos) throws IOException { //System.err.println("<=" + pos + "; "); // We've already got enough data cached if (pos < length) return pos; // pos >= length but length isn't getting any bigger, so return it if (foundEOF) return length; long len = pos - length; cache.seek(length); while (len > 0) { // Copy a buffer's worth of data from the source to the cache // BUFFER_LENGTH will always fit into an int so this is safe final int nbytes = stream.read(streamBuf, 0, (int)Math.min(len, (long)BUFFER_LENGTH)); if (nbytes == -1) { if (DEBUG.IMAGE && DEBUG.IO) System.err.println("<EOF @ " + length + ">"); foundEOF = true; return length; } //if (DEBUG.IMAGE && DEBUG.IO) System.err.format("+%4d; ", nbytes); if (DEBUG.IO) Log.debug(String.format("+%4d bytes; %7d total", nbytes, length+nbytes)); cache.write(streamBuf, 0, nbytes); len -= nbytes; length += nbytes; //System.out.println("READ TO " + length); if (listener != null) listener.gotImageProgress(stream, length, -1); } return pos; } public int read() throws IOException { bitOffset = 0; long next = streamPos + 1; long pos = readUntil(next); if (pos >= next) { if (DEBUG.IMAGE && DEBUG.IO) Log.debug("SEEK " + (streamPos+1)); cache.seek(streamPos++); return cache.read(); } else { return -1; } } public int read(byte[] b, int off, int len) throws IOException { if (b == null) throw new NullPointerException(); if (off < 0 || len < 0 || off + len > b.length || off + len < 0) throw new IndexOutOfBoundsException(); if (len == 0) return 0; checkClosed(); bitOffset = 0; long pos = readUntil(streamPos + len); // len will always fit into an int so this is safe len = (int)Math.min((long)len, pos - streamPos); if (len > 0) { if (DEBUG.IMAGE && DEBUG.IO) Log.debug("SEEK " + streamPos); cache.seek(streamPos); cache.readFully(b, off, len); streamPos += len; return len; } else { return -1; } } public void close() throws IOException { super.close(); cache.close(); stream.close(); } public String toString() { return getClass().getName() + "[" + file.toString() + "]"; } }