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