1 /* Copyright (C) 2005-2011 Fabio Riccardi */
2 
3 package com.lightcrafts.app;
4 
5 import static com.lightcrafts.app.Locale.LOCALE;
6 import com.lightcrafts.app.other.OtherApplication;
7 import com.lightcrafts.app.other.LightroomApplication;
8 import com.lightcrafts.app.other.UnknownApplication;
9 import com.lightcrafts.image.ImageInfo;
10 import com.lightcrafts.image.export.ImageExportOptions;
11 import com.lightcrafts.image.types.LZNImageType;
12 import com.lightcrafts.jai.JAIContext;
13 import com.lightcrafts.jai.opimage.CachedImage;
14 import com.lightcrafts.mediax.jai.PlanarImage;
15 import com.lightcrafts.model.Engine;
16 import com.lightcrafts.platform.Platform;
17 import com.lightcrafts.platform.ProgressDialog;
18 import com.lightcrafts.ui.browser.model.PreviewUpdater;
19 import com.lightcrafts.ui.editor.Document;
20 import com.lightcrafts.ui.export.SaveOptions;
21 import com.lightcrafts.utils.thread.ProgressThread;
22 import com.lightcrafts.utils.xml.XMLException;
23 import com.lightcrafts.utils.xml.XmlDocument;
24 import com.lightcrafts.utils.xml.XmlNode;
25 
26 import java.awt.*;
27 import java.awt.image.RenderedImage;
28 import java.io.*;
29 
30 /**
31  * Lengthy procedures that get run during save and export.
32  */
33 public class DocumentWriter {
34 
35     /**
36      * Save the given Document to a File, reporting progress during lengthy
37      * jobs using the given ProgressThread.  The XMLException can only arise
38      * when surprises occur in the LZN structure as it is navigated for
39      * features that mangle XML like "copy original" and "multilayer TIFF".
40      */
save( Document doc, ComboFrame frame, boolean saveDirectly, ProgressThread progress )41     public static boolean save(
42         Document doc, ComboFrame frame, boolean saveDirectly,
43         ProgressThread progress
44     ) throws IOException {
45         // Construct the LZN data that encode the Document's state:
46         XmlDocument xml = new XmlDocument(
47             Application.LznNamespace, "LightZoneTransform"
48         );
49         XmlNode root = xml.getRoot();
50         doc.save(root);
51 
52         // Try/catch XML manipulation errors:
53         try {
54             // The next steps depend on the SaveOptions:
55             SaveOptions options = doc.getSaveOptions();
56 
57             if (options.isLzn()) {
58                 // Just write the XML to a file, with a thumbnail and a preview:
59                 frame.pause();
60                 saveLzn(doc, xml);
61                 frame.resume();
62             }
63             else {
64                 // We're performing some kind of Engine export:
65                 Engine engine = doc.getEngine();
66                 ImageExportOptions export =
67                     SaveOptions.getExportOptions(options);
68 
69                 OtherApplication app = (OtherApplication)doc.getSource();
70 
71                 // Mangle LZN and add it to the export options as appropriate:
72                 if (options.isSidecarJpeg() || options.isSidecarTiff()) {
73                     if (app instanceof LightroomApplication) {
74                         File file = LightroomApplication.getOriginalFile(
75                             options.getFile()
76                         );
77                         mangleLznSidecarFile(xml,file);
78                     }
79                     addLznMetadata(export, xml);
80                 }
81                 else if (options.isMultilayerTiff()) {
82                     mangleLznMultilayerTiff(xml);
83                     addLznMetadata(export, xml);
84                 }
85                 String message = LOCALE.get("SavingMessage");
86 
87                 if (app != null && app != UnknownApplication.INSTANCE &&
88                     saveDirectly) {
89                     String name = app.getName();
90                     message = LOCALE.get("SavingToMessage", name);
91                 }
92                 if (progress != null) {
93                     export(engine, export, progress);
94                     return true;
95                 }
96                 return exportWithDialog(engine, export, message, frame);
97             }
98         }
99         catch (XMLException e) {
100             throw new IOException(
101                 "Internal error in XML mangling: " + e.getMessage()
102             );
103         }
104         return true;
105     }
106 
107     /**
108      * Call DocumentWriter.export() under a progress dialog.
109      */
exportWithDialog( final Engine engine, final ImageExportOptions options, final String title, final Frame parent )110     static boolean exportWithDialog(
111         final Engine engine,
112         final ImageExportOptions options,
113         final String title,
114         final Frame parent
115     ) throws IOException {
116         ProgressDialog dialog = Platform.getPlatform().getProgressDialog();
117         ProgressThread thread = new ProgressThread(dialog) {
118             public void run() {
119                 try {
120                     // Write the file:
121                     export(engine, options, this);
122                 }
123                 catch (IOException e) {
124                     // This exception should be unpacked in the error handling
125                     // below, following dialog.showProgress().
126                     throw new RuntimeException(e);
127                 }
128             }
129         };
130         dialog.showProgress(parent, thread, title, 0, 10, true);
131 
132         // Unpack any Throwable, in case it hides a checked exception:
133         Throwable error = dialog.getThrown();
134         if (error != null) {
135             if ( error instanceof IOException )
136                 throw (IOException) error;
137             if ( error instanceof RuntimeException )
138                 throw (RuntimeException) error;
139             throw new RuntimeException( error );
140         }
141         if (thread.isCanceled()) {
142             return false;
143         }
144         return true;
145     }
146 
147     /**
148      * Write the XML to a file as-is, and add a thumbnail for the browser.
149      */
saveLzn(Document doc, XmlDocument xmlDoc)150     static void saveLzn(Document doc, XmlDocument xmlDoc) throws IOException {
151         Engine engine = doc.getEngine();
152 
153         // Fill up the LZN file:
154         SaveOptions options = doc.getSaveOptions();
155         File file = options.getFile();
156         OutputStream out = new FileOutputStream(file);
157         xmlDoc.write(out);
158         out.close();
159 
160         // Add thumbnail data for the browser:
161         RenderedImage thumb = engine.getRendering(new Dimension(320, 320));
162         // divorce the preview from the document
163         thumb = new CachedImage((PlanarImage) thumb, JAIContext.fileCache);
164         ImageInfo info = ImageInfo.getInstanceFor(file);
165         LZNImageType.INSTANCE.putImage(info, thumb);
166 
167         // Cache a high resolution preview:
168         int size = PreviewUpdater.PreviewSize;
169         RenderedImage preview = engine.getRendering(new Dimension(size, size));
170         // divorce the preview from the document
171         preview = new CachedImage((PlanarImage) preview, JAIContext.fileCache);
172         PreviewUpdater.cachePreviewForImage(file, preview);
173     }
174 
175     /**
176      * Set the "auxilliary data" on the ImageExportOptions
177      * to an XMP structure derived from the contents of the XmlDocument while
178      * preserving any preexisting XMP in the given metadata.
179      */
addLznMetadata( ImageExportOptions export, XmlDocument xml )180     private static void addLznMetadata( ImageExportOptions export,
181                                         XmlDocument xml ) throws IOException {
182         ByteArrayOutputStream out = new ByteArrayOutputStream();
183         xml.write( out );
184         byte[] bytes = out.toByteArray();
185         export.setAuxData( bytes );
186     }
187 
mangleLznSidecarFile(XmlDocument xml, File file)188     private static void mangleLznSidecarFile(XmlDocument xml, File file)
189         throws XMLException
190     {
191         // Tags cloned from com.lightcrafts.ui.editor.Document:
192         XmlNode root = xml.getRoot();
193         XmlNode imageNode = root.getChild("Image");
194         imageNode.setAttribute("path", file.getAbsolutePath());
195         imageNode.setAttribute("relativePath", file.getName());
196     }
197 
mangleLznMultilayerTiff(XmlDocument xml)198     private static void mangleLznMultilayerTiff(XmlDocument xml)
199         throws XMLException
200     {
201         // Tags cloned from com.lightcrafts.ui.editor.Document:
202         XmlNode root = xml.getRoot();
203         XmlNode imageNode = root.getChild("Image");
204         imageNode.setAttribute("path", "");
205         imageNode.setAttribute("relativePath", "");
206         imageNode.setAttribute("self", "true");
207     }
208 
209     /**
210      * Call Engine.export() with some options and a progress object for
211      * feedback.  This works by creating a temp file, streaming into that,
212      * removing the final destination file if it exists, and then renaming
213      * the temp file.  If anything goes wrong, the temp file gets cleaned up.
214      */
export( Engine engine, ImageExportOptions options, ProgressThread progress )215     public static void export(
216         Engine engine, ImageExportOptions options, ProgressThread progress
217     ) throws IOException {
218 
219         File tempFile = null;
220         try {
221             // Set up the temp file where the export goes first:
222             File exportFile = options.getExportFile();
223             File exportDir = exportFile.getParentFile();
224             tempFile = File.createTempFile(
225                 "LZExport", ".tmp", exportDir
226             );
227             options.setExportFile(tempFile);
228 
229             // Write to the temp file:
230             engine.write(progress, options);
231 
232             // Restore the final destination file:
233 
234             options.setExportFile(exportFile);
235 
236             // First unlink any file that is in the way:
237             if (exportFile.exists()) {
238                 boolean deleteOK = exportFile.delete();
239                 if (! deleteOK) {
240                     // What side effects does closeAll() have on other threads?
241                     ImageInfo.closeAll();
242                     deleteOK = exportFile.delete();
243                 }
244                 if (! deleteOK) {
245                     throw new IOException(
246                         LOCALE.get("ExportDeleteError", exportFile.getPath())
247                     );
248                 }
249             }
250             // Then move the temp file to the final destination:
251             boolean renameOK = tempFile.renameTo(exportFile);
252             if (! renameOK) {
253                 // What side effects does closeAll() have on other threads?
254                 ImageInfo.closeAll();
255                 renameOK = tempFile.renameTo(exportFile);
256             }
257             if (! renameOK) {
258                 throw new IOException(
259                     LOCALE.get(
260                         "ExportRenameError",
261                         tempFile.getPath(),
262                         exportFile.getPath()
263                     )
264                 );
265             }
266         }
267         finally {
268             if (tempFile != null) {
269                 tempFile.delete();
270             }
271         }
272     }
273 }
274