fixed a bug causing nullpointer exceptions in some image files (i.e. the ones
[mir.git] / source / mir / media / image / ImageMagickImageProcessor.java
1 /*
2  * Copyright (C) 2005 The Mir-coders group
3  *
4  * This file is part of Mir.
5  *
6  * Mir is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * Mir is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Mir; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  *
20  * In addition, as a special exception, The Mir-coders gives permission to link
21  * the code of this program with  any library licensed under the Apache Software License.
22  * You must obey the GNU General Public License in all respects for all of the code used
23  * other than the above mentioned libraries.  If you modify this file, you may extend this
24  * exception to your version of the file, but you are not obligated to do so.
25  * If you do not wish to do so, delete this exception statement from your version.
26  */
27 package mir.media.image;
28
29 import mir.log.LoggerWrapper;
30 import mir.media.MediaExc;
31 import mir.media.MediaFailure;
32 import mir.util.StreamCopier;
33 import mir.util.ShellRoutines;
34 import mir.config.MirPropertiesConfiguration;
35
36 import java.io.BufferedOutputStream;
37 import java.io.ByteArrayOutputStream;
38 import java.io.File;
39 import java.io.FileInputStream;
40 import java.io.FileNotFoundException;
41 import java.io.FileOutputStream;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.io.OutputStream;
45 import java.util.StringTokenizer;
46
47
48 /**
49  * Image processing by calling the ImageMagick command line progrmas
50  * "convert" and "identify". The main task of this class is to scale
51  * images. The path to ImageMagick commandline programs can be
52  * specified in the coonfiguration file.
53  *
54  * @author <grok@no-log.org>, the Mir-coders group
55  */
56 public class ImageMagickImageProcessor implements ImageProcessor {
57   protected static MirPropertiesConfiguration configuration =
58       MirPropertiesConfiguration.instance();
59   static final LoggerWrapper logger =
60       new LoggerWrapper("media.image.imagemagick");
61
62   private ImageFile sourceImage;
63   private ImageFile scaledImage;
64
65   /**
66    * ImageFile  is a  thin  wrapper  around a  file  that contains  an
67    * image.  It uses  ImageMagick  to retreive  information about  the
68    * image. It  can also scale images using  ImageMagick. Intended for
69    * use in the ImageMagickImageProcessor class.
70    */
71   static class ImageFile {
72     /**
73      * path to the file represented by this class
74      */
75     File file;
76     /**
77      * whether the file must be deleted on cleanup
78      */
79     boolean fileIsTemp = false;
80     /**
81      * image information is stored here to avoid multiple costly calls to
82      * "identify"
83      */
84     int width;
85     int height;
86     int fileSize;
87     
88     /**
89      * Image type as returned by identify %m : "PNG", "GIF", ...
90      */
91     String type;
92     /**
93      * number of scenes in image >1 (typically animated gif)
94      */
95     boolean isAnimation;
96
97     /**
98      * Empty constructor automatically creates a temporary file
99      * that will later hold an image
100      */
101     ImageFile() throws IOException {
102       file = File.createTempFile("mirimage", "");
103       fileIsTemp = true;
104     }
105
106     /**
107      * if the file doesn't already have an image in it
108      * we don't want to read its information
109      */
110     ImageFile(File file, boolean doReadInfo) throws IOException {
111       this.file = file;
112       if (doReadInfo) {
113         readInfo();
114       }
115     }
116
117     ImageFile(File file) throws IOException {
118       this(file, true);
119     }
120
121     /**
122      * delete temporary files
123      */
124     public void cleanup() {
125       logger.debug("ImageFile.cleanup()");
126       if (fileIsTemp) {
127         logger.debug("deleting:" + file);
128         file.delete();
129         file = null;
130         fileIsTemp = false;
131       }
132     }
133
134     void debugOutput() {
135       logger.debug(" filename:" + file +
136           " Info:" +
137           " width:" + width +
138           " height:" + height +
139           " type:" + type +
140           " isAnimation:" + isAnimation);
141     }
142
143     private void checkFile() throws IOException {
144       if (file == null || !file.exists()) {
145         String message = "ImageFile.checkFile file \"" + file +
146             "\" does not exist";
147         logger.error(message);
148         throw new IOException(message);
149       }
150     }
151
152     /**
153      * Uses the imagemagick "identify" command to retreive image information
154      */
155     public void readInfo() throws IOException {
156       checkFile();
157       String infoString = ShellRoutines.execIntoString
158           (getImageMagickPath() +
159               "identify " + "-format \"%w %h %m %n %b \" " + 
160               file.getAbsolutePath()); // extra space, for multiframe (animations)              
161       StringTokenizer st = new StringTokenizer(infoString);
162       width = Integer.parseInt(st.nextToken());
163       height = Integer.parseInt(st.nextToken());
164       type = st.nextToken();
165       isAnimation = Integer.parseInt(st.nextToken()) > 1;
166       // yoss: different versions of ImageMagick produce different formatted output file sizes
167       // for example, Version: ImageMagick 6.2.4 (Ubuntu Dapper 6.06) produces a byte value, i.e. 67013
168       // Version: ImageMagick 6.0.6 (Debian Sarge) produces output like 67kb or 500mb.
169       // Trying to do an int.parse in Sarge fails for obvious reasons.
170       /*String sFileSize = st.nextToken();
171       if (sFileSize.endsWith("kb") || sFileSize.endsWith("Kb") || sFileSize.endsWith("KB") || sFileSize.endsWith("kB")){
172           sFileSize = sFileSize.substring(0, sFileSize.length() - 2);
173           fileSize = 1024 * Integer.parseInt(sFileSize);
174       } else if (sFileSize.endsWith("mb") || sFileSize.endsWith("Mb") || sFileSize.endsWith("MB") || sFileSize.endsWith("mB")){
175           sFileSize = sFileSize.substring(0, sFileSize.length() - 2);
176           fileSize = 1048576 * Integer.parseInt(sFileSize);       
177       } else {
178           fileSize = Integer.parseInt(sFileSize);
179       }*/
180       fileSize = (int)file.length();
181     }
182
183     public ImageFile scale(float aScalingFactor) throws IOException {
184       logger.debug("ImageFile.scale");
185       checkFile();
186       ImageFile result = new ImageFile();
187       String command = getImageMagickPath() + "convert " +
188           file.getAbsolutePath() + " " +
189           "-scale " +
190           Float.toString(aScalingFactor * 100) + "% " +
191           result.file.getAbsolutePath();
192       logger.debug("ImageFile.scale:command:" + command);
193       ShellRoutines.simpleExec(command);
194       result.readInfo();
195       return result;
196     }
197   }
198
199   public ImageMagickImageProcessor(InputStream inputImageStream)
200       throws IOException {
201     logger.debug("ImageMagickImageProcessor(stream)");
202     sourceImage = new ImageFile();
203     // copy stream into temporary file
204
205     FileOutputStream outputStream = new FileOutputStream(sourceImage.file);
206     try {
207       StreamCopier.copy(inputImageStream, outputStream);
208     }
209     finally {
210       outputStream.close();
211     }
212     sourceImage.readInfo();
213     sourceImage.debugOutput();
214   }
215
216
217   public ImageMagickImageProcessor(File aFile) throws IOException {
218     logger.debug("ImageMagickImageProcessor(file)");
219     sourceImage = new ImageFile(aFile);
220     sourceImage.debugOutput();
221   }
222
223   /**
224    * Deletes temporary files
225    */
226   public void cleanup() {
227     logger.debug("ImageMagickImageProcessor.cleanup()");
228     sourceImage.cleanup();
229     scaledImage.cleanup();
230   }
231
232   /**
233    * Path to ImageMagick commandline programs
234    */
235   private static String getImageMagickPath() {
236     String result = configuration.getString("Producer.Image.ImageMagickPath");
237     // we want the path to finish by "/", so add it if it's missing
238     if (result.length() != 0 && !result.endsWith("/")) {
239       result = result.concat("/");
240     }
241     logger.debug("getImageMagickPath:" + result);
242     return result;
243   }
244
245   public void descaleImage(int aMaxSize) throws MediaExc {
246     descaleImage(aMaxSize, 0);
247   }
248
249   public void descaleImage(int aMaxSize, float aMinDescale) throws MediaExc {
250     descaleImage(aMaxSize, aMaxSize, aMinDescale, 0);
251   }
252
253   public void descaleImage(int aMaxSize, int aMinResize) throws MediaExc {
254     descaleImage(aMaxSize, aMaxSize, 0, aMinResize);
255   }
256
257   public void descaleImage(int aMaxSize, float aMinDescale, int aMinResize)
258       throws MediaExc {
259     descaleImage(aMaxSize, aMaxSize, aMinDescale, aMinResize);
260   }
261
262   /**
263    * {@inheritDoc}
264    */
265   public void descaleImage(int aMaxWidth, int aMaxHeight,
266                            float aMinDescale, int aMinResize) throws MediaExc {
267     float scale;
268     logger.debug("descaleImage:" +
269         "  aMaxWidth:" + aMaxWidth +
270         ", aMaxHeight:" + aMaxHeight +
271         ", aMinDescale:" + aMinDescale +
272         ", aMinResize:" + aMinResize);
273     if ((aMaxWidth > 0 && getWidth() > aMaxWidth + aMinResize - 1) ||
274         (aMaxHeight > 0 && getHeight() > aMaxHeight + aMinResize - 1)) {
275       logger.debug("descaleImage: image needs scaling");
276
277       scale = 1;
278
279       if (aMaxWidth > 0 && getWidth() > aMaxWidth) {
280         scale = Math.min(scale, (float) aMaxWidth / (float) getWidth());
281       }
282       if (aMaxHeight > 0 && getHeight() > aMaxHeight) {
283         scale = Math.min(scale, (float) aMaxHeight / (float) getHeight());
284       }
285
286       if (1 - scale > aMinDescale) {
287         scaleImage(scale);
288
289         return;
290       }
291     }
292     logger.debug("descaleImage: image size is ok, not scaling image");
293     try {
294       scaledImage = new ImageFile(sourceImage.file);
295     }
296     catch (IOException e) {
297       throw new MediaExc(e.toString());
298     }
299   }
300
301
302   /**
303    * Scales image by a factor using the convert ImageMagick command
304    */
305   public void scaleImage(float aScalingFactor)
306       throws MediaExc {
307     logger.debug("scaleImage:" + aScalingFactor);
308     try {
309       // first cleanup previous temp scaledimage file if necesary
310       if (scaledImage != null) {
311         scaledImage.cleanup();
312       }
313       // now create temp file and execute convert
314       scaledImage = sourceImage.scale(aScalingFactor);
315     }
316     catch (Exception e) {
317       throw new MediaExc(e.toString());
318     }
319     logger.debug(" scaledImage:");
320     scaledImage.debugOutput();
321   }
322
323   public int getWidth() {
324     return sourceImage.width;
325   }
326
327   public int getHeight() {
328     return sourceImage.height;
329   }
330   
331   public int getSourceFileSize() {
332           return sourceImage.fileSize;
333   }
334   
335   public int getScaledFileSize() {
336           return scaledImage.fileSize;
337   }
338
339   public int getScaledWidth() {
340     return scaledImage.width;
341   }
342
343   public int getScaledHeight() {
344     return scaledImage.height;
345   }
346
347   public void writeScaledData(OutputStream aStream, String anImageType)
348       throws MediaExc {
349     // we can't asume that requested "anImageType" is the same as the
350     // scaled image type, so we have to convert 
351     try {
352       // if image is an animation and target type doesn't support
353       // animations, then just use first frame
354       String frame = "";
355       scaledImage.debugOutput();
356       if (scaledImage.isAnimation && !anImageType.equals("GIF")) {
357         frame = "[0]";
358       }
359       // ImageMagick "convert" into temp file
360       File temp = File.createTempFile("mirimage", "");
361       String command = getImageMagickPath() + "convert " +
362           scaledImage.file.getAbsolutePath() + frame + " " +
363           anImageType + ":" + temp.getAbsolutePath();
364       logger.debug("writeScaledData command:" + command);
365       ShellRoutines.simpleExec(command);
366       // copy temp file into stream
367       StreamCopier.copy(new FileInputStream(temp), aStream);
368       temp.delete();
369     }
370     catch (Exception e) {
371       throw new MediaExc(e.toString());
372     }
373   }
374
375   public byte[] getScaledData(String anImageType) throws MediaExc {
376     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
377     writeScaledData(outputStream, anImageType);
378     return outputStream.toByteArray();
379   }
380
381   public void writeScaledData(File aFile, String anImageType) throws MediaExc {
382     try {
383       OutputStream stream = new BufferedOutputStream(new FileOutputStream(aFile), 8192);
384
385       try {
386         writeScaledData(stream, anImageType);
387       }
388       finally {
389         try {
390           stream.close();
391         }
392         catch (Throwable t) {
393                 logger.debug("Unable to close stream when writing scaled data.");
394         }
395       }
396     }
397     catch (FileNotFoundException f) {
398       throw new MediaFailure(f);
399     }
400     catch (Exception e) {
401         logger.debug("Exception caught while trying to write scaled data: " + e.toString());
402     }
403   }
404 }