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