+/*
+ * 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);
+ }
+ }
+}