/* * 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 java.lang.ref.*; import java.awt.Image; import java.awt.Graphics2D; import java.awt.Color; import java.awt.geom.AffineTransform; import tufts.vue.Images.Handle; /** * A representation of an image that can allow itself to be garbage collected, and reconstituted * later if needed. */ // This would be better as part of an Images or Media package. The package-private methods // are intended for use by ImageRef. public abstract class ImageRep implements /*ImageRef.Rep,*/ Images.Listener { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(ImageRep.class); static final int[] ZERO_SIZE = new int[2]; private static interface Ref<T> { T get(); boolean isLoader(); } //=================================================================================================== private static final Ref IMG_UNLOADED = new NullRef("UNLOADED"); private static final Ref IMG_CACHING = new NullRef("CACHING"); private static final Ref IMG_LOADING = new LoadingRef("LOADING"); private static final Ref IMG_LOADING_AFTER_ERROR = new LoadingRef("LOADING-POST-ERROR"); private static final Ref IMG_ERROR = new NullRef("ERROR"); private static final Ref IMG_ERROR_MEMORY = new NullRef("LOW_MEMORY"); //=================================================================================================== private volatile Ref<Image> _handle = IMG_UNLOADED; // note: could make use of AtomicReference v.s. volatile /** pixel width and height of this representation _size[0] is width, _size[1] is height */ private int[] _size = ZERO_SIZE; private final ImageRef _ref; private final ImageSource _data; // note: could pull from _ref when needed, at least for original source //=================================================================================================== static ImageRep create(ImageRef ref, ImageSource src, boolean allowGC) { if (allowGC) return new ImageRep.Soft(ref, src); else return new ImageRep.Hard(ref, src); } static ImageRep create(ImageRef ref, ImageSource src) { return create(ref, src, true); } private static final class Hard extends ImageRep { Hard(ImageRef ref, ImageSource src) { super(ref, src); } Ref newRef(Image o) { if (o == null) Log.warn("HardRef to null " + this, new Throwable("HERE")); return new HardRef(o); } } private static final class Soft extends ImageRep { Soft(ImageRef ref, ImageSource src) { super(ref, src); } Ref newRef(Image o) { return new SoftRef(o); } } public static final ImageRep UNAVAILABLE = new ImageRep() { @Override public boolean available() { return false; } @Override protected Image image() { return null; } @Override protected boolean reconstitute() { error(); return false; } @Override protected Handle reconstitute(Object when) { error(); return null; } @Override protected void cacheData(Images.Handle i, Object debug) { error(); } @Override void renderRep(Graphics2D g, float width, float height) { fillRect(g, width, height, DEBUG.Enabled ? Color.orange : LoadingColor); } @Override public String toString() { return "REP.UNAVAILABLE"; } @Override Ref newRef(Image o) { return (Ref) error(); } private Object error() { throw new Error("constant-class"); } }; //=================================================================================================== private ImageRep(ImageRef ref, ImageSource src) { if (src == null) throw new Error("no ImageSource constructing rep for " + ref); _data = src; _ref = ref; } // private ImageRep(ImageRef ref, Image image, ImageSource src) { // if (src == null) // throw new Error("no ImageSource constructing rep for " + ref); // _data = src; // _ref = ref; // if (image != null) { // cacheData(image, false); // } // } private ImageRep() { // for UNAVAILABLE _data = null; _ref = null; } abstract Ref<Image> newRef(Image o); private synchronized void setSize(final int w, final int h) { if (w <= 0 || h <= 0) { Log.warn("bad size " + w + "x" + h); _size = ZERO_SIZE; } else { if (_size == ZERO_SIZE || w != _size[0] || h != _size[1]) { final int[] size = new int[2]; size[0] = w; size[1] = h; takeSize(size); } } } synchronized void takeSize(int[] size) { _size = size; } // todo: could change _size to volatile (and fetch into a final in methods that make 2 refs to it) // can only guarantee width/height coherency by fetching them together in a singal sync synchronized int[] size() { return _size; // cloning would be even safer, tho this is not for general consumption } synchronized int area() { return _size[0] * _size[1]; } synchronized float aspect() { return (float) _size[0] / (float) _size[1]; } public boolean available() { return get(_handle) != null; } public boolean loading() { return _handle.isLoader(); } public boolean hasError() { return _handle == IMG_ERROR || _handle == IMG_ERROR_MEMORY; } Ref handle() { return _handle; } // Note: old DEADLOCK if reconstitute was synchronized: // AWT: ImageRep locks in reconstitute, calling getImage, which is trigger a sync callback // through synchronized CachingRelayer (addListener) ImageProcessor: CachingRelayer gotImage // locks CachingRelayer, which after calling gotImage to notifyRepHasArrived attempts to pull // ImageRep aspect, which is locked above on AWT on reconstitute. Doesn't appear to // be a problem now, but take heed. private static final Object LOAD_NORMAL = "painting"; private static final Object LOAD_IMMEDIATE = "immediate"; private static final Object LOAD_CACHE = "cache"; /** @return true if all data was immediately available, false if we're waiting for more info from a callback */ protected boolean reconstitute() { return reconstitute(LOAD_NORMAL) != IS_WAITING; } protected void requestCaching() { reconstitute(LOAD_CACHE); } private static final Handle AT_ERROR = Handle.emptyInstance(); private static final Handle IS_WAITING = Handle.emptyInstance(); protected synchronized Handle reconstitute(final Object when) { if (_handle == IMG_ERROR) { // if this was an OutOfMemoryError (a potentially recoverable error), we allow us to retry indefinitely if (DEBUG.IMAGE) debug("skipping reconstitue: last load had error: " + this); return AT_ERROR; } final boolean wasAtError = (_handle == IMG_ERROR_MEMORY); // why only memory? //if (DEBUG.IMAGE) Log.debug(Util.TERM_CYAN + "RECONSTITUTE " + Util.TERM_CLEAR + _data, new Throwable("HERE")); if (_handle.isLoader()) { if (DEBUG.IMAGE) debug("recon: already loading: " + _data);//, new Throwable("HERE")); return IS_WAITING; } if (DEBUG.IMAGE) debug(Util.TERM_CYAN + "RECONSTITUTE(" + when + ") " + Util.TERM_CLEAR + _data); final Ref oldHandle = _handle; final Images.Handle imageData; if (when == LOAD_IMMEDIATE) { // LOAD THE IMAGE SYNCHRONOUSLY, BLOCKING THE CURRENT THREAD IF NEEDED: // Due to the ImageRep impl using cacheData for result capture, the listener // can be null for this call -- we don't need the listener callbacks if we // know we'll immediately have a result. imageData = Images.getImageImmediately(_data, null); if (imageData == null) Log.warn("null on immediate request: " + this); } else if (when == LOAD_CACHE) { if (_handle == IMG_CACHING) { //Log.info("repeated cache request"); return IS_WAITING; } // Note, do NOT want callbacks (we're not providing a listener), as we don't // want to be issuing repaint updates for images that were only requested to // be cached. However, in case this IS later requested to draw, we DO // need the listener. So for now we do listen and suffer the extra repaints. // What we really need is to track our state as to CACHING v.s. LOADING, and // only issue callbacks when an image arrives if we're LOADING. What the means // tho is being able to upgrade our state from CACHING to LOADING if a paint // request comes in for us while we're already in queue for caching. imageData = Images.cacheImage(_data, this); if (imageData == null) { // as expected: setHandle(IMG_CACHING, "cacheRequest"); return IS_WAITING; } // Result will often be null, as expected, but if it's available, we'll // want to QUIETLY load it w/out triggering callbacks / notifications. } else { // LOAD_NORMAL if (DEBUG.Enabled && when != LOAD_NORMAL) Log.error("bad recon code: " + Util.tags(when)); imageData = Images.getImageHandle(_data, this); } if (imageData != null) { // Record the image data and update our status: cacheData(imageData, when); // Old comment: if we knew the return value of reconstitute was attented to we // could potentially skip the notify that's always made in cacheData. return imageData; } //---------------------------------------------------------------------------------------- // There was no image data immediately available: make sure our status // is set to some version of "loading" //---------------------------------------------------------------------------------------- if (_handle == oldHandle) { // As expected: _handle hasn't changed, as we had no result (only null returned) // (Why, EXACTLY, might _handle change here?) if (wasAtError) { setHandle(IMG_LOADING_AFTER_ERROR, "recon-kicked:" + when); } else { // Note: this may also be upgrading our status from CACHING to LOADING: setHandle(IMG_LOADING, "recon-kicked:" + when); } } else { // No data was returned, and yet, our _handle has changed anyway! // This can happen if a loader has completed and has it's image, but it hasn't left // the cache yet, so our getImage call returned null, but the image content was // still available and was delivered immediately as part of partial results. // That is, the result came via immediate callback, even though the return was null. // Images now checks for this, tho we're leaving this check in just in case. if (DEBUG.Enabled) debug("*** got immediate callback w/image, handle is now " + _handle); } return IS_WAITING; // is this really true for both above cases? } private void debug(String s) { debug(s, false); } private void debug(String s, boolean dumpStack) { String msg = String.format("%08x[%s] %s", System.identityHashCode(this), ImageRef.debugSRC(_data), s); if (dumpStack) Log.debug(msg, new Throwable("HERE")); else Log.debug(msg); } private void setHandle(Ref r, String debug) { if (DEBUG.IMAGE||DEBUG.WORK) { String t; if (r instanceof NullRef && !(r instanceof ProgressRef)) t = r.toString(); else t = Util.tags(r); debug("setHandle " + t + "; " + debug, false); } _handle = r; } protected void cacheData(Images.Handle imageData, Object cause) { final Image image = imageData.image; if (image() == image) { if (DEBUG.IMAGE||DEBUG.WORK) debug(" re-cache:"+cause); return; } if (_data.readable instanceof Image) { // In case this was a runtime generated icon, we MUST be certian to // null any original raw image data in the source to allow for GC. _data.readable = null; } synchronized (this) { // Recording the size here should be redundant to the gotImageSize we've already received, // tho there are some special cases where that call may not arrive (e.g., icon generation). // Note: multi-threaded coherency agaist AWT thread for the next 3 stores, // as well as locked against cacheData calls happening in AWT via reconstitute setSize(image.getWidth(null), image.getHeight(null)); // record the new width & height first before installing the handle just in case setHandle(newRef(image), "[cacheData/"+cause+"]"); } // (at this point we could null local stack image var for potential GC help) // We do not include the below notify in the sync. AWT blocks we're avoiding: // If the parent ImageRef uses this arrived rep to generate an icon, that could // take a while, and if this is an image processing thread (the common case), // any local class syncs in the render code (renderRep) will block until this // thread runs out, hanging the AWT until then. //if (true /*notify*/) { if (cause != LOAD_CACHE) { // we send the image as an argument so there's at least a temporary // guaranteed, hard, non-GC-able reference to it in case the ImageRef wants // to do something with the image data. // TODO: this is overkill if this was for a request during a paint, and the // cache already had the content! We could do the paint immediatley. This // is an mainly an issue only with multiple-maps tho -- the first paint of a // new tab, even if all the images are loaded, will need to update all the // image refs to the loaded state. Currently, they make a default getImage // call, which immediately makes a callback to here with the result, and // then a second call to cacheData with the returned result. All that // points to another reason for an Images cache that holds ImageRef's // directly. _ref.notifyRepHasArrived(this, imageData); } } //public void gotImageUpdate(Object key, Images.Progress p) {} public void gotImageSize(Object imageSrc, int w, int h, long bytes, int[] ss) { setSize(w, h); // could issue a special notifyRepHasProgress with a cause token } public void gotImage(Object imageSrc, Images.Handle imageData) { // note: coherency control point against AWT (this an image processing thread) // could there ever be a problem if this runs while an AWT thread is issuing // a reconstitue call at the same time? cacheData(imageData, "gotImage"); } public void gotImageProgress(Object imageSrc, long bytesSoFar, float pct) { // Currently, only the full rep can report progress (if it's loading over the network) -- // icon generation doesn't report progress -- that may be possible someday via our own // tracking ImageProducer/Consumer for the down-scaling filter, but that's alot of work for // something that's usually pretty quick, and for which we normally get to see the full-rep // drawn on-screen while it's happening. final Ref handle = _handle; if (handle.getClass() == ProgressRef.class) { if (((ProgressRef)handle).trackProgress(pct)) { _ref.notifyRepHasProgress(this, pct); } } else if (handle.isLoader() && pct > 0 /*&& pct < Float.POSITIVE_INFINITY*/) { // todo: should never see infinity setHandle(new ProgressRef(pct), "newProgress"); _ref.notifyRepHasProgress(this, pct); } else { Log.warn("got image progress w/non-loading status: " + this); } } public void gotImageError(Object imageSrc, String msg) { // todo: distinguish between recoverable v.s. non-recoverable (e.g. OutOfMemory v.s. no image file) if (msg == Images.OUT_OF_MEMORY) { setHandle(IMG_ERROR_MEMORY, "gotMemoryError"); } else { setHandle(IMG_ERROR, "gotError"); } if (_ref == null) Log.warn("rep w/out null ref: " + this + "; error=" + msg); else _ref.notifyRepHasArrived(this, null); //_ref.notifyRepHasProgress(this, -1); // force a repaint (don't: can create thrashing loop us during low-memory conditions) } private Image get(final Ref<Image> handle) { final Image image = handle.get(); if (image == null) { if (handle.getClass() == SoftRef.class) { if (DEBUG.Enabled) Log.debug("GC'd: " + _data.original); Images.setLowMemory("image-data-GC"); setHandle(IMG_UNLOADED, "GC'd"); // don't do this if want to know of an EXPIRED state } return null; } else { return image; } } /** @return the image, if currently available */ protected Image image() { return get(_handle); } Image getImageBlocking() { final Image image = image(); if (image == null) { final Handle rawHandle = reconstitute(true); if (rawHandle == null || rawHandle.image == null) { return null; } else { return rawHandle.image; } } else return image; } /** LoadingColor chosen as what has best chance of presenting some contrast against all backgrounds */ private static final Color LoadingColor = new Color(128,128,128,128); private static final Color ErrorColor = new Color(255,0,0,128); private static final Color LowMemoryColor = new Color(0,255,255,128); /** * Draw the representation into the given width/height with floating point scaling * resolution. If the rep is not available, it will draw a transparent box, and * kick off reconstituting of the representation. */ void renderRep(Graphics2D g, float toWidth, float toHeight) { final Ref<Image> handle = _handle; // fetch the volatile final Image image = get(handle); if (image == null) { drawUnavailable(g, toWidth, toHeight); // if (!handle.isLoader()) // reconstitute(); // could pass handle as arg to improve coherency? Or would we just miss updates we want to see? } else { ImageRef.renderImage(g, image, toWidth, toHeight); } } // void renderImage(Graphics2D g, Image image, float toWidth, float toHeight) { // final int[] size = size(); // final float pixelsWide = size[0]; // final float pixelsTall = size[1]; // g.drawImage(image, // AffineTransform.getScaleInstance(toWidth / pixelsWide, // toHeight / pixelsTall), // null); // } boolean isFading() { return false; // can't test for this w/out using a ReferenceQueue -- will always be false //return _handle instanceof SoftReference && ((SoftReference)_handle).isEnqueued(); } boolean isTrackingProgress() { return _handle.getClass() == ProgressRef.class; } protected void drawUnavailable(Graphics2D g, float width, float height) { final Ref handle = _handle; if (handle.getClass() == ProgressRef.class) { drawPartialProgress(g, ((ProgressRef)handle).progress, width, height); } else { drawStatus(g, width, height, handle); } } private void drawStatus(Graphics2D g, float width, float height, Object status) { if (DEBUG.BOXES||DEBUG.IMAGE) debug("DRAWING STATUS " + status); if (status == IMG_ERROR) { g.setColor(ErrorColor); } else if (status == IMG_ERROR_MEMORY || status == IMG_LOADING_AFTER_ERROR) { g.setColor(LowMemoryColor); } else if (DEBUG.Enabled) { if (status == IMG_LOADING) g.setColor(LoadingColor); else g.setColor(Color.green); // no status yet } else { g.setColor(LoadingColor); } g.fillRect(0, 0, (int)width, (int)height); // okay if not a sub-pixel-perfect fill ImageRef.drawBetterRepAvailableIndicator(g, width, height); } private static void fillRect(Graphics2D g, float width, float height, Color c) { g.setColor(c); g.fillRect(0, 0, (int)width, (int)height); } private void drawPartialProgress(Graphics2D g, float progress, float width, float height) { // final int split = (int) (width * pct); // dc.g.setColor(getLoadedColor(dc)); // dc.g.fillRect(0, 0, split, height); // dc.g.setColor(getEmptyColor(dc)); // dc.g.fillRect(split, 0, width - split, height); //Log.debug("drawPartialProgress " + progress); final java.awt.geom.Rectangle2D.Float r = new java.awt.geom.Rectangle2D.Float(); final float split = width * progress; g.setColor(Color.darkGray); r.setRect(0,0, split, height); g.fill(r); g.setColor(Color.gray); r.setRect(split,0, width - split, height); g.fill(r); } @Override public String toString() { //return "ImageRep[" + Util.tags(_handle.get()) + ", rs=" + _data + "]"; // Fetch handle & contents once so don't have to worry about threading inconsistencies final Ref handle = _handle; final Object ptr = get(handle); return String.format("ImageRep@%08x[%s,%s %4dx%-4d %s]", System.identityHashCode(this), //state(handle, ptr), handle == null ? "<<<BAD HANDLE>>>" : handle, ptr == null ? "" : (" " + Util.tags(ptr) + ","), _size[0], _size[1], _data == null ? "NULL" : _data); //_data == null ? "NULL" : _data.original); // os=original-source } // do not confuse the below as having anything to do with ImageRef's -- these are just // pointer handle impls entirely encapsulated in ImageRep, which is used by ImageRef. private static final class HardRef<T> implements Ref<T> { final T o; HardRef(T o) { this.o = o; } /*@Override*/ public T get() { return o; } /*@Override*/ public boolean isLoader() { return false; } @Override public String toString() { return "HARD"; } } // Note: garbage collection slows relative to the total number of // Reference objects in the runtime at any given time. Probably // not by that much, but something to keep in mind. private static final class SoftRef<T> extends SoftReference<T> implements Ref<T> { SoftRef(T o) { super(o); } @Override public String toString() { if (get() == null) return "SOFT(GC'd)"; else return "SOFT"; } /*@Override*/ public boolean isLoader() { return false; } } private static class NullRef implements Ref { final String type; NullRef(String s) { type = s; } /*@Override*/ public Object get() { return null; } /*@Override*/ public boolean isLoader() { return false; } @Override public String toString() { return type; } } private static class LoadingRef extends NullRef { LoadingRef(String s) { super(s); } @Override public boolean isLoader() { return true; } } private static final class ProgressRef extends LoadingRef { private static final float MAX_REPORTS = 128; // at 100% view, 1 pixel per update on a wide-aspect 128px icon private int lastReport = -1; private volatile float progress; /** @return true if the change from the last update is worth reporting downstream */ boolean trackProgress(final float percent) { // as downstream reports of this progress are going to result in repaint // requests, limit the number of reports we can maximally make. An // interesting impl would be to handle this in ImageRef where we're already // figuring the on-screen pixel resolution, and we could issue reports // whenever a slice change would result in a visible progress pixel. progress = percent; final int slices = (int) (percent * MAX_REPORTS); //Log.debug("track progress int=" + slice + " pct " + percent); if (slices > lastReport) { lastReport = slices; return true; } else return false; } float progress() { return progress; } ProgressRef(float first) { super("LOADING%"); progress = first; } } } // final class LoaderRep implements Images.Listener { // implements Rep or extends ImageRep? if extends, can do w/out interface // boolean available() { return false; } // boolean loading() { return true; } // boolean isTrackingProgress() { return false; } // happens only after size arrives // boolean hasError() { return _hadError; } // int width() { return -1; } // int height() { return -1; } // void renderRep(Graphics2D g, float toW, float toH) { /*static drawUnavailable*/ } // final ImageRef _ref; // final ImageSource _src; // ImageRep _concreteRep; // boolean _hadError; // LoaderRep(ImageRef ref, ImageSource is) { // _ref = ref; // _src = is; // } // void reconstitute() { // final Image image = Images.getImage(_src, this); // // no override? need to make sure isn't called from AWT once a rep is in replacement critical section // // need to reproduce semantics of ImageRep.reconstitute? // } // public void gotImageSize(Object imageSrc, int w, int h, long byteSize) { // // _concreteRep = new ImageRep(imageSrc, w, h); // need hard/soft flag // // _ref.notifyRepIsReplaced(this, _concreteRep); // // note: base rep would still need size replacing code for on-disk image changes & reloads during runtime // } // public void gotImage(Object imageSrc, Image image, ImageRef ref) { // _concreteRep.gotImage(imageSrc, image, ref); // } // public void gotImageProgress(Object imageSrc, long bytesSoFar, float pct) { // _concreteRep.gotImageProgress(imageSrc, bytesSoFar, pct); // } // public void gotImageError(Object imageSrc, String msg) { // // Go to error state if had early error (e.g., file not found), otherwise relay // if (_concreteRep == null) // _hadError = true; // else // _concreteRep.gotImageError(imageSrc, msg); // } // } // What if the size of the underlying image actually DOES change tho? E.g., a new // version of it has been written to disk over the old one by the user. We'd need to be // ready to issue a new concrete rep in that case as well, tho that should actually be // no big deal as long as we've got a standard method of replacing a rep. Hell, // actually, we could use that code as the STANDARD way of updating the rep as well! // Then we have no ProtoRep at all -- just bootstrapping ImageReps, old-size ImageReps, // and currently sized ImageReps. // The reason to provide a new rep as soon as we have the size is for early size // recording, tho we don't even do that now, except incidentally by updating the // width/height volatiles, which will only effect any progress reports till we push some // kind of event. //=================================================================================================== // Note: subclassing java.awt.Image is not actually supported; in AWT Graphics drawing: // SurfaceManager can't get surface. see // http://forums.sun.com/thread.jspa?threadID=5208043 -- basically, this is a 10+ year // old bug that's never been fixed -- either java.awt.Image needs to be abstract or the // impls need changing, and it looks like there's just no hope on this one at all. // Subclassing BufferedImage MAY be possible tho... this would lock us in 100% to never // using ToolkitImage's, tho we can probably live with that as that impl gives us memory // heartburn, at least on the Mac. The bigger issue is we'd like to be able to rely on // using createCompatibleImage for creating our BufferedImages, and not have to create // our own delegating instances.