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