// $Id: SaveGraphicsManager.java 132 2010-09-26 23:32:33Z marcusvnac $
// Copyright (c) 2005-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.uml.ui;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
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.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;
//#if defined(LOGGING)
//@#$LPS-LOGGING:GranularityType:Import
import org.apache.log4j.Logger;
//#endif
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;
import org.tigris.gef.base.Editor;
import org.tigris.gef.base.Globals;
import org.tigris.gef.base.SaveEPSAction;
import org.tigris.gef.base.SaveGIFAction;
import org.tigris.gef.base.SaveGraphicsAction;
import org.tigris.gef.base.SavePNGAction;
import org.tigris.gef.base.SavePSAction;
import org.tigris.gef.base.SaveSVGAction;
import org.tigris.gef.persistence.export.PostscriptWriter;
/**
* This class has some similar functions like PersistenceManager. <p>
*
* It centralizes all knowledge about the different graphical formats.
* This class is the only one that is supposed to know
* the complete list of supported graphics formats.
*
* @author mvw@tigris.org
*/
public final class SaveGraphicsManager {
private static final int MIN_MARGIN = 15;
/**
* The configuration key for the preferred graphics format.
*/
public static final ConfigurationKey KEY_DEFAULT_GRAPHICS_FILTER =
Configuration.makeKey("graphics", "default", "filter");
/**
* The configuration key for the "save graphics" file location.
*/
public static final ConfigurationKey KEY_SAVE_GRAPHICS_PATH =
Configuration.makeKey("graphics", "save", "path");
/**
* The configuration key for the "save all graphics" file location.
*/
public static final ConfigurationKey KEY_SAVEALL_GRAPHICS_PATH =
Configuration.makeKey("graphics", "save-all", "path");
/**
* The configuration key for the export graphics resolution.
*/
public static final ConfigurationKey KEY_GRAPHICS_RESOLUTION =
Configuration.makeKey("graphics", "export", "resolution");
/**
* The default file format.
*/
private SuffixFilter defaultFilter;
/**
* The list of other file formats.
*/
private List<SuffixFilter> otherFilters = new ArrayList<SuffixFilter>();
/**
* The singleton instance.
*/
private static SaveGraphicsManager instance;
/**
* The constructor.
*/
private SaveGraphicsManager() {
defaultFilter = FileFilters.PNG_FILTER;
otherFilters.add(FileFilters.GIF_FILTER);
otherFilters.add(FileFilters.SVG_FILTER);
otherFilters.add(FileFilters.PS_FILTER);
otherFilters.add(FileFilters.EPS_FILTER);
setDefaultFilterBySuffix(Configuration.getString(
KEY_DEFAULT_GRAPHICS_FILTER,
defaultFilter.getSuffix()));
}
/**
* @param suffix the extension of the new default file-format
*/
public void setDefaultFilterBySuffix(String suffix) {
for (SuffixFilter sf : otherFilters) {
if (sf.getSuffix().equalsIgnoreCase(suffix)) {
setDefaultFilter(sf);
break;
}
}
}
/**
* @param f the new default file-format
*/
public void setDefaultFilter(SuffixFilter f) {
otherFilters.remove(f);
if (!otherFilters.contains(defaultFilter)) {
otherFilters.add(defaultFilter);
}
defaultFilter = f;
Configuration.setString(
KEY_DEFAULT_GRAPHICS_FILTER,
f.getSuffix());
Collections.sort(otherFilters, new Comparator<SuffixFilter>() {
public int compare(SuffixFilter arg0, SuffixFilter arg1) {
return arg0.getSuffix().compareToIgnoreCase(
arg1.getSuffix());
}
});
}
/**
* @return returns the singleton
*/
public static SaveGraphicsManager getInstance() {
if (instance == null) {
instance = new SaveGraphicsManager();
}
return instance;
}
/**
* This function allows to add new filters. This can be done e.g.
* by modules.<p>
*
* @param f the filter
*/
public void register(SuffixFilter f) {
otherFilters.add(f);
}
/**
* @param chooser the filechooser of which the filters will be set
* @param defaultName default filename to show when chooser is displayed
*/
public void setFileChooserFilters(
JFileChooser chooser, String defaultName) {
chooser.addChoosableFileFilter(defaultFilter);
Iterator iter = otherFilters.iterator();
while (iter.hasNext()) {
chooser.addChoosableFileFilter((SuffixFilter) iter.next());
}
chooser.setFileFilter(defaultFilter);
String fileName = defaultName + "." + defaultFilter.getSuffix();
chooser.setSelectedFile(new File(fileName));
chooser.addPropertyChangeListener(
JFileChooser.FILE_FILTER_CHANGED_PROPERTY,
new FileFilterChangedListener(chooser, defaultName));
}
/**
* This class listens to changes in the selected filefilter.
* If the user changes the filefilter
* (e.g. he changes from *.gif to *.png),
* then the filename field got emptied before I introduced this class.
* Now, a new filename is made up, based on
* the diagram name + the new extension (suffix).
*
* @author mvw@tigris.org
*/
static class FileFilterChangedListener implements PropertyChangeListener {
private JFileChooser chooser;
private String defaultName;
/**
* Constructor.
* @param c
* @param name
*/
public FileFilterChangedListener(JFileChooser c, String name) {
chooser = c;
defaultName = name;
}
/*
* @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
*/
public void propertyChange(PropertyChangeEvent evt) {
SuffixFilter filter = (SuffixFilter) evt.getNewValue();
String fileName = defaultName + "." + filter.getSuffix();
/* The next line does not work: */
// chooser.setSelectedFile(new File(fileName));
/* So, let's do it the hard way: */
SwingUtilities.invokeLater(new Anonymous1(fileName));
}
class Anonymous1 implements Runnable {
private String fileName;
/**
* Constructor.
*
* @param fn The filename.
*/
Anonymous1(String fn) {
fileName = fn;
}
/*
* @see java.lang.Runnable#run()
*/
public void run() {
chooser.setSelectedFile(new File(fileName));
}
}
}
/**
* @param name the filename
* @return the filter
*/
public SuffixFilter getFilterFromFileName(String name) {
if (name.toLowerCase()
.endsWith("." + defaultFilter.getSuffix())) {
return defaultFilter;
}
Iterator iter = otherFilters.iterator();
while (iter.hasNext()) {
SuffixFilter filter = (SuffixFilter) iter.next();
if (name.toLowerCase().endsWith("." + filter.getSuffix())) {
return filter;
}
}
return null;
}
/**
* @return the extension of the default filter
* (just the text, not the ".")
*/
public String getDefaultSuffix() {
return defaultFilter.getSuffix();
}
/**
* @param in the input file or path name which may or may not
* have a recognised extension
* @return the amended file or pathname, guaranteed to have
* a recognised extension
*/
public String fixExtension(String in) {
if (getFilterFromFileName(in) == null) {
in += "." + getDefaultSuffix();
}
return in;
}
/**
* @param suffix the suffix (extension) of the filename,
* which corresponds to the graphics format to be used
* @return the action that will do the save
*/
public SaveGraphicsAction getSaveActionBySuffix(String suffix) {
SaveGraphicsAction cmd = null;
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 SaveScaledEPSAction(
Translator.localize("action.save-eps"));
} else if (FileFilters.PNG_FILTER.getSuffix().equals(suffix)) {
cmd = new SavePNGAction2(Translator.localize("action.save-png"));
} else if (FileFilters.GIF_FILTER.getSuffix().equals(suffix)) {
cmd = new SaveGIFAction(Translator.localize("action.save-gif"));
// TODO: The following can be used when we drop Java 5 support or
// when an ImageIO GIF writer plugin is bundled
// cmd = new SaveGIFAction2(Translator.localize("action.save-gif"));
} else if (FileFilters.SVG_FILTER.getSuffix().equals(suffix)) {
cmd = new SaveSVGAction(Translator.localize("action.save-svg"));
}
return cmd;
}
/**
* @return the complete collection of SuffixFilters,
* the first one is the default one
*/
public List<SuffixFilter> getSettingsList() {
List<SuffixFilter> c = new ArrayList<SuffixFilter>();
c.add(defaultFilter);
c.addAll(otherFilters);
return c;
}
/**
* Adjust the drawing area so that instead of a tight bounding box, it
* includes the canvas origin and some space around the lower and right
* sides so that the elements will be roughly centered. Elements which are
* off the top or left side of the canvas may still be clipped (ie if the
* original drawing area had a negative x or y coordinate).
*
* @param area rectangle representing original drawing area
* @return an expanded rectangle
*/
static Rectangle adjustDrawingArea(Rectangle area) {
int xMargin = area.x;
if (xMargin < 0) {
xMargin = 0;
}
int yMargin = area.y;
if (yMargin < 0) {
yMargin = 0;
}
int margin = Math.max(xMargin, yMargin);
if (margin < MIN_MARGIN) {
margin = MIN_MARGIN;
}
return new Rectangle(0, 0,
area.width + (2 * margin),
area.height + (2 * margin));
}
}
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 <tfmorris@gmail.com>
*/
class SavePNGAction2 extends SavePNGAction {
//#if defined(LOGGING)
//@#$LPS-LOGGING:GranularityType:Field
private static final Logger LOG = Logger.getLogger(SavePNGAction2.class);
//#endif
SavePNGAction2(String name) {
super(name);
}
@Override
public void actionPerformed(ActionEvent ae) {
Editor ce = Globals.curEditor();
Rectangle drawingArea =
ce.getLayerManager().getActiveLayer().calcDrawingArea();
// If the diagram is empty, GEF won't write anything, leaving us with
// an empty (and invalid) file. Handle this case ourselves to prevent
// this from happening.
if (drawingArea.width <= 0 || drawingArea.height <= 0) {
Rectangle dummyArea = new Rectangle(0, 0, 50, 50);
try {
saveGraphics(outputStream, ce, dummyArea);
} catch (java.io.IOException e) {
//#if defined(LOGGING)
//@#$LPS-LOGGING:GranularityType:Statement
//@#$LPS-LOGGING:Localization:NestedStatement
LOG.error("Error while exporting Graphics:", e);
//#endif
}
return;
}
// Anything else is handled the normal way
super.actionPerformed(ae);
}
/**
* 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 {
Rectangle canvasArea =
SaveGraphicsManager.adjustDrawingArea(drawingArea);
// Create an image which will do deferred rendering of the GEF
// diagram on demand as data is pulled from it
RenderedImage i = new DeferredBufferedImage(canvasArea,
BufferedImage.TYPE_INT_ARGB, ce, scale);
//#if defined(LOGGING)
//@#$LPS-LOGGING:GranularityType:Statement
LOG.debug("Created DeferredBufferedImage - drawingArea = "
+ canvasArea + " , scale = " + scale);
//#endif
ImageIO.write(i, "png", s);
}
}
/**
* Action to save a diagram as a GIF image in a supplied OutputStream.
*
* TODO: This requires Java 6 in its current state, so don't use.
*
* @author Tom Morris <tfmorris@gmail.com>
*/
class SaveGIFAction2 extends SaveGIFAction {
/**
* Creates a new SaveGIFAction
*
* @param name The name of the action
*/
SaveGIFAction2(String name) {
super(name);
}
/**
* Write the diagram contained by the current editor into an OutputStream as
* a GIF image.
*/
@Override
protected void saveGraphics(OutputStream s, Editor ce,
Rectangle drawingArea) throws IOException {
Rectangle canvasArea =
SaveGraphicsManager.adjustDrawingArea(drawingArea);
RenderedImage i = new DeferredBufferedImage(canvasArea,
BufferedImage.TYPE_INT_ARGB, ce, scale);
// NOTE: GEF's GIF writer uses Jeff Poskanzer's GIF encoder, but that
// saves a copy of the entire image in an internal buffer before
// starting work, defeating the whole purpose of our incremental
// rendering.
// Java SE 6 has a native GIF writer, but it's not in Java 5. One
// is available in the JAI-ImageIO library, but we don't currently
// bundle that and at 6+ MB it seems like a heavyweight solution, but
// I don't have time to produce a stripped down version right now - tfm
// https://jai-imageio.dev.java.net/
ImageIO.write(i, "gif", s);
}
}