Adding a new ImageMagickImageProcessor class to replace
[mir.git] / source / mir / media / image / ImageMagickImageProcessor.java
diff --git a/source/mir/media/image/ImageMagickImageProcessor.java b/source/mir/media/image/ImageMagickImageProcessor.java
new file mode 100755 (executable)
index 0000000..3838634
--- /dev/null
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2001, 2002 The Mir-coders group
+ *
+ * This file is part of Mir.
+ *
+ * Mir is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Mir is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mir; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * In addition, as a special exception, The Mir-coders gives permission to link
+ * the code of this program with  any library licensed under the Apache Software License,
+ * The Sun (tm) Java Advanced Imaging library (JAI), The Sun JIMI library
+ * (or with modified versions of the above that use the same license as the above),
+ * and distribute linked combinations including the two.  You must obey the
+ * GNU General Public License in all respects for all of the code used other than
+ * the above mentioned libraries.  If you modify this file, you may extend this
+ * exception to your version of the file, but you are not obligated to do so.
+ * If you do not wish to do so, delete this exception statement from your version.
+ */
+
+package mir.media.image;
+
+import mir.log.LoggerWrapper;
+import mir.media.MediaExc;
+import mir.media.MediaFailure;
+import mir.util.StreamCopier;
+import mir.util.ExecFunctions;
+import mir.config.MirPropertiesConfiguration;
+
+import java.io.*;
+import java.util.StringTokenizer;
+
+
+
+/**
+ * Image processing by calling the ImageMagick command line progrmas
+ * "convert" and "identify". The main task of this class is to scale
+ * images. The path to ImageMagick commandline programs can be
+ * specified in the coonfiguration file.
+ * @author <grok@no-log.org>, the Mir-coders group
+ */
+public class ImageMagickImageProcessor implements ImageProcessor 
+{
+  protected static MirPropertiesConfiguration configuration = 
+    MirPropertiesConfiguration.instance();
+  static final LoggerWrapper logger = 
+    new LoggerWrapper("media.image.imagemagick");
+
+  private ImageFile sourceImage;
+  private ImageFile scaledImage;
+
+  /**
+   * ImageFile  is a  thin  wrapper  around a  file  that contains  an
+   * image.  It uses  ImageMagick  to retreive  information about  the
+   * image. It  can also scale images using  ImageMagick. Intended for
+   * use in the ImageMagickImageProcessor class.
+   */
+  static class ImageFile
+  {
+    /** path to the file represented by this class */
+    File file;
+    /** whether the file must be deleted on cleanup */
+    boolean fileIsTemp=false;
+    /** image information is stored here to avoid multiple costly calls to 
+     *  "identify" */
+    int width;
+    int height;
+    /** Image type as returned by identify %m : "PNG", "GIF", ...  */
+    String type;
+    /** number of scenes in image >1 (typically animated gif) */
+    boolean isAnimation;
+
+    /** Empty constructor automatically creates a temporary file 
+     * that will later hold an image */
+    ImageFile() throws IOException
+    {
+      file=File.createTempFile("mirimage","");
+      fileIsTemp=true;
+    }
+    /** if the file doesn't already have an image in it 
+     * we don't want to read its information */
+    ImageFile(File file,boolean doReadInfo) throws IOException
+    {
+      this.file=file;
+      if(doReadInfo){readInfo();}
+    }
+    ImageFile(File file) throws IOException
+    {
+      this(file,true);
+    }
+    /** delete temporary files  */
+    public void cleanup()
+    {
+      logger.debug("ImageFile.cleanup()");
+      if(fileIsTemp)
+      { 
+        logger.debug("deleting:"+file);
+        file.delete();
+        file=null;
+        fileIsTemp=false;
+      }
+    }
+    void debugOutput()
+    {
+      logger.debug(" filename:"+file+
+                   " Info:"+
+                   " width:"+width+
+                   " height:"+height+
+                   " type:"+type+
+                   " isAnimation:"+isAnimation);
+    }
+    private void checkFile() throws IOException
+    {
+      if(file==null || !file.exists())
+      {
+        String message="ImageFile.checkFile file \""+file+
+          "\" does not exist";
+        logger.error(message);
+        throw new IOException(message);
+      }
+    }
+
+    /** Uses the imagemagick "identify" command to retreive image information */
+    public void readInfo() throws IOException
+    {
+      checkFile();
+      String infoString=ExecFunctions.execIntoString
+        (getImageMagickPath()+
+         "identify "+
+         file.getAbsolutePath()+" "+
+         "-format \"%w %h %m %n \"");// extra space, for multiframe (animations)
+      StringTokenizer st = new StringTokenizer(infoString);
+      width      =Integer.parseInt(st.nextToken());
+      height     =Integer.parseInt(st.nextToken());
+      type       =                 st.nextToken() ;
+      isAnimation=Integer.parseInt(st.nextToken())>1;
+    } 
+
+    public ImageFile scale(float aScalingFactor) throws IOException
+    {
+      logger.debug("ImageFile.scale");
+      checkFile();
+      ImageFile result=new ImageFile();
+      String command=getImageMagickPath()+"convert "+
+        file.getAbsolutePath()+" "+
+        "-scale "+
+        new Float(aScalingFactor*100).toString()+"% "+
+        result.file.getAbsolutePath();
+      logger.debug("ImageFile.scale:command:"+command);
+      ExecFunctions.simpleExec(command);
+      result.readInfo();
+      return result;
+    }
+  }
+
+  public ImageMagickImageProcessor(InputStream inputImageStream)
+    throws IOException
+  {
+    logger.debug("ImageMagickImageProcessor(stream)");
+    sourceImage=new ImageFile();
+    // copy stream into temporary file 
+    StreamCopier.copy(inputImageStream,new FileOutputStream(sourceImage.file));
+    sourceImage.readInfo();
+    sourceImage.debugOutput();
+  }
+
+
+  public ImageMagickImageProcessor(File aFile) throws IOException 
+  {
+    logger.debug("ImageMagickImageProcessor(file)");
+    sourceImage=new ImageFile(aFile);
+    sourceImage.debugOutput();
+  }
+
+  /**
+   * Deletes temporary files 
+   */
+  public void cleanup()
+  {
+    logger.debug("ImageMagickImageProcessor.cleanup()");
+    sourceImage.cleanup();
+    scaledImage.cleanup();
+  }
+
+  /**
+   * Path to ImageMagick commandline programs
+   */
+  private static String getImageMagickPath()
+  {
+    String result=configuration.getString("Producer.Image.ImageMagickPath");
+    // we want the path to finish by "/", so add it if it's missing
+    if(result.length()!=0 && !result.endsWith("/"))
+    {
+      result=result.concat("/");
+    }
+    logger.debug("getImageMagickPath:"+result);
+    return result;
+  }
+
+  public void descaleImage(int aMaxSize) throws MediaExc {
+    descaleImage(aMaxSize, 0);
+  }
+
+  public void descaleImage(int aMaxSize, float aMinDescale) throws MediaExc {
+    descaleImage(aMaxSize, aMaxSize, aMinDescale, 0);
+  }
+
+  public void descaleImage(int aMaxSize, int aMinResize) throws MediaExc 
+  {
+    descaleImage(aMaxSize, aMaxSize, 0, aMinResize);
+  }
+
+  public void descaleImage(int aMaxSize, float aMinDescale, int aMinResize) 
+    throws MediaExc 
+  {
+    descaleImage(aMaxSize, aMaxSize, aMinDescale, aMinResize);
+  }
+
+  /** {@inheritDoc} */
+  public void descaleImage(int   aMaxWidth, int aMaxHeight, 
+                           float aMinDescale, int aMinResize) throws MediaExc 
+  {
+    float scale;
+    logger.debug("descaleImage:"+
+                 "  aMaxWidth:"+aMaxWidth+
+                 ", aMaxHeight:"+aMaxHeight+
+                 ", aMinDescale:"+aMinDescale+
+                 ", aMinResize:"+aMinResize);
+    if ((aMaxWidth >0 && getWidth ()>aMaxWidth +aMinResize-1) || 
+        (aMaxHeight>0 && getHeight()>aMaxHeight+aMinResize-1))
+    {
+      logger.debug("descaleImage: image needs scaling");
+
+      scale=1;
+
+      if (aMaxWidth>0 && getWidth()>aMaxWidth) 
+      {
+        scale = Math.min(scale, (float) aMaxWidth / (float) getWidth());
+      }
+      if (aMaxHeight>0 && getHeight()>aMaxHeight) 
+      {
+        scale = Math.min(scale, (float) aMaxHeight / (float) getHeight());
+      }
+
+      if (1-scale>aMinDescale) 
+      {
+        scaleImage(scale);
+      }
+    }
+    else
+    {
+      logger.debug("descaleImage: image size is ok, not scaling image");
+      try{scaledImage=new ImageFile(sourceImage.file);}
+      catch(IOException e){throw new MediaExc(e.toString());}
+    }
+  }
+
+
+  /**
+   * Scales image by a factor using the convert ImageMagick command
+   */
+  public void scaleImage(float aScalingFactor) 
+    throws MediaExc 
+  {
+    logger.debug("scaleImage:"+aScalingFactor);
+    try
+    {
+      // first cleanup previous temp scaledimage file if necesary
+      if(scaledImage!=null){scaledImage.cleanup();}
+      // now create temp file and execute convert
+      scaledImage=sourceImage.scale(aScalingFactor);
+    }
+    catch(Exception e){throw new MediaExc(e.toString());}
+    logger.debug(" scaledImage:");
+    scaledImage.debugOutput();
+  }
+
+  public int getWidth() {
+    return sourceImage.width;
+  }
+
+  public int getHeight() {
+    return sourceImage.height;
+  }
+
+  public int getScaledWidth() {
+    return scaledImage.width;
+  }
+
+  public int getScaledHeight() {
+    return scaledImage.height;
+  }
+
+  public void writeScaledData(OutputStream aStream, String anImageType) 
+    throws MediaExc
+  {
+    // we can't asume that requested "anImageType" is the same as the
+    // scaled image type, so we have to convert 
+    try
+    {
+      // if image is an animation and target type doesn't support
+      // animations, then just use first frame
+      String frame="";
+      scaledImage.debugOutput();
+      if(scaledImage.isAnimation && !anImageType.equals("GIF"))
+      {
+        frame="[0]";
+      }
+      // ImageMagick "convert" into temp file
+      File temp=File.createTempFile("mirimage","");
+      String command=getImageMagickPath()+"convert "+
+        scaledImage.file.getAbsolutePath()+frame+" "+
+        anImageType+":"+temp.getAbsolutePath();
+      logger.debug("writeScaledData command:"+command);
+      ExecFunctions.simpleExec(command);
+      // copy temp file into stream
+      StreamCopier.copy(new FileInputStream(temp),aStream);
+      temp.delete();
+    }
+    catch(Exception e){throw new MediaExc(e.toString());}
+  }
+
+  public byte[] getScaledData(String anImageType) throws MediaExc 
+  {
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    writeScaledData(outputStream, anImageType);
+    return outputStream.toByteArray();
+  }
+
+  public void writeScaledData(File aFile, String anImageType) throws MediaExc {
+    try {
+      writeScaledData(new BufferedOutputStream
+                      (new FileOutputStream(aFile),8192), anImageType);
+    }
+    catch (FileNotFoundException f) {
+      throw new MediaFailure(f);
+    }
+  }
+}