/* $Id: DeferredBufferedImage.java 17820 2010-01-12 18:42:20Z linus $ ***************************************************************************** * Copyright (c) 2009 Contributors - see below * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * tfmorris ***************************************************************************** * * Some portions of this file was previously release using the BSD License: */ // Copyright (c) 2008 The Regents of the University of California. All // Rights Reserved. Permission to use, copy, modify, and distribute this // software and its documentation without fee, and without a written // agreement is hereby granted, provided that the above copyright notice // and this paragraph appear in all copies. This software program and // documentation are copyrighted by The Regents of the University of // California. The software program and documentation are supplied "AS // IS", without any accompanying services from The Regents. The Regents // does not warrant that the operation of the program will be // uninterrupted or error-free. The end-user understands that the program // was developed for research purposes and is advised not to rely // exclusively on the program for any reason. IN NO EVENT SHALL THE // UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, // SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, // ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF // THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE // PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF // CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, // UPDATES, ENHANCEMENTS, OR MODIFICATIONS. package org.argouml.gefext; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.util.Vector; import org.apache.log4j.Logger; import org.tigris.gef.base.Editor; /** * Rendered GEF image which uses a band buffer to minimize memory usage for * high resolution images. Image is rendered on demand in multiple passes * as getData is called. * <p> * NOTE: Only the methods which are called by ImageIO.write have been tested * and proven to work (getMinX/Y, getWidth, getHeight, & getData). This is * <em>not</em> a general purpose RenderedImage implementation, so you are on * your own with any others. * * @author Tom Morris <tfmorris@gmail.com> * @since ArgoUML 0.26 */ public class DeferredBufferedImage implements RenderedImage { private static final Logger LOG = Logger.getLogger(DeferredBufferedImage.class); /** * RGB values for background color for image. Set as transparent. Chosen * because it's unlikely to be selected by the user, and leaves the diagram * readable if viewed without transparency. */ public static final int TRANSPARENT_BG_COLOR = 0x00efefef; /** * Background color */ public static final Color BACKGROUND_COLOR = new Color(TRANSPARENT_BG_COLOR, true); private static final int BUFFER_HEIGHT = 32; private int x, y; private int width; private int height; private int scale; private BufferedImage image; private Editor editor; private int scaledBufferHeight; private int y1, y2; /** * Construct a new DeferredBufferedImage which will use GEF to render the * current diagram on demand. * * @param drawingArea bounding rectangle of the area to be drawn * @param imageType Type of image to be created (e.g. TYPE_INT_ARGB). Must * be on an image type which is supported by BufferedImage * @param ed GEF editor to be used for rendering the image * @param scaleFactor Integer scale factor to multiply by when rendering * image. */ public DeferredBufferedImage(Rectangle drawingArea, int imageType, Editor ed, int scaleFactor) { editor = ed; scale = scaleFactor; x = drawingArea.x; y = drawingArea.y; width = drawingArea.width; height = drawingArea.height; // Scale everything up x = x * scale; y = y * scale; width = width * scale; height = height * scale; scaledBufferHeight = BUFFER_HEIGHT * scale; // Create our bandbuffer which is just a small slice of the image // TODO: We used a fixed height buffer now, but we could be smarter and // compute a height which would fit in some memory budget, allowing us // to use taller buffers with narrower images, minimizing the overhead // of multiple rendering passes image = new BufferedImage(width, scaledBufferHeight, imageType); // Initialize band buffer bounds y1 = y; y2 = y1; } public Raster getData() { LOG.debug("getData with no params"); return getData(new Rectangle(x, y, width, height)); } public Raster getData(Rectangle clip) { // LOG.debug("getData Rectangle = " + clip); if (!isRasterValid(clip)) { LOG.debug("Raster not valid, computing new raster"); computeRaster(clip); } Rectangle oClip = offsetWindow(clip); Raster ras = image.getData(oClip); Raster translatedRaster = ras.createTranslatedChild(clip.x, clip.y); // LOG.debug("getData returning raster = " + translatedRaster); return translatedRaster; } /** * @param clip clipping rectangle to test * @return true if clip rectangle is completely contained in image raster */ private boolean isRasterValid(Rectangle clip) { if (clip.height > scaledBufferHeight) { throw new IndexOutOfBoundsException( "clip rectangle must fit in band buffer"); } return (clip.y >= y1 && (clip.y + clip.height - 1) < y2); } /** * Rasterize the next slice in the band buffer. Raster will be the full * width of the image and based vertically at the Y value of the current * clip rectangle. * * @param clip clip rectangle of interest */ private void computeRaster(Rectangle clip) { LOG.debug("Computing raster for rectangle " + clip); // Create a new graphics context so we start with fresh transforms Graphics2D graphics = image.createGraphics(); graphics.scale(1.0 * scale, 1.0 * scale); // Fill with our background color graphics.setColor(BACKGROUND_COLOR); Composite c = graphics.getComposite(); graphics.setComposite(AlphaComposite.Src); graphics.fillRect(0, 0, width, scaledBufferHeight); graphics.setComposite(c); // Translate & clip graphic to match region of interest graphics.setClip(0, 0, width, scaledBufferHeight); graphics.translate(0, -clip.y / scale); y1 = clip.y; y2 = y1 + scaledBufferHeight; // Ask GEF to print a band of the diagram (translated & clipped) editor.print(graphics); // Make sure it isn't caching anything that should be written graphics.dispose(); } /** * Return a new clip rectangle which is offset by the base of our band * buffer. * * @param clip clipping rectangle * @return adjusted clipping rectangle */ private Rectangle offsetWindow(Rectangle clip) { int baseY = clip.y - y1; return new Rectangle(clip.x, baseY, clip.width, Math.min(clip.height, scaledBufferHeight - baseY)); } /** * Not implemented * * {@inheritDoc} * @see java.awt.image.RenderedImage#copyData(java.awt.image.WritableRaster) */ public WritableRaster copyData(WritableRaster outRaster) { throw new UnsupportedOperationException(); // This needs to iterate to fill entire output raster if implemented // return image.copyData(outRaster); } /** * Not implemented. * {@inheritDoc} * * @see java.awt.image.RenderedImage#getSources() */ public Vector<RenderedImage> getSources() { return null; } public ColorModel getColorModel() { return image.getColorModel(); } public int getMinX() { LOG.debug("getMinX = 0"); return 0; } public int getMinY() { LOG.debug("getMinY = 0"); return 0; } public int getMinTileX() { LOG.debug("getMinTileX = 0"); return 0; } public int getMinTileY() { LOG.debug("getMinTileY = 0"); return 0; } public int getNumXTiles() { LOG.debug("getNumXTiles = 1"); return 1; } /** * Untested. Not guaranteed to work. * {@inheritDoc} * * @see java.awt.image.RenderedImage#getNumYTiles() */ public int getNumYTiles() { int tiles = (getHeight() + scaledBufferHeight - 1) / scaledBufferHeight; LOG.debug("getNumYTiles = " + tiles); return tiles; } public Object getProperty(String name) { return image.getProperty(name); } public String[] getPropertyNames() { return image.getPropertyNames(); } public SampleModel getSampleModel() { return image.getSampleModel(); } /** * Untested. Not guaranteed to work. * {@inheritDoc} * * @see java.awt.image.RenderedImage#getNumYTiles() */ public Raster getTile(int tileX, int tileY) { LOG.debug("getTile x=" + tileX + " y = " + tileY); if (tileX < getMinTileX() || tileX >= getMinTileX() + getNumXTiles() || tileY < getMinTileY() || tileY >= getMinTileY() + getNumYTiles()) { throw new IndexOutOfBoundsException(); } // FIXME: Boundary condition at end of image for non-integral // multiples of BUFFER_HEIGHT Rectangle tileBounds = new Rectangle(0, (tileY - getMinTileY() * scaledBufferHeight), width, scaledBufferHeight); return getData(tileBounds); } public int getTileGridXOffset() { LOG.debug("getTileGridXOffset = 0"); return 0; } public int getTileGridYOffset() { LOG.debug("getTileGridYOffset = 0"); return 0; } public int getTileWidth() { LOG.debug("getTileWidth = " + width); return width; } public int getTileHeight() { LOG.debug("getTileHeight = " + scaledBufferHeight); return scaledBufferHeight; } public int getWidth() { LOG.debug("getWidth = " + width); return width; } public int getHeight() { LOG.debug("getHeight = " + height); return height; } }