2 * Copyright (C) 2005 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 * You must obey the GNU General Public License in all respects for all of the code used
23 * other than the above mentioned libraries. If you modify this file, you may extend this
24 * exception to your version of the file, but you are not obligated to do so.
25 * If you do not wish to do so, delete this exception statement from your version.
27 package mir.media.image;
29 import mir.log.LoggerWrapper;
30 import mir.media.MediaExc;
31 import mir.media.MediaFailure;
32 import mir.util.StreamCopier;
33 import mir.util.ShellRoutines;
34 import mir.config.MirPropertiesConfiguration;
36 import java.io.BufferedOutputStream;
37 import java.io.ByteArrayOutputStream;
39 import java.io.FileInputStream;
40 import java.io.FileNotFoundException;
41 import java.io.FileOutputStream;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.io.OutputStream;
45 import java.util.StringTokenizer;
49 * Image processing by calling the ImageMagick command line progrmas
50 * "convert" and "identify". The main task of this class is to scale
51 * images. The path to ImageMagick commandline programs can be
52 * specified in the coonfiguration file.
54 * @author <grok@no-log.org>, the Mir-coders group
56 public class ImageMagickImageProcessor implements ImageProcessor {
57 protected static MirPropertiesConfiguration configuration =
58 MirPropertiesConfiguration.instance();
59 static final LoggerWrapper logger =
60 new LoggerWrapper("media.image.imagemagick");
62 private ImageFile sourceImage;
63 private ImageFile scaledImage;
66 * ImageFile is a thin wrapper around a file that contains an
67 * image. It uses ImageMagick to retreive information about the
68 * image. It can also scale images using ImageMagick. Intended for
69 * use in the ImageMagickImageProcessor class.
71 static class ImageFile {
73 * path to the file represented by this class
77 * whether the file must be deleted on cleanup
79 boolean fileIsTemp = false;
81 * image information is stored here to avoid multiple costly calls to
89 * Image type as returned by identify %m : "PNG", "GIF", ...
93 * number of scenes in image >1 (typically animated gif)
98 * Empty constructor automatically creates a temporary file
99 * that will later hold an image
101 ImageFile() throws IOException {
102 file = File.createTempFile("mirimage", "");
107 * if the file doesn't already have an image in it
108 * we don't want to read its information
110 ImageFile(File file, boolean doReadInfo) throws IOException {
117 ImageFile(File file) throws IOException {
122 * delete temporary files
124 public void cleanup() {
125 logger.debug("ImageFile.cleanup()");
127 logger.debug("deleting:" + file);
135 logger.debug(" filename:" + file +
138 " height:" + height +
140 " isAnimation:" + isAnimation);
143 private void checkFile() throws IOException {
144 if (file == null || !file.exists()) {
145 String message = "ImageFile.checkFile file \"" + file +
147 logger.error(message);
148 throw new IOException(message);
153 * Uses the imagemagick "identify" command to retreive image information
155 public void readInfo() throws IOException {
157 String infoString = ShellRoutines.execIntoString
158 (getImageMagickPath() +
159 "identify " + "-format \"%w %h %m %n %b \" " +
160 file.getAbsolutePath()); // extra space, for multiframe (animations)
161 StringTokenizer st = new StringTokenizer(infoString);
162 width = Integer.parseInt(st.nextToken());
163 height = Integer.parseInt(st.nextToken());
164 type = st.nextToken();
165 isAnimation = Integer.parseInt(st.nextToken()) > 1;
166 // yoss: different versions of ImageMagick produce different formatted output file sizes
167 // for example, Version: ImageMagick 6.2.4 (Ubuntu Dapper 6.06) produces a byte value, i.e. 67013
168 // Version: ImageMagick 6.0.6 (Debian Sarge) produces output like 67kb or 500mb.
169 // Trying to do an int.parse in Sarge fails for obvious reasons.
170 /*String sFileSize = st.nextToken();
171 if (sFileSize.endsWith("kb") || sFileSize.endsWith("Kb") || sFileSize.endsWith("KB") || sFileSize.endsWith("kB")){
172 sFileSize = sFileSize.substring(0, sFileSize.length() - 2);
173 fileSize = 1024 * Integer.parseInt(sFileSize);
174 } else if (sFileSize.endsWith("mb") || sFileSize.endsWith("Mb") || sFileSize.endsWith("MB") || sFileSize.endsWith("mB")){
175 sFileSize = sFileSize.substring(0, sFileSize.length() - 2);
176 fileSize = 1048576 * Integer.parseInt(sFileSize);
178 fileSize = Integer.parseInt(sFileSize);
180 fileSize = (int)file.length();
183 public ImageFile scale(float aScalingFactor) throws IOException {
184 logger.debug("ImageFile.scale");
186 ImageFile result = new ImageFile();
187 String command = getImageMagickPath() + "convert " +
188 file.getAbsolutePath() + " " +
190 Float.toString(aScalingFactor * 100) + "% " +
191 result.file.getAbsolutePath();
192 logger.debug("ImageFile.scale:command:" + command);
193 ShellRoutines.simpleExec(command);
199 public ImageMagickImageProcessor(InputStream inputImageStream)
201 logger.debug("ImageMagickImageProcessor(stream)");
202 sourceImage = new ImageFile();
203 // copy stream into temporary file
205 FileOutputStream outputStream = new FileOutputStream(sourceImage.file);
207 StreamCopier.copy(inputImageStream, outputStream);
210 outputStream.close();
212 sourceImage.readInfo();
213 sourceImage.debugOutput();
217 public ImageMagickImageProcessor(File aFile) throws IOException {
218 logger.debug("ImageMagickImageProcessor(file)");
219 sourceImage = new ImageFile(aFile);
220 sourceImage.debugOutput();
224 * Deletes temporary files
226 public void cleanup() {
227 logger.debug("ImageMagickImageProcessor.cleanup()");
228 sourceImage.cleanup();
229 scaledImage.cleanup();
233 * Path to ImageMagick commandline programs
235 private static String getImageMagickPath() {
236 String result = configuration.getString("Producer.Image.ImageMagickPath");
237 // we want the path to finish by "/", so add it if it's missing
238 if (result.length() != 0 && !result.endsWith("/")) {
239 result = result.concat("/");
241 logger.debug("getImageMagickPath:" + result);
245 public void descaleImage(int aMaxSize) throws MediaExc {
246 descaleImage(aMaxSize, 0);
249 public void descaleImage(int aMaxSize, float aMinDescale) throws MediaExc {
250 descaleImage(aMaxSize, aMaxSize, aMinDescale, 0);
253 public void descaleImage(int aMaxSize, int aMinResize) throws MediaExc {
254 descaleImage(aMaxSize, aMaxSize, 0, aMinResize);
257 public void descaleImage(int aMaxSize, float aMinDescale, int aMinResize)
259 descaleImage(aMaxSize, aMaxSize, aMinDescale, aMinResize);
265 public void descaleImage(int aMaxWidth, int aMaxHeight,
266 float aMinDescale, int aMinResize) throws MediaExc {
268 logger.debug("descaleImage:" +
269 " aMaxWidth:" + aMaxWidth +
270 ", aMaxHeight:" + aMaxHeight +
271 ", aMinDescale:" + aMinDescale +
272 ", aMinResize:" + aMinResize);
273 if ((aMaxWidth > 0 && getWidth() > aMaxWidth + aMinResize - 1) ||
274 (aMaxHeight > 0 && getHeight() > aMaxHeight + aMinResize - 1)) {
275 logger.debug("descaleImage: image needs scaling");
279 if (aMaxWidth > 0 && getWidth() > aMaxWidth) {
280 scale = Math.min(scale, (float) aMaxWidth / (float) getWidth());
282 if (aMaxHeight > 0 && getHeight() > aMaxHeight) {
283 scale = Math.min(scale, (float) aMaxHeight / (float) getHeight());
286 if (1 - scale > aMinDescale) {
292 logger.debug("descaleImage: image size is ok, not scaling image");
294 scaledImage = new ImageFile(sourceImage.file);
296 catch (IOException e) {
297 throw new MediaExc(e.toString());
303 * Scales image by a factor using the convert ImageMagick command
305 public void scaleImage(float aScalingFactor)
307 logger.debug("scaleImage:" + aScalingFactor);
309 // first cleanup previous temp scaledimage file if necesary
310 if (scaledImage != null) {
311 scaledImage.cleanup();
313 // now create temp file and execute convert
314 scaledImage = sourceImage.scale(aScalingFactor);
316 catch (Exception e) {
317 throw new MediaExc(e.toString());
319 logger.debug(" scaledImage:");
320 scaledImage.debugOutput();
323 public int getWidth() {
324 return sourceImage.width;
327 public int getHeight() {
328 return sourceImage.height;
331 public int getSourceFileSize() {
332 return sourceImage.fileSize;
335 public int getScaledFileSize() {
336 return scaledImage.fileSize;
339 public int getScaledWidth() {
340 return scaledImage.width;
343 public int getScaledHeight() {
344 return scaledImage.height;
347 public void writeScaledData(OutputStream aStream, String anImageType)
349 // we can't asume that requested "anImageType" is the same as the
350 // scaled image type, so we have to convert
352 // if image is an animation and target type doesn't support
353 // animations, then just use first frame
355 scaledImage.debugOutput();
356 if (scaledImage.isAnimation && !anImageType.equals("GIF")) {
359 // ImageMagick "convert" into temp file
360 File temp = File.createTempFile("mirimage", "");
361 String command = getImageMagickPath() + "convert " +
362 scaledImage.file.getAbsolutePath() + frame + " " +
363 anImageType + ":" + temp.getAbsolutePath();
364 logger.debug("writeScaledData command:" + command);
365 ShellRoutines.simpleExec(command);
366 // copy temp file into stream
367 StreamCopier.copy(new FileInputStream(temp), aStream);
370 catch (Exception e) {
371 throw new MediaExc(e.toString());
375 public byte[] getScaledData(String anImageType) throws MediaExc {
376 ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
377 writeScaledData(outputStream, anImageType);
378 return outputStream.toByteArray();
381 public void writeScaledData(File aFile, String anImageType) throws MediaExc {
383 OutputStream stream = new BufferedOutputStream(new FileOutputStream(aFile), 8192);
386 writeScaledData(stream, anImageType);
392 catch (Throwable t) {
393 logger.debug("Unable to close stream when writing scaled data.");
397 catch (FileNotFoundException f) {
398 throw new MediaFailure(f);
400 catch (Exception e) {
401 logger.debug("Exception caught while trying to write scaled data: " + e.toString());