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