Author: tfmorris
Date: 2008-05-20 16:25:31-0700
New Revision: 14781
Added:
trunk/src/argouml-app/src/org/argouml/gefext/DeferredBufferedImage.java (contents, props changed)
Modified:
trunk/src/argouml-app/src/org/argouml/uml/ui/SaveGraphicsManager.java
Log:
Issue 3535 - Add band buffer renderer to optimize memory use
Added: trunk/src/argouml-app/src/org/argouml/gefext/DeferredBufferedImage.java
Url: http://argouml.tigris.org/source/browse/argouml/trunk/src/argouml-app/src/org/argouml/gefext/DeferredBufferedImage.java?view=auto&rev=14781
==============================================================================
--- (empty file)
+++ trunk/src/argouml-app/src/org/argouml/gefext/DeferredBufferedImage.java 2008-05-20 16:25:31-0700
@@ -0,0 +1,338 @@
+// $Id$
+// 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 <[email protected]>
+ * @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 static final int MARGIN = 10;
+
+ 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;
+
+ // We're going to draw from the origin, so adjust width and height
+ // Also add a margin to the bottom and right, because GEF sets the
+ // bounding box very tight
+ width = width + x + MARGIN;
+ x = 0;
+ height = height + y + MARGIN;
+ y = 0;
+
+ // 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
+ 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) < 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;
+ }
+}
Modified: trunk/src/argouml-app/src/org/argouml/uml/ui/SaveGraphicsManager.java
Url: http://argouml.tigris.org/source/browse/argouml/trunk/src/argouml-app/src/org/argouml/uml/ui/SaveGraphicsManager.java?view=diff&rev=14781&p1=trunk/src/argouml-app/src/org/argouml/uml/ui/SaveGraphicsManager.java&p2=trunk/src/argouml-app/src/org/argouml/uml/ui/SaveGraphicsManager.java&r1=14780&r2=14781
==============================================================================
--- trunk/src/argouml-app/src/org/argouml/uml/ui/SaveGraphicsManager.java (original)
+++ trunk/src/argouml-app/src/org/argouml/uml/ui/SaveGraphicsManager.java 2008-05-20 16:25:31-0700
@@ -25,23 +25,27 @@
package org.argouml.uml.ui;
import java.awt.Rectangle;
+import java.awt.image.BufferedImage;
+import java.awt.image.RenderedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
+import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.SwingUtilities;
+import org.apache.log4j.Logger;
import org.argouml.configuration.Configuration;
import org.argouml.configuration.ConfigurationKey;
+import org.argouml.gefext.DeferredBufferedImage;
import org.argouml.i18n.Translator;
import org.argouml.util.FileFilters;
import org.argouml.util.SuffixFilter;
@@ -104,7 +108,7 @@
/**
* The list of other file formats.
*/
- private List otherFilters = new ArrayList();
+ private List<SuffixFilter> otherFilters = new ArrayList<SuffixFilter>();
/**
* The singleton instance.
@@ -129,9 +133,7 @@
* @param suffix the extension of the new default file-format
*/
public void setDefaultFilterBySuffix(String suffix) {
- Iterator i = otherFilters.iterator();
- while (i.hasNext()) {
- SuffixFilter sf = (SuffixFilter) i.next();
+ for (SuffixFilter sf : otherFilters) {
if (sf.getSuffix().equalsIgnoreCase(suffix)) {
setDefaultFilter(sf);
break;
@@ -152,13 +154,10 @@
KEY_DEFAULT_GRAPHICS_FILTER,
f.getSuffix());
- Collections.sort(otherFilters, new Comparator() {
- /*
- * @see java.util.Comparator#compare(T, T)
- */
- public int compare(Object arg0, Object arg1) {
- return ((SuffixFilter) arg0).getSuffix().compareToIgnoreCase(
- ((SuffixFilter) arg1).getSuffix());
+ Collections.sort(otherFilters, new Comparator<SuffixFilter>() {
+ public int compare(SuffixFilter arg0, SuffixFilter arg1) {
+ return arg0.getSuffix().compareToIgnoreCase(
+ arg1.getSuffix());
}
});
}
@@ -323,7 +322,8 @@
}
return cmd;
}
-
+
+
/**
* @param suffix the suffix (extension) of the filename,
* which corresponds to the graphics format to be used
@@ -334,9 +334,10 @@
if (FileFilters.PS_FILTER.getSuffix().equals(suffix)) {
cmd = new SavePSAction(Translator.localize("action.save-ps"));
} else if (FileFilters.EPS_FILTER.getSuffix().equals(suffix)) {
- cmd = new SaveEPSAction(Translator.localize("action.save-eps"));
+ cmd = new SaveScaledEPSAction(
+ Translator.localize("action.save-eps"));
} else if (FileFilters.PNG_FILTER.getSuffix().equals(suffix)) {
- cmd = new SavePNGAction(Translator.localize("action.save-png"));
+ cmd = new SavePNGAction2(Translator.localize("action.save-png"));
} else if (FileFilters.GIF_FILTER.getSuffix().equals(suffix)) {
cmd = new SaveGIFAction(Translator.localize("action.save-gif"));
} else if (FileFilters.SVG_FILTER.getSuffix().equals(suffix)) {
@@ -350,19 +351,16 @@
* @return the complete collection of SuffixFilters,
* the first one is the default one
*/
- public Collection getSettingsList() {
- Collection c = new ArrayList();
+ public List<SuffixFilter> getSettingsList() {
+ List<SuffixFilter> c = new ArrayList<SuffixFilter>();
c.add(defaultFilter);
- Iterator iter = otherFilters.iterator();
- while (iter.hasNext()) {
- c.add((iter.next()));
- }
+ c.addAll(otherFilters);
return c;
}
}
/**
- * Class to adjust {@link org.tigris.gef.base.CmdSaveEPS} for our purpuses.<p>
+ * Class to adjust {@link org.tigris.gef.base.CmdSaveEPS} for our purposes.<p>
*
* TODO: While doing this refactoring (February 2004) it is unclear to me (Linus
* Tolke) why this modification in the {@link org.tigris.gef.base.CmdSaveEPS}
@@ -371,13 +369,13 @@
*
* TODO: Does this behavior need to be replicated for {@link SaveEPSAction}
* as well? - tfm - 20070511
+ * <p>
+ * @deprecated for 0.25.5 by tfmorris use {@link SaveScaledEPSAction}
*/
+@SuppressWarnings("deprecation")
+@Deprecated
class ActionSaveGraphicsCmdSaveEPS extends CmdSaveEPS {
- /*
- * @see org.tigris.gef.base.CmdSaveGraphics#saveGraphics(
- * java.io.OutputStream, org.tigris.gef.base.Editor,
- * java.awt.Rectangle)
- */
+
protected void saveGraphics(OutputStream s, Editor ce,
Rectangle drawingArea)
throws IOException {
@@ -403,3 +401,68 @@
private static final long serialVersionUID = 2859279843998315644L;
}
+class SaveScaledEPSAction extends SaveEPSAction {
+
+ SaveScaledEPSAction(String name) {
+ super(name);
+ }
+
+ @Override
+ protected void saveGraphics(OutputStream s, Editor ce,
+ Rectangle drawingArea)
+ throws IOException {
+
+ double editorScale = ce.getScale();
+ int x = (int) (drawingArea.x * editorScale);
+ int y = (int) (drawingArea.y * editorScale);
+ int h = (int) (drawingArea.height * editorScale);
+ int w = (int) (drawingArea.width * editorScale);
+ drawingArea = new Rectangle(x, y, w, h);
+
+ PostscriptWriter ps = new PostscriptWriter(s, drawingArea);
+
+ ps.scale(editorScale, editorScale);
+
+ ce.print(ps);
+ ps.dispose();
+ }
+
+}
+
+/**
+ * Write out a PNG image of the current diagram using a more memory efficient
+ * scheme than GEF uses.
+ *
+ * @author Tom Morris <[email protected]>
+ */
+class SavePNGAction2 extends SavePNGAction {
+
+ private static final Logger LOG = Logger.getLogger(SavePNGAction2.class);
+
+ SavePNGAction2(String name) {
+ super(name);
+ }
+
+
+ /**
+ * Write the diagram contained by the current editor into an OutputStream as
+ * a PNG image.
+ */
+ @Override
+ protected void saveGraphics(OutputStream s, Editor ce,
+ Rectangle drawingArea)
+ throws IOException {
+
+ // Create an image which will do deferred rendering of the GEF
+ // diagram on demand as data is pulled from it
+ RenderedImage i = new DeferredBufferedImage(drawingArea,
+ BufferedImage.TYPE_INT_ARGB, ce, scale);
+
+ LOG.debug("Created DeferredBufferedImage - drawingArea = "
+ + drawingArea + " , scale = " + scale);
+
+ ImageIO.write(i, "png", s);
+
+ }
+}
+
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.