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