KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > mmbase > util > images > ImageMagickImageConverter


1 /*
2
3 This software is OSI Certified Open Source Software.
4 OSI Certified is a certification mark of the Open Source Initiative.
5
6 The license (Mozilla version 1.0) can be read at the MMBase site.
7 See http://www.MMBase.org/license
8
9  */

10 package org.mmbase.util.images;
11
12 import java.util.*;
13 import java.io.*;
14 import java.util.regex.*;
15
16 import org.mmbase.util.externalprocess.CommandLauncher;
17 import org.mmbase.util.externalprocess.ProcessException;
18 import org.mmbase.util.Encode;
19
20 import org.mmbase.util.logging.Logging;
21 import org.mmbase.util.logging.Logger;
22
23 /**
24  * Converts images using ImageMagick.
25  *
26  * @author Rico Jansen
27  * @author Michiel Meeuwissen
28  * @author Nico Klasens
29  * @author Jaco de Groot
30  * @version $Id: ImageMagickImageConverter.java,v 1.4.2.1 2006/12/01 15:23:42 michiel Exp $
31  */

32 public class ImageMagickImageConverter implements ImageConverter {
33     private static final Logger log = Logging.getLoggerInstance(ImageMagickImageConverter.class);
34
35     private static final Pattern IM_VERSION_PATTERN = Pattern.compile("(?is).*?\\s(\\d+)\\.(\\d+)\\.(\\d+)\\s.*");
36
37     private int imVersionMajor = 6;
38     private int imVersionMinor = 2;
39     private int imVersionPatch = 4;
40
41     // Currently only ImageMagick works, this are the default value's
42
private static String JavaDoc converterPath = "convert"; // in the path.
43

44     private static int colorizeHexScale = 100;
45     // The modulate scale base holds the builder property to specify the scalebase.
46
// If ModulateScaleBase property is not defined, then value stays max int.
47
private static int modulateScaleBase = Integer.MAX_VALUE;
48
49     // private static String CONVERT_LC_ALL= "LC_ALL=en_US.UTF-8"; I don't know how to change it.
50

51
52     /**
53      * This function initalises this class
54      * @param params a <code>Map</code> of <code>String</code>s containing informationn, this should contain the key's
55      * ImageConvert.ConverterRoot and ImageConvert.ConverterCommand specifing the converter root, and it can also contain
56      * ImageConvert.DefaultImageFormat which can also be 'asis'.
57      */

58     public void init(Map params) {
59         String JavaDoc converterRoot = "";
60         String JavaDoc converterCommand = "convert";
61
62         String JavaDoc tmp;
63         tmp = (String JavaDoc) params.get("ImageConvert.ConverterRoot");
64         if (tmp != null && ! tmp.equals("")) {
65             converterRoot = tmp;
66         }
67
68         tmp = (String JavaDoc) params.get("ImageConvert.ConverterCommand");
69         if (tmp != null && ! tmp.equals("")) {
70             converterCommand = tmp;
71         }
72
73         if(System.getProperty("os.name") != null && System.getProperty("os.name").startsWith("Windows")) {
74             // on the windows system, we _can_ assume the it uses .exe as extention...
75
// otherwise the check on existance of the program will fail.
76
if (!converterCommand.endsWith(".exe")) {
77                 converterCommand += ".exe";
78             }
79         }
80
81         String JavaDoc configFile = params.get("configfile").toString();
82         if (configFile == null) configFile = "images builder xml";
83
84         converterPath = converterCommand; // default.
85
if (!converterRoot.equals("")) { // also a root was indicated, add it..
86
// now check if the specified ImageConvert.converterRoot does exist and is a directory
87
File checkConvDir = new File(converterRoot).getAbsoluteFile();
88             if (!checkConvDir.exists()) {
89                 log.error( "ImageConvert.ConverterRoot " + converterRoot + " in " + configFile + " does not exist");
90             } else if (!checkConvDir.isDirectory()) {
91                 log.error( "ImageConvert.ConverterRoot " + converterRoot + " in " + configFile + " is not a directory");
92             } else {
93                 // now check if the specified ImageConvert.Command does exist and is a file..
94
File checkConvCom = new File(converterRoot, converterCommand);
95                 converterPath = checkConvCom.toString();
96                 if (!checkConvCom.exists()) {
97                     log.error( converterPath + " specified by " + configFile + " does not exist");
98                 } else if (!checkConvCom.isFile()) {
99                     log.error( converterPath + " specified by " + configFile + " is not a file");
100                 }
101             }
102         }
103         // do a test-run, maybe slow during startup, but when it is done this way, we can also output some additional info in the log about version..
104
// and when somebody has failure with converting images, it is much earlier detectable, when it wrong in settings, since it are settings of
105
// the builder...
106

107         // TODO: on error switch to Dummy????
108
// TODO: research how we tell convert, that is should use the System.getProperty(); with respective the value's 'java.io.tmpdir', 'user.dir'
109
// this, since convert writes at this moment inside the 'user.dir'(working dir), which isnt writeable all the time.
110

111         ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
112         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
113         try {
114             CommandLauncher launcher = new CommandLauncher("ConvertImage");
115             log.debug("Starting convert");
116             List cmd = new ArrayList();
117             cmd.add("-version");
118             launcher.execute(converterPath, (String JavaDoc[]) cmd.toArray(new String JavaDoc[] {}));
119             launcher.waitAndRead(outputStream, errorStream);
120         } catch (ProcessException e) {
121             log.error("Convert test failed. " + converterPath + " (" + e.toString() + ") conv.root='" + converterRoot
122                       + "' conv.command='" + converterCommand + "'", e);
123         }
124
125         String JavaDoc imOutput = outputStream.toString();
126         Matcher m = IM_VERSION_PATTERN.matcher(imOutput);
127         if (m.matches()) {
128             imVersionMajor = Integer.parseInt(m.group(1));
129             imVersionMinor = Integer.parseInt(m.group(2));
130             imVersionPatch = Integer.parseInt(m.group(3));
131             log.info("Found ImageMagick version " + imVersionMajor + "." + imVersionMinor + "." + imVersionPatch);
132         } else {
133             log.error( "converter from location " + converterPath + ", gave strange result: " + imOutput
134                        + "conv.root='" + converterRoot + "' conv.command='" + converterCommand + "'");
135             log.info("Supposing ImageMagick version " + imVersionMajor + "." + imVersionMinor + "." + imVersionPatch);
136
137         }
138
139         // Cant do more checking then this, i think....
140
tmp = (String JavaDoc) params.get("ImageConvert.ColorizeHexScale");
141         if (tmp != null) {
142             try {
143                 colorizeHexScale = Integer.parseInt(tmp);
144             } catch (NumberFormatException JavaDoc e) {
145                 log.error( "Property ImageConvert.ColorizeHexScale should be an integer: " + e.toString() + "conv.root='" + converterRoot + "' conv.command='" + converterCommand + "'");
146             }
147         }
148         // See if the modulate scale base is defined. If not defined, it will be ignored.
149
log.debug("Searching for ModulateScaleBase property.");
150         tmp = (String JavaDoc) params.get("ImageConvert.ModulateScaleBase");
151         if (tmp != null) {
152             try {
153                 modulateScaleBase = Integer.parseInt(tmp);
154             } catch (NumberFormatException JavaDoc nfe) {
155                 log.error( "Property ImageConvert.ModulateScaleBase should be an integer, instead of:'" + tmp + "'" + ", conv.root='" + converterRoot + "' conv.command='" + converterCommand + "'");
156                 log.error("Ignoring modulateScaleBase property.");
157                 log.error(nfe.getMessage());
158             }
159         } else {
160             log.debug(
161             "ModulateScaleBase property not found, ignoring the modulateScaleBase.");
162         }
163     }
164
165     private static class ParseResult {
166         List args;
167         String JavaDoc format;
168         File cwd;
169     }
170
171     /**
172      * This functions converts an image by the given parameters
173      * @param input an array of <code>byte</code> which represents the original image
174      * @param commands a <code>List</code> of <code>String</code>s containing commands which are operations on the image which will be returned.
175      * ImageConvert.converterRoot and ImageConvert.converterCommand specifing the converter root....
176      * @return an array of <code>byte</code>s containing the new converted image.
177      *
178      */

179     public byte[] convertImage(byte[] input, String JavaDoc sourceFormat, List commands) {
180         byte[] pict = null;
181         if (commands != null && input != null) {
182             ParseResult parsedCommands = getConvertCommands(commands);
183             if (parsedCommands.format.equals("asis") && sourceFormat != null) {
184                 parsedCommands.format = sourceFormat;
185             }
186             pict = convertImage(input, parsedCommands.args, parsedCommands.format, parsedCommands.cwd);
187         }
188         return pict;
189     }
190
191     /**
192      * Translates MMBase color format (without #) to an convert color format (with or without);
193      */

194     protected String JavaDoc color(String JavaDoc c) {
195         if (c.charAt(0) == 'X') {
196             // the # was mentioned but replaced by X in ImageTag
197
c = '#' + c.substring(1); // put it back.
198
}
199         if (c.length() == 6) {
200             // obviously a little to simple now, because color names of 6 letters don't work now
201
return "#" + c.toLowerCase();
202         } else {
203             return c.toLowerCase();
204         }
205     }
206
207
208     /**
209      * Translates the arguments for img.db to arguments for convert of ImageMagick.
210      * @param params List with arguments. First one is the image's number, which will be ignored.
211      * @return Map with three keys: 'args', 'cwd', 'format'.
212      */

213     private ParseResult getConvertCommands(List params) {
214         if (log.isDebugEnabled()) {
215             log.debug("getting convert commands from " + params);
216         }
217         ParseResult result = new ParseResult();
218         List cmds = new ArrayList();
219         result.args = cmds;
220         result.cwd = null;
221         result.format = Factory.getDefaultImageFormat();
222
223         String JavaDoc key, type;
224         String JavaDoc cmd;
225         int pos, pos2;
226         Iterator t = params.iterator();
227         while (t.hasNext()) {
228             key = (String JavaDoc) t.next();
229             if (log.isDebugEnabled()) log.debug("parsing '" + key + "'");
230             pos = key.indexOf('(');
231             pos2 = key.lastIndexOf(')');
232             if (pos != -1 && pos2 != -1) {
233                 type = key.substring(0, pos).toLowerCase();
234                 cmd = key.substring(pos + 1, pos2);
235                 if (log.isDebugEnabled()) {
236                     log.debug("getCommands(): type=" + type + " cmd=" + cmd);
237                 }
238                 // Following code translates some MMBase specific things to imagemagick's convert arguments.
239
type = Imaging.getAlias(type);
240                 // Following code will only be used when ModulateScaleBase builder property is defined.
241
if (type.equals("modulate") && (modulateScaleBase != Integer.MAX_VALUE)) {
242                     cmd = calculateModulateCmd(cmd, modulateScaleBase);
243                 } else if (type.equals("colorizehex")) {
244                     // Incoming hex number rrggbb is converted to
245
// decimal values rr,gg,bb which are inverted on a scale from 0 to 100.
246
if (log.isDebugEnabled())
247                         log.debug("colorizehex, cmd: " + cmd);
248                     String JavaDoc hex = cmd;
249                     // Check if hex length is 123456 6 chars.
250
if (hex.length() == 6) {
251
252                         // Byte.decode doesn't work correctly.
253
int r = colorizeHexScale - Math.round( colorizeHexScale * Integer.parseInt( hex.substring(0, 2), 16) / 255.0f);
254                         int g = colorizeHexScale - Math.round( colorizeHexScale * Integer.parseInt( hex.substring(2, 4), 16) / 255.0f);
255                         int b = colorizeHexScale - Math.round( colorizeHexScale * Integer.parseInt( hex.substring(4, 6), 16) / 255.0f);
256                         if (log.isDebugEnabled()) {
257                             log.debug("Hex is :" + hex);
258                             log.debug( "Calling colorize with r:" + r + " g:" + g + " b:" + b);
259                         }
260                         type = "colorize";
261                         cmd = r + "/" + g + "/" + b;
262                     }
263                 } else if (type.equals("gamma")) {
264                     StringTokenizer tok = new StringTokenizer(cmd, ",/");
265                     String JavaDoc r = tok.nextToken();
266                     String JavaDoc g = tok.nextToken();
267                     String JavaDoc b = tok.nextToken();
268                     cmd = r + "/" + g + "/" + b;
269                 } else if (
270                 type.equals("pen")
271                 || type.equals("transparent")
272                 || type.equals("fill")
273                 || type.equals("bordercolor")
274                 || type.equals("background")
275                 || type.equals("box")
276                 || type.equals("opaque")
277                 || type.equals("stroke")) {
278                     // rather sucks, because we have to maintain manually which options accept a color
279
cmd = color(cmd);
280                 } else if (type.equals("text")) {
281                     int firstcomma = cmd.indexOf(',');
282                     int secondcomma = cmd.indexOf(',', firstcomma + 1);
283                     if (imVersionMajor < 6) {
284                         type = "draw";
285                         try {
286                             File tempFile = File.createTempFile("mmbase_image_text_", null);
287                             tempFile.deleteOnExit();
288                             Encode encoder = new Encode("ESCAPE_SINGLE_QUOTE");
289                             String JavaDoc text = cmd.substring(secondcomma + 1);
290                             FileOutputStream tempFileOutputStream = new FileOutputStream(tempFile);
291                             tempFileOutputStream.write(encoder.decode(text.substring(1, text.length() - 1)).getBytes("UTF-8"));
292                             tempFileOutputStream.close();
293                             cmd = "text " + cmd.substring(0, secondcomma) + " '@" + tempFile.getPath() + "'";
294                         } catch (IOException e) {
295                             log.error("Could not create temporary file for text: " + e.toString());
296                             cmd = "text " + cmd.substring(0, secondcomma) + " 'Could not create temporary file for text.'";
297                         }
298                     } else {
299                         cmds.add("-encoding");
300                         cmds.add("unicode");
301                         cmds.add("-annotate");
302                         try {
303                             File tempFile = File.createTempFile("mmbase_image_text_", null);
304                             tempFile.deleteOnExit();
305                             FileOutputStream tempFileOutputStream = new FileOutputStream(tempFile);
306                             Encode encoder = new Encode("ESCAPE_SINGLE_QUOTE");
307                             String JavaDoc text = cmd.substring(secondcomma + 1);
308                             log.debug("Using '" + text + "'");
309                             tempFileOutputStream.write(encoder.decode(text.substring(1, text.length() - 1)).getBytes("UTF-8"));
310                             tempFileOutputStream.close();
311                             cmds.add("+" + cmd.substring(0, firstcomma) + "+" + cmd.substring(firstcomma + 1, secondcomma));
312                             cmds.add("@" + tempFile.getPath());
313                         } catch (IOException e) {
314                             log.error("Could not create temporary file for text: " + e.toString());
315                             cmd = cmd.substring(0, secondcomma) + " 'Could not create temporary file for text.'";
316                         }
317                         continue;
318                     }
319                 } else if (type.equals("draw")) {
320                     //try {
321
//cmd = new String(cmd.getBytes("UTF-8"), "ISO-8859-1");
322
// can be some text in the draw command
323
//} catch (java.io.UnsupportedEncodingException e) {
324
// log.error(e.toString());
325
//}
326
} else if (type.equals("font")) {
327                     if (cmd.startsWith("mm:")) {
328                         // recognize MMBase config dir, so that it is easy to put the fonts there.
329
cmd = org.mmbase.module.core.MMBaseContext.getConfigPath()+ File.separator + cmd.substring(3);
330                     }
331                     File fontFile = new File(cmd);
332                     if (!fontFile.isFile()) {
333                         // if not pointed to a normal file, then set the cwd to <config>/fonts where you can put a type.mgk
334
File fontDir =
335                         new File( org.mmbase.module.core.MMBaseContext.getConfigPath(),"fonts");
336                         if (fontDir.isDirectory()) {
337                             if (log.isDebugEnabled()) {
338                                 log.debug("Using " + fontDir + " as working dir for conversion. A 'type.mgk' (see ImageMagick documentation) can be in this dir to define fonts");
339                             }
340                             result.cwd = fontDir;
341                         } else {
342                             log.debug(
343                             "Using named font without MMBase 'fonts' directory, using ImageMagick defaults only");
344                         }
345                     }
346
347                 } else if (type.equals("circle")) {
348                     type = "draw";
349                     cmd = "circle " + cmd;
350                 } else if (type.equals("part")) {
351                     StringTokenizer tok = new StringTokenizer(cmd, "x,\n\r");
352                     try {
353                         int x1 = Integer.parseInt(tok.nextToken());
354                         int y1 = Integer.parseInt(tok.nextToken());
355                         int x2 = Integer.parseInt(tok.nextToken());
356                         int y2 = Integer.parseInt(tok.nextToken());
357                         type = "crop";
358                         cmd = (x2 - x1) + "x" + (y2 - y1) + "+" + x1 + "+" + y1;
359                     } catch (Exception JavaDoc e) {
360
361                         log.error(e.toString());
362                     }
363                 } else if (type.equals("roll")) {
364                     StringTokenizer tok = new StringTokenizer(cmd, "x,\n\r");
365                     String JavaDoc str;
366                     int x = Integer.parseInt(tok.nextToken());
367                     int y = Integer.parseInt(tok.nextToken());
368                     if (x >= 0)
369                         str = "+" + x;
370                     else
371                         str = "" + x;
372                     if (y >= 0)
373                         str += "+" + y;
374                     else
375                         str += "" + y;
376                     cmd = str;
377                 } else if (type.equals("f")) {
378                     if (! (cmd.equals("asis") && result.format != null)) {
379                         result.format = cmd;
380                     }
381                     continue; // ignore this one, don't add to cmds.
382
}
383                 if (log.isDebugEnabled()) {
384                     log.debug("adding " + type + " " + cmd);
385                 }
386                 // all other things are recognized as well..
387
if (! isCommandPrefixed(type)) { // if no prefix given, suppose '-'
388
cmds.add("-" + type);
389                 } else {
390                     cmds.add(type);
391                 }
392                 cmds.add(cmd);
393
394             } else {
395                 key = Imaging.getAlias(key);
396                 if (key.equals("lowcontrast")) {
397                     cmds.add("+contrast");
398                 } else if (key.equals("neg")) {
399                     cmds.add("+negate");
400                 } else {
401                     if (! isCommandPrefixed(key)) { // if no prefix given, suppose '-'
402
cmds.add("-" + key);
403                     } else {
404                         cmds.add(key);
405                     }
406                 }
407             }
408         }
409         return result;
410     }
411
412     /**
413      * @since MMBase-1.7
414      */

415     private boolean isCommandPrefixed(String JavaDoc s) {
416         if (s == null || s.length() == 0) return false;
417         char c = s.charAt(0);
418         return c == '-' || c == '+';
419     }
420
421     /**
422      * Calculates the modulate parameter values (brightness,saturation,hue) using a scale base.
423      * ImageMagick's convert command changed its modulate scale somewhere between version v4.2.9 and v5.3.8.<br />
424      * In version 4.2.9 the scale ranges from -100 to 100.<br />
425      * (relative, eg. 20% higher, value is 20, 10% lower, value is -10).<br />
426      * In version 5.3.8 the scale ranges from 0 to 100.<br />
427      * (absolute, eg. 20% higher, value is 120, 10% lower, value is 90).<br />
428      * Now, for different convert versions the scale range can be corrected with the scalebase. <br />
429      * The internal scale range that's used will be from -100 to 100. (eg. modulate 20,-10,0).
430      * With the base you can change this, so for v4.2.9 scalebase=0 and for v5.3.9 scalebase=100.
431      * @param cmd modulate command string
432      * @param scaleBase the scale base value
433      * @return the transposed modulate command string.
434      */

435     private String JavaDoc calculateModulateCmd(String JavaDoc cmd, int scaleBase) {
436         log.debug( "Calculating modulate cmd using scale base " + scaleBase + " for modulate cmd: " + cmd);
437         String JavaDoc modCmd = "";
438         StringTokenizer st = new StringTokenizer(cmd, ",/");
439         while (st.hasMoreTokens()) {
440             modCmd += scaleBase + Integer.parseInt(st.nextToken()) + ",";
441         }
442         if (!modCmd.equals("")) {
443             modCmd = modCmd.substring(0, modCmd.length() - 1);
444         }
445         // remove last ',' char.
446
log.debug("Modulate cmd after calculation: " + modCmd);
447         return modCmd;
448     }
449
450     /**
451      * Does the actual conversion.
452      *
453      * @param pict Byte array with the original picture
454      * @param cmd List with convert parameters.
455      * @param format The picture format to output to (jpg, gif etc.).
456      * @return The result of the conversion (a picture).
457      *
458      */

459     private byte[] convertImage(byte[] pict, List cmd, String JavaDoc format, File cwd) {
460
461         if (pict != null && pict.length > 0) {
462             cmd.add(0, "-");
463             cmd.add(0, converterPath);
464             cmd.add(format+ ":-");
465
466             String JavaDoc command = cmd.toString(); // only for debugging.
467
log.debug("Converting image(#" + pict.length + " bytes) to '" + format + "' ('" + command + "')");
468
469             CommandLauncher launcher = new CommandLauncher("ConvertImage");
470             ByteArrayOutputStream imageStream = new ByteArrayOutputStream();
471             ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
472             ByteArrayInputStream originalStream = new ByteArrayInputStream(pict);
473
474             try {
475                 if (cwd != null) {
476                     // using MAGICK_HOME for mmbase config/fonts if 'font' option used (can put type.mgk)
477
String JavaDoc[] env = { "MAGICK_HOME=" + cwd.toString() };
478                     if (log.isDebugEnabled()) {
479                         log.debug("MAGICK_HOME " + env[0]);
480                     }
481                     launcher.execute((String JavaDoc[]) cmd.toArray(new String JavaDoc[0]), env);
482                 }
483                 else {
484                     launcher.execute((String JavaDoc[]) cmd.toArray(new String JavaDoc[0]));
485                 }
486                 launcher.waitAndWrite(originalStream, imageStream, errorStream);
487
488                 log.debug("retrieved all information");
489                 byte[] image = imageStream.toByteArray();
490
491                 if (image.length < 1) {
492                     // No bytes in the image -
493
// ImageMagick failed to create a proper image.
494
// return null so this image is not by accident stored in the database
495
log.error("Imagemagick conversion did not succeed. Returning null.");
496                     String JavaDoc errorMessage = errorStream.toString();
497
498                     if (errorMessage.length() > 0) {
499                         log.error( "From stderr with command '" + command + "' in '" + new File("").getAbsolutePath() + "' --> '" + errorMessage + "'");
500                     } else {
501                         log.debug("No information on stderr found");
502                     }
503                     return null;
504                 }
505                 else {
506                     // print some info and return....
507
if (log.isServiceEnabled()) {
508                         log.service("converted image(#" + pict.length + " bytes) to '" + format + "'-image(#" + image.length + " bytes)('" + command + "')");
509                     }
510                     return image;
511                 }
512             }
513             catch (ProcessException e) {
514                 log.error("converting image with command: '" + command + "' failed with reason: '" + e.getMessage() + "'");
515                 log.error(Logging.stackTrace(e));
516             }
517             finally {
518                 try {
519                     if (originalStream != null) {
520                         originalStream.close();
521                     }
522                 }
523                 catch (IOException ioe) {
524                 }
525                 try {
526                     if (imageStream != null) {
527                         imageStream.close();
528                     }
529                 }
530                 catch (IOException ioe) {
531                 }
532             }
533         }
534         else {
535             log.error("Converting an empty image does not make sense.");
536         }
537
538         return null;
539     }
540
541 }
542
Popular Tags