/* * Copyright 2015 Brandon Borkholder * * Licensed under the Apache 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.apache.org/licenses/LICENSE-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 org.jogamp.glg2d.impl; import static org.jogamp.glg2d.GLG2DRenderingHints.KEY_CLEAR_TEXTURES_CACHE; import static org.jogamp.glg2d.GLG2DRenderingHints.VALUE_CLEAR_TEXTURES_CACHE_DEFAULT; import static org.jogamp.glg2d.GLG2DRenderingHints.VALUE_CLEAR_TEXTURES_CACHE_EACH_PAINT; import static org.jogamp.glg2d.impl.GLG2DNotImplemented.notImplemented; import java.awt.Color; import java.awt.Image; import java.awt.RenderingHints.Key; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ImageObserver; import java.awt.image.RenderedImage; import java.awt.image.VolatileImage; import java.awt.image.renderable.RenderableImage; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.logging.Level; import java.util.logging.Logger; import org.jogamp.glg2d.GLG2DImageHelper; import org.jogamp.glg2d.GLG2DRenderingHints; import org.jogamp.glg2d.GLGraphics2D; import com.jogamp.opengl.util.texture.Texture; import com.jogamp.opengl.util.texture.TextureCoords; import com.jogamp.opengl.util.texture.awt.AWTTextureIO; public abstract class AbstractImageHelper implements GLG2DImageHelper { private static final Logger LOGGER = Logger.getLogger(AbstractImageHelper.class.getName()); /** * See {@link GLG2DRenderingHints#KEY_CLEAR_TEXTURES_CACHE} */ protected TextureCache imageCache = new TextureCache(); protected Object clearCachePolicy; protected GLGraphics2D g2d; protected abstract void begin(Texture texture, AffineTransform xform, Color bgcolor); protected abstract void applyTexture(Texture texture, int dx1, int dy1, int dx2, int dy2, float sx1, float sy1, float sx2, float sy2); protected abstract void end(Texture texture); @Override public void setG2D(GLGraphics2D g2d) { this.g2d = g2d; if (clearCachePolicy == VALUE_CLEAR_TEXTURES_CACHE_EACH_PAINT) { imageCache.clear(); } } @Override public void push(GLGraphics2D newG2d) { // nop } @Override public void pop(GLGraphics2D parentG2d) { // nop } @Override public void setHint(Key key, Object value) { if (key == KEY_CLEAR_TEXTURES_CACHE) { clearCachePolicy = value; } } @Override public void resetHints() { clearCachePolicy = VALUE_CLEAR_TEXTURES_CACHE_DEFAULT; } @Override public void dispose() { imageCache.clear(); } @Override public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { return drawImage(img, AffineTransform.getTranslateInstance(x, y), bgcolor, observer); } @Override public boolean drawImage(Image img, AffineTransform xform, ImageObserver observer) { return drawImage(img, xform, (Color) null, observer); } @Override public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) { double imgHeight = img.getHeight(null); double imgWidth = img.getWidth(null); if (imgHeight < 0 || imgWidth < 0) { return false; } AffineTransform transform = AffineTransform.getTranslateInstance(x, y); transform.scale(width / imgWidth, height / imgHeight); return drawImage(img, transform, bgcolor, observer); } @Override public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) { Texture texture = getTexture(img, observer); if (texture == null) { return false; } float height = texture.getHeight(); float width = texture.getWidth(); begin(texture, null, bgcolor); applyTexture(texture, dx1, dy1, dx2, dy2, sx1 / width, sy1 / height, sx2 / width, sy2 / height); end(texture); return true; } protected boolean drawImage(Image img, AffineTransform xform, Color color, ImageObserver observer) { Texture texture = getTexture(img, observer); if (texture == null) { return false; } begin(texture, xform, color); applyTexture(texture); end(texture); return true; } protected void applyTexture(Texture texture) { int width = texture.getWidth(); int height = texture.getHeight(); TextureCoords coords = texture.getImageTexCoords(); applyTexture(texture, 0, 0, width, height, coords.left(), coords.top(), coords.right(), coords.bottom()); } /** * Cache the texture if possible. I have a feeling this will run into issues * later as images change. Just not sure how to handle it if they do. I * suspect I should be using the ImageConsumer class and dumping pixels to the * screen as I receive them. * * <p> * If an image is a BufferedImage, turn it into a texture and cache it. If * it's not, draw it to a BufferedImage and see if all the image data is * available. If it is, cache it. If it's not, don't cache it. But if not all * the image data is available, we will draw it what we have, since we draw * anything in the image to a BufferedImage. * </p> */ protected Texture getTexture(Image image, ImageObserver observer) { Texture texture = imageCache.get(image); if (texture == null) { BufferedImage bufferedImage; if (image instanceof BufferedImage && ((BufferedImage) image).getType() != BufferedImage.TYPE_CUSTOM) { bufferedImage = (BufferedImage) image; } else { bufferedImage = toBufferedImage(image); } if (bufferedImage != null) { texture = create(bufferedImage); addToCache(image, texture); } } return texture; } protected Texture create(BufferedImage image) { // we'll assume the image is complete and can be rendered return AWTTextureIO.newTexture(g2d.getGLContext().getGL().getGLProfile(), image, false); } protected void destroy(Texture texture) { texture.destroy(g2d.getGLContext().getGL()); } protected void addToCache(Image image, Texture texture) { if (clearCachePolicy instanceof Number) { int maxSize = ((Number) clearCachePolicy).intValue(); if (imageCache.size() > maxSize) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Clearing texture cache with size " + imageCache.size()); } imageCache.clear(); } } imageCache.put(image, texture); } protected BufferedImage toBufferedImage(Image image) { if (image instanceof VolatileImage) { return ((VolatileImage) image).getSnapshot(); } int width = image.getWidth(null); int height = image.getHeight(null); if (width < 0 || height < 0) { return null; } BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); bufferedImage.createGraphics().drawImage(image, null, null); return bufferedImage; } @Override public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { notImplemented("drawImage(BufferedImage, BufferedImageOp, int, int)"); } @Override public void drawImage(RenderedImage img, AffineTransform xform) { notImplemented("drawImage(RenderedImage, AffineTransform)"); } @Override public void drawImage(RenderableImage img, AffineTransform xform) { notImplemented("drawImage(RenderableImage, AffineTransform)"); } /** * We could use a WeakHashMap here, but we want access to the ReferenceQueue * so we can dispose the Textures when the Image is no longer referenced. */ @SuppressWarnings("serial") protected class TextureCache extends HashMap<WeakKey<Image>, Texture> { private ReferenceQueue<Image> queue = new ReferenceQueue<Image>(); public void expungeStaleEntries() { Reference<? extends Image> ref = queue.poll(); while (ref != null) { Texture texture = remove(ref); if (texture != null) { destroy(texture); } ref = queue.poll(); } } public Texture get(Image image) { expungeStaleEntries(); WeakKey<Image> key = new WeakKey<Image>(image, null); return get(key); } public Texture put(Image image, Texture texture) { expungeStaleEntries(); WeakKey<Image> key = new WeakKey<Image>(image, queue); return put(key, texture); } } protected static class WeakKey<T> extends WeakReference<T> { private final int hash; public WeakKey(T value, ReferenceQueue<T> queue) { super(value, queue); hash = value.hashCode(); } @Override public int hashCode() { return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } else if (obj instanceof WeakKey) { WeakKey<?> other = (WeakKey<?>) obj; return other.hash == hash && get() == other.get(); } else { return false; } } } }