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