/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.ui.editor;
import com.lightcrafts.image.BadImageFileException;
import com.lightcrafts.image.ColorProfileException;
import com.lightcrafts.image.ImageInfo;
import com.lightcrafts.image.UnknownImageTypeException;
import com.lightcrafts.image.export.ImageExportOptions;
import com.lightcrafts.image.metadata.ImageMetadata;
import com.lightcrafts.image.metadata.ImageOrientation;
import com.lightcrafts.mediax.jai.util.ImagingException;
import com.lightcrafts.model.Engine;
import com.lightcrafts.model.EngineFactory;
import com.lightcrafts.model.Scale;
import static com.lightcrafts.ui.editor.Locale.LOCALE;
import com.lightcrafts.ui.export.SaveOptions;
import com.lightcrafts.ui.print.PrintLayoutModel;
import com.lightcrafts.utils.UserCanceledException;
import com.lightcrafts.utils.thread.ProgressThread;
import com.lightcrafts.utils.xml.XMLException;
import com.lightcrafts.utils.xml.XmlDocument;
import com.lightcrafts.utils.xml.XmlNode;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.RenderedImage;
import java.io.*;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
/**
* A Document is the public face of an image editor. It ties together an
* image, an engine which processes it, and an editor component for user
* interaction. It provides access to various pieces of package-private
* functionality to interact with application context, such as save/restore,
* undo/redo, frames, menus, printing and export.
*/
public class Document {
/**
* Thrown by the constructors when the Image tag in a saved Document
* doesn't point to a valid source image.
*/
public class MissingImageFileException extends Exception {
private File imageFile;
private MissingImageFileException(File imageFile) {
super(LOCALE.get("MissingImageError", imageFile.getPath()));
this.imageFile = imageFile;
}
public File getImageFile() {
return imageFile;
}
}
/**
* A version number for the saved-Document file format:
* 1: Original version, all the 1.0 releases, until December 29, 2005.
* 2: For the clone tool, in 1.1 betas, until January 20, 2006.
* 3: For more precise slider values, until February 8, 2006.
* 4: Add the region invert switch on tools, until February 18, 2006.
* 5: The new dropper-style white point tool, until July 21, 2006.
* 6: Add SaveOptions and ExportOptions, new tools, the black point
* compensation export option, and PPI for printing, until April 23,
* 2007.
* 7: Add the "RAW Adjustments" controls, implying versioning of
* the Engine's RAW conversion, until July 6, 2007.
* 8: Add selective color controls.
*/
public final static int SavedDocVersion = 8;
private ImageMetadata meta;
private SaveOptions save;
private Editor editor;
private Engine engine;
private CropRotateManager crop;
private RegionManager regions;
private ScaleModel scale;
private XFormModel xform;
private PrintLayoutModel print;
private DocUndoManager undo;
private boolean dirty;
private Collection<DocumentListener> listeners;
// Recent values used for image export (to initialize the file chooser):
private ImageExportOptions export;
// Documents may be opened in a way that effects how they should be saved:
private Object source;
/**
* Calls Document(doc, null).
*/
public Document(XmlDocument doc)
throws ColorProfileException,
UserCanceledException,
IOException,
XMLException,
MissingImageFileException,
BadImageFileException,
UnknownImageTypeException
{
this(doc, null);
}
/**
* Calls Document(doc, meta, null, null).
*/
public Document(XmlDocument doc, ImageMetadata meta)
throws UserCanceledException,
ColorProfileException,
IOException,
XMLException,
MissingImageFileException,
BadImageFileException,
UnknownImageTypeException
{
this(doc, meta, null, null);
}
/** Initialize from an XmlDocument structure of the form created by
* <code>save()</code>.
* <p>
* Throws XMLException if there's something wrong with the XmlDocument
* format, and throws IOException if there's trouble reading the image
* file referenced inside.
* <p>
* The ImageMetadata argument can override the image file reference
* inside the XmlDocument, or it can be null.
*/
public Document(
XmlDocument doc,
ImageMetadata meta,
ImageInfo versionInfo,
ProgressThread thread
)
throws XMLException,
IOException,
BadImageFileException,
ColorProfileException,
UnknownImageTypeException,
UserCanceledException,
MissingImageFileException,
ImagingException
{
XmlNode root = doc.getRoot();
int version = root.getVersion();
if (version > SavedDocVersion) {
throw new XMLException(LOCALE.get("FutureLznError"));
}
if (version < 0) {
throw new XMLException(LOCALE.get("MissingLznVersionError"));
}
if (meta == null) {
// Find the original image:
XmlNode node = root.getChild(ImageTag);
String path = node.getAttribute(ImagePathTag);
File file = new File(path);
if (! file.isFile()) {
throw new MissingImageFileException(file);
}
// Override with a relative path, if one was defined:
if (node.hasAttribute(ImageRelativePathTag)) {
path = node.getAttribute(ImageRelativePathTag);
File relativeFile = new File(path);
if (relativeFile.isFile()) {
file = relativeFile;
}
}
ImageInfo info = ImageInfo.getInstanceFor(file);
try {
meta = info.getMetadata();
}
catch (FileNotFoundException e) {
throw new MissingImageFileException(file);
}
}
this.meta = meta;
// Enforce the saved original image orientation, which defines the
// coordinate system used for regions and crop bounds:
XmlNode imageNode = root.getChild(ImageTag);
if (imageNode.hasAttribute(ImageOrientationTag)) {
String value = imageNode.getAttribute(ImageOrientationTag);
try {
ImageOrientation oldOrientation =
ImageOrientation.getOrientationFor(Short.parseShort(value));
ImageOrientation newOrientation = meta.getOrientation();
if (oldOrientation != newOrientation) {
meta.setOrientation(oldOrientation);
}
}
catch (NumberFormatException e) {
throw new XMLException(
"Image orientation \"" + value + "\" is not a number", e
);
}
}
// Backwards compatibility: before XMP support, the convention in LZN
// files was that the image orientation was its original orientation,
// before any browser rotations.
else {
// Make sure this pre-XMP LZN structure is not a Template.
// (See Application.saveTemplate().)
if (! root.getName().equals("Template")) {
ImageOrientation origOrient = meta.getOriginalOrientation();
meta.setOrientation(origOrient);
}
}
engine = EngineFactory.createEngine(meta, versionInfo, thread);
xform = new XFormModel(engine);
regions = new RegionManager();
crop = new CropRotateManager(engine, xform);
scale = new ScaleModel(engine);
XmlNode scaleNode = root.getChild(ScaleTag);
Scale s = new Scale(scaleNode);
scale.setScale(s);
editor = new Editor(engine, scale, xform, regions, crop, this);
editor.showWait(LOCALE.get("EditorWaitText"));
crop.setEditor( editor );
XmlNode controlNode = root.getChild(ControlTag);
// this does the inverse of save(XmlNode):
try {
editor.restore(controlNode);
} catch (XMLException e) {
dispose();
throw e;
}
commonInitialization();
if (root.hasChild(SaveTag)) {
XmlNode saveNode = root.getChild(SaveTag);
save = SaveOptions.restore(saveNode);
}
if (root.hasChild(PrintTag)) {
XmlNode printNode = root.getChild(PrintTag);
Dimension size = engine.getNaturalSize();
print = new PrintLayoutModel(size.width, size.height);
print.restore(printNode);
}
if (root.hasChild(ExportTag)) {
XmlNode exportNode = root.getChild(ExportTag);
export = ImageExportOptions.read(exportNode);
}
}
/**
* Calls Document(meta, null).
*/
public Document(ImageMetadata meta)
throws UserCanceledException,
ColorProfileException,
IOException,
BadImageFileException,
UnknownImageTypeException
{
this(meta, null);
}
/**
* Initialize from an image with everything else set to defaults.
* (On the resulting instance, <code>getFile()</code> will return null.)
*/
public Document(
ImageMetadata meta, ProgressThread thread
)
throws BadImageFileException,
ColorProfileException,
ImagingException,
IOException,
UnknownImageTypeException,
UserCanceledException
{
this.meta = meta;
engine = EngineFactory.createEngine(meta, null, thread);
xform = new XFormModel(engine);
regions = new RegionManager();
scale = new ScaleModel(engine);
crop = new CropRotateManager(engine, xform);
editor = new Editor(engine, scale, xform, regions, crop, this);
crop.setEditor( editor );
editor.showWait(LOCALE.get("EditorWaitText"));
commonInitialization();
}
private void commonInitialization() {
undo = new DocUndoManager(this);
editor.addUndoableEditListener(undo);
print = null;
scale.addScaleListener(
new ScaleListener() {
public void scaleChanged(Scale scale) {
xform.update();
}
}
);
xform.addXFormListener(
new XFormListener() {
public void xFormChanged(AffineTransform xform) {
regions.setXForm(xform);
}
}
);
listeners = new LinkedList<DocumentListener>();
}
/**
* Notify this Document that it has been saved to a File. Effects the
* results returned by getSaveOptions() and getName().
*/
public void setSaveOptions(SaveOptions save) {
this.save = save;
}
/**
* If this Document has been saved, then this returns the most recent way
* it was saved. Otherwise it returns null.
*/
public SaveOptions getSaveOptions() {
return save;
}
/**
* If this Document has been saved, return the saved File. This is just
* a convenience method for getSaveOptions().getFile().
*/
public File getFile() {
return (save != null) ? save.getFile() : null;
}
/**
* Get metadata for this Document's original image file.
*/
public ImageMetadata getMetadata() {
return meta;
}
/**
* Get the editor component for this Document.
*/
public Editor getEditor() {
return editor;
}
/**
* Get a placeholder editor that is entirely disabled, for display in
* cases where there is no Document.
*/
public static DisabledEditor createDisabledEditor(
DisabledEditor.Listener listener
) {
return new DisabledEditor(listener);
}
/**
* Get the backing Engine for this Document. Useful for printing and for
* propagating debug actions from the Engine to the menus.
*/
public Engine getEngine() {
return engine;
}
/**
* Get the Action that shows and hides the proof control in the editor.
*/
public Action getProofAction() {
return editor.getProofAction();
}
/**
* Get the Action that rotates the image ninety degrees to the right.
*/
public Action getRotateRightAction() {
return crop.getRightAction();
}
/**
* Get the Action that rotates the image ninety degrees to the left.
*/
public Action getRotateLeftAction() {
return crop.getLeftAction();
}
/**
* Get the Action that performs undo.
*/
public Action getUndoAction() {
return undo.getUndoAction();
}
/**
* Get the Action that performs redo.
*/
public Action getRedoAction() {
return undo.getRedoAction();
}
/**
* Get the DocUndoManager that handles all of undo and redo.
*/
DocUndoManager getUndoManager() {
return undo;
}
/**
* Get a List of Actions that cause all the available tool controls to
* be pushed onto the tool stack in the editor.
*/
public List<Action> getOperations() {
return editor.getOperations();
}
/**
* Get the model for scale changes in the editor. Through the scale model,
* you can change the editor's zoom factor, and also find out when the zoom
* changes.
*/
public ScaleModel getScaleModel() {
return scale;
}
/**
* Get the RegionManager for this Document, the gateway to everything
* about tool masks in the editor. The RegionManager is the place to
* access the curve type (polygon, basis spline, bezier curve); the curve
* selection model; and the region copy/paste feature.
*/
public RegionManager getRegionManager() {
return regions;
}
/**
* The zoom-to-fit operation depends simultaneously on the ScaleModel,
* the editor component, and the Engine; therefore this operation is
* handled inside Document.
*/
public void zoomToFit() {
Rectangle rect = editor.getMaxImageBounds();
// Sometimes during frame initialization, the max image bounds
// is reported as zero. Perhaps some layout glitch involving
// scroll pane interaction?
if ((rect.width > 0) && (rect.height > 0)) {
Scale oldScale = scale.getCurrentScale();
Scale newScale = engine.setScale(rect);
if (! scale.setScale(newScale)) {
engine.setScale(oldScale);
}
}
}
/**
* Reset the dirty flag, which indicates whether this Document has been
* edited by the user. See isDirty().
*/
public void markClean() {
if (dirty) {
dirty = false;
notifyListeners();
}
}
/**
* Set the dirty flag, which indicates whether this Document has been
* edited by the user. See isDirty().
*/
public void markDirty() {
if (! dirty) {
dirty = true;
notifyListeners();
}
}
/**
* Query the dirty flag on this Document. True means the user has done
* some work since this Document was initialized.
*/
public boolean isDirty() {
return dirty;
}
/**
* If the editor is in zoom-to-fit mode, turn the mode off temporarily.
*/
public void pushFitMode() {
editor.pushFitMode();
}
/**
* If the editor was in zoom-to-fit mode before a call to pushFitMode(),
* then turn the mode back on.
*/
public boolean popFitMode() {
return editor.popFitMode();
}
/**
* Forget all undoable edits in the current undo stack.
*/
public void discardEdits() {
undo.discardAllEdits();
}
/**
* Detect whether the current tool stack includes a RAW Adjustments tool.
* When a RAW image or an older LZN file is opened (before LZN version 7),
* this tool will be absent and must be added manually.
*/
public boolean hasRawAdjustments() {
return editor.hasRawAdjustments();
}
/**
* Set a cookie, probably representing the way this Document was opened.
*/
public void setSource(Object source) {
this.source = source;
}
/**
* Get the cookie from setSource(), probably representing the way this
* Document was opened.
*/
public Object getSource() {
return source;
}
public void addDocumentListener(DocumentListener listener) {
listeners.add(listener);
}
public void removeDocumentListener(DocumentListener listener) {
listeners.remove(listener);
}
private void notifyListeners() {
for (DocumentListener listener : listeners) {
listener.documentChanged(this, dirty);
}
}
/**
* Get the most recent print settings applied to this Document, or null
* if these parameters have never been set.
*/
public PrintLayoutModel getPrintLayout() {
if (print != null) {
// Image bounds may have changed due to cropping:
Dimension size = engine.getNaturalSize();
print.updateImageSize(size.width, size.height);
}
return print;
}
/**
* Set print settings for this Document, for getPrintLayout().
*/
public void setPrintLayout(PrintLayoutModel print) {
this.print = print;
markDirty();
}
/**
* Get the most recent export options applied to this Document, or null
* if no export options have ever been set.
*/
public ImageExportOptions getExportOptions() {
return export;
}
/**
* Set export options on this Document, for getExportOptions().
*/
public void setExportOptions(ImageExportOptions options) {
export = options;
markDirty();
}
public void dispose() {
editor.dispose();
engine.dispose();
}
public TemporaryEditorCommitState saveStart() {
return editor.saveStart();
}
public void saveEnd(TemporaryEditorCommitState state) {
editor.saveEnd(state);
}
// String constants used for serialization:
private final static String ScaleTag = "Scale";
private final static String ImageTag = "Image";
private final static String ImagePathTag = "path";
private final static String ImageOrientationTag = "orientation";
private final static String ImageRelativePathTag = "relativePath";
private final static String ControlTag = "Controls";
private final static String SaveTag = "Save";
private final static String PrintTag = "Print";
private final static String ExportTag = "Export";
public void save(XmlNode node) {
node.setVersion(SavedDocVersion);
XmlNode scaleNode = node.addChild(ScaleTag);
scale.getCurrentScale().save(scaleNode);
XmlNode imageNode = node.addChild(ImageTag);
imageNode.setAttribute(
ImagePathTag, meta.getFile().getAbsolutePath()
);
imageNode.setAttribute(
ImageOrientationTag,
Integer.toString(meta.getOrientation().getTIFFConstant())
);
if (save != null) {
String path;
try {
path = RelativePathUtility.getRelativePath(
save.getFile(), meta.getFile()
);
}
catch (IOException e) {
path = meta.getFile().getName();
}
imageNode.setAttribute(ImageRelativePathTag, path);
}
XmlNode controlNode = node.addChild(ControlTag);
editor.save(controlNode);
if (save != null) {
XmlNode saveNode = node.addChild(SaveTag);
save.save(saveNode);
}
if (print != null) {
XmlNode printNode = node.addChild(PrintTag);
print.save(printNode);
}
if (export != null) {
XmlNode exportNode = node.addChild(ExportTag);
export.write(exportNode);
}
}
// Just like save(), but without SaveTag, PrintTag, or ExportTag, an
// empty image pointer, and no image relative path. The result may
// be used either in a Document constructor or in applyTemplate().
public void saveTemplate(XmlNode node) {
node.setVersion(SavedDocVersion);
XmlNode scaleNode = node.addChild(ScaleTag);
scale.getCurrentScale().save(scaleNode);
XmlNode imageNode = node.addChild(ImageTag);
imageNode.setAttribute(ImagePathTag, "");
XmlNode controlNode = node.addChild(ControlTag);
editor.save(controlNode);
}
public void applyTemplate(XmlNode node)
throws XMLException
{
int version = node.getVersion();
if (version > SavedDocVersion) {
throw new XMLException(LOCALE.get("FutureTemplateError"));
}
if (version < 0) {
throw new XMLException(LOCALE.get("MissingTemplateVersionError"));
}
// Ignore the scale factor under ScaleTag.
XmlNode controlNode = node.getChild(ControlTag);
editor.addControls(controlNode);
}
// Since Anton keeps forgetting to dispose documents, I add a finalizer
public void finalize() throws Throwable {
super.finalize();
dispose();
}
public static void main(String[] args) throws Exception {
InputStream in = new FileInputStream(args[0]);
XmlDocument xml = new XmlDocument(in);
Document doc = new Document(xml, null);
RenderedImage image = doc.engine.getRendering(new Dimension(100, 100));
ImageIO.write(image, "jpeg", new File("out.jpg"));
}
}