2 * Copyright (C) 2001, 2002 The Mir-coders group
4 * This file is part of Mir.
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.
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.
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
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.
31 package mir.media.image;
33 import mir.log.LoggerWrapper;
34 import mir.media.MediaExc;
35 import mir.media.MediaFailure;
36 import mir.util.StreamCopier;
37 import mir.util.ShellRoutines;
38 import mir.config.MirPropertiesConfiguration;
41 import java.util.StringTokenizer;
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.
50 * @author <grok@no-log.org>, the Mir-coders group
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");
58 private ImageFile sourceImage;
59 private ImageFile scaledImage;
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.
67 static class ImageFile {
69 * path to the file represented by this class
73 * whether the file must be deleted on cleanup
75 boolean fileIsTemp = false;
77 * image information is stored here to avoid multiple costly calls to
83 * Image type as returned by identify %m : "PNG", "GIF", ...
87 * number of scenes in image >1 (typically animated gif)
92 * Empty constructor automatically creates a temporary file
93 * that will later hold an image
95 ImageFile() throws IOException {
96 file = File.createTempFile("mirimage", "");
101 * if the file doesn't already have an image in it
102 * we don't want to read its information
104 ImageFile(File file, boolean doReadInfo) throws IOException {
111 ImageFile(File file) throws IOException {
116 * delete temporary files
118 public void cleanup() {
119 logger.debug("ImageFile.cleanup()");
121 logger.debug("deleting:" + file);
129 logger.debug(" filename:" + file +
132 " height:" + height +
134 " isAnimation:" + isAnimation);
137 private void checkFile() throws IOException {
138 if (file == null || !file.exists()) {
139 String message = "ImageFile.checkFile file \"" + file +
141 logger.error(message);
142 throw new IOException(message);
147 * Uses the imagemagick "identify" command to retreive image information
149 public void readInfo() throws IOException {
151 String infoString = ShellRoutines.execIntoString
152 (getImageMagickPath() +
153 "identify " + "-format \"%w %h %m %n \" " +
154 file.getAbsolutePath()); // extra space, for multiframe (animations)
155 StringTokenizer st = new StringTokenizer(infoString);
156 width = Integer.parseInt(st.nextToken());
157 height = Integer.parseInt(st.nextToken());
158 type = st.nextToken();
159 isAnimation = Integer.parseInt(st.nextToken()) > 1;
162 public ImageFile scale(float aScalingFactor) throws IOException {
163 logger.debug("ImageFile.scale");
165 ImageFile result = new ImageFile();
166 String command = getImageMagickPath() + "convert " +
167 file.getAbsolutePath() + " " +
169 Float.toString(aScalingFactor * 100) + "% " +
170 result.file.getAbsolutePath();
171 logger.debug("ImageFile.scale:command:" + command);
172 ShellRoutines.simpleExec(command);
178 public ImageMagickImageProcessor(InputStream inputImageStream)
180 logger.debug("ImageMagickImageProcessor(stream)");
181 sourceImage = new ImageFile();
182 // copy stream into temporary file
184 FileOutputStream outputStream = new FileOutputStream(sourceImage.file);
186 StreamCopier.copy(inputImageStream, outputStream);
189 outputStream.close();
191 sourceImage.readInfo();
192 sourceImage.debugOutput();
196 public ImageMagickImageProcessor(File aFile) throws IOException {
197 logger.debug("ImageMagickImageProcessor(file)");
198 sourceImage = new ImageFile(aFile);
199 sourceImage.debugOutput();
203 * Deletes temporary files
205 public void cleanup() {
206 logger.debug("ImageMagickImageProcessor.cleanup()");
207 sourceImage.cleanup();
208 scaledImage.cleanup();
212 * Path to ImageMagick commandline programs
214 private static String getImageMagickPath() {
215 String result = configuration.getString("Producer.Image.ImageMagickPath");
216 // we want the path to finish by "/", so add it if it's missing
217 if (result.length() != 0 && !result.endsWith("/")) {
218 result = result.concat("/");
220 logger.debug("getImageMagickPath:" + result);
224 public void descaleImage(int aMaxSize) throws MediaExc {
225 descaleImage(aMaxSize, 0);
228 public void descaleImage(int aMaxSize, float aMinDescale) throws MediaExc {
229 descaleImage(aMaxSize, aMaxSize, aMinDescale, 0);
232 public void descaleImage(int aMaxSize, int aMinResize) throws MediaExc {
233 descaleImage(aMaxSize, aMaxSize, 0, aMinResize);
236 public void descaleImage(int aMaxSize, float aMinDescale, int aMinResize)
238 descaleImage(aMaxSize, aMaxSize, aMinDescale, aMinResize);
244 public void descaleImage(int aMaxWidth, int aMaxHeight,
245 float aMinDescale, int aMinResize) throws MediaExc {
247 logger.debug("descaleImage:" +
248 " aMaxWidth:" + aMaxWidth +
249 ", aMaxHeight:" + aMaxHeight +
250 ", aMinDescale:" + aMinDescale +
251 ", aMinResize:" + aMinResize);
252 if ((aMaxWidth > 0 && getWidth() > aMaxWidth + aMinResize - 1) ||
253 (aMaxHeight > 0 && getHeight() > aMaxHeight + aMinResize - 1)) {
254 logger.debug("descaleImage: image needs scaling");
258 if (aMaxWidth > 0 && getWidth() > aMaxWidth) {
259 scale = Math.min(scale, (float) aMaxWidth / (float) getWidth());
261 if (aMaxHeight > 0 && getHeight() > aMaxHeight) {
262 scale = Math.min(scale, (float) aMaxHeight / (float) getHeight());
265 if (1 - scale > aMinDescale) {
269 logger.debug("descaleImage: image size is ok, not scaling image");
271 scaledImage = new ImageFile(sourceImage.file);
273 catch (IOException e) {
274 throw new MediaExc(e.toString());
281 * Scales image by a factor using the convert ImageMagick command
283 public void scaleImage(float aScalingFactor)
285 logger.debug("scaleImage:" + aScalingFactor);
287 // first cleanup previous temp scaledimage file if necesary
288 if (scaledImage != null) {
289 scaledImage.cleanup();
291 // now create temp file and execute convert
292 scaledImage = sourceImage.scale(aScalingFactor);
294 catch (Exception e) {
295 throw new MediaExc(e.toString());
297 logger.debug(" scaledImage:");
298 scaledImage.debugOutput();
301 public int getWidth() {
302 return sourceImage.width;
305 public int getHeight() {
306 return sourceImage.height;
309 public int getScaledWidth() {
310 return scaledImage.width;
313 public int getScaledHeight() {
314 return scaledImage.height;
317 public void writeScaledData(OutputStream aStream, String anImageType)
319 // we can't asume that requested "anImageType" is the same as the
320 // scaled image type, so we have to convert
322 // if image is an animation and target type doesn't support
323 // animations, then just use first frame
325 scaledImage.debugOutput();
326 if (scaledImage.isAnimation && !anImageType.equals("GIF")) {
329 // ImageMagick "convert" into temp file
330 File temp = File.createTempFile("mirimage", "");
331 String command = getImageMagickPath() + "convert " +
332 scaledImage.file.getAbsolutePath() + frame + " " +
333 anImageType + ":" + temp.getAbsolutePath();
334 logger.debug("writeScaledData command:" + command);
335 ShellRoutines.simpleExec(command);
336 // copy temp file into stream
337 StreamCopier.copy(new FileInputStream(temp), aStream);
340 catch (Exception e) {
341 throw new MediaExc(e.toString());
345 public byte[] getScaledData(String anImageType) throws MediaExc {
346 ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
347 writeScaledData(outputStream, anImageType);
348 return outputStream.toByteArray();
351 public void writeScaledData(File aFile, String anImageType) throws MediaExc {
353 OutputStream stream = new BufferedOutputStream(new FileOutputStream(aFile), 8192);
356 writeScaledData(stream, anImageType);
362 catch (Throwable t) {
366 catch (FileNotFoundException f) {
367 throw new MediaFailure(f);