Wiki source code of Development-Thumbnailing

Last modified by Pascal Robert on 2010/09/19 10:27

Show last authors
1 == Overview ==
2
3 Many people have asked how to create thumbnails of images using WebObjects. There is nothing specific to WebObjects about this problem, and there are quite a few options for solving it:
4
5 * On Mac OS X, you can use Runtime.exec(..) and call the commandline program "sips," which uses the ImageIO and CoreImage frameworks
6 * On Mac OS X, you can use Brendan Duddridge's [[ImageIO JNI Wrapper>>url:http://wocode.com/cgi-bin/WebObjects/WOCode.woa/wa/ShareCodeItem?itemId=430||shape="rect"]]
7 * An all platforms, you can use Java2D with Java's ImageIO and BufferedImage
8 * On many platforms, you can run [[ImageMagick>>url:http://imagemagick.org/script/index.php||shape="rect"]] and use Runtime.exec to call the commandline "convert"
9 * On many platforms, you can build and run [[JMagick>>url:http://www.yeo.id.au/jmagick/||shape="rect"]], a Java JNI wrapper around ImageMagick
10
11 == ImageMagick ==
12
13 A utility class to call ImageMagick binaries as an external process with Runtime.exec, to either discover height/width or resize an image. It has a few default filepaths that correspond to what I need on my systems, you probably want to change them for your system(s).
14
15 //Anjo Krank: I may be wrong, but I'm pretty sure that this code won't work, at least not reliably. There is no guarantee that the the contents of the supplied arrays are filled before the process exits. I had a lot of null-results when I tried this in Wonder. The only safe way I've seen so far is to actually wait until the stream is truly finished reading before accessing the result. And this can only be done by waiting on a sema. Take a look at ERXRuntimeUtilities for a version that does work.//
16
17 {{code}}
18
19 /* ImageMagickUtil.java created by jrochkind on Thu 24-Apr-2003 */
20
21 import com.webobjects.foundation.*;
22 import com.webobjects.eocontrol.*;
23 import com.webobjects.eoaccess.*;
24 import com.webobjects.appserver.*;
25
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.InputStreamReader;
29 import java.io.BufferedReader;
30
31 /* Utility methods that deal with images by calling the ImageMagick software
32 as an external process */
33
34 /* Dealing with Runtime.exec in a thread-safe way is tricky! Sorry for that.
35 I used the article at
36 http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html
37 as a guide for understanding how to do it right. */
38
39 public class ImageMagickUtil {
40 //protected static final String imUtilLocationPath = "C:\\Program Files\\ImageMagick-5.5.6-Q8\\";
41 protected static final String imUtilLocationPath;// = "/export/home/jar247/mtBin/";
42 //The image util location path can be supplied as a Java property,
43 //or we can try to guess it from the OS.
44 static {
45 String locProp = System.getProperty("imUtilPath");
46 String osName = System.getProperty("os.name");
47 if ( locProp != null ) {
48 imUtilLocationPath = locProp;
49 }
50 else if ( osName.indexOf("Windows") != -1 ) {
51 imUtilLocationPath = "C:\\Program Files\\ImageMagick-5.5.6-Q8\\";
52 }
53 else {
54 //Assume our deployment machine, which is currently set up
55 //to make this the location...
56 imUtilLocationPath = "/export/home/webob/ImageMagick/bin/";
57 }
58 }
59
60 protected static final String imIdentifyCommand = "identify";
61 protected static final String imConvertCommand = "convert";
62
63 public static ImageProperties getImageSize(String filePath) throws IMException {
64 return getImageSize( new java.io.File(filePath) );
65 }
66
67 public static ImageProperties getImageSize(java.io.File imageFile) throws IMException {
68 String filePathToImage = imageFile.getPath();
69
70 String[] cmdArray = new String[] {
71 imUtilLocationPath + imIdentifyCommand,
72 "-format", "%w\n%h", // [width][newline][height]
73 filePathToImage
74 };
75
76 NSMutableArray stdOutContents = new NSMutableArray();
77 NSMutableArray stdErrContents = new NSMutableArray();
78 int resultCode = -1;
79 try {
80 resultCode = exec(cmdArray, stdOutContents, stdErrContents);
81 }
82 catch (IOException ioE ) {
83 //For some reason we couldn't exec the process! Convert it to an IMException.
84 //One reason this exception is thrown is if the path specified to the im
85 //executable isn't correct.
86 throw new IMException("Could not exec imagemagick process: " + ioE.getMessage(), cmdArray, null);
87 }
88 catch ( InterruptedException intE ) {
89 //re-throw it as an IMException.
90 //This exception should really never be thrown, as far as I know.
91 throw new IMException("imagemagick process interrupted! " + intE.getMessage(), cmdArray, null);
92 }
93
94 if ( resultCode != 0 ) {
95 //The external process reports failure!
96 IMException e = new IMException("Identify failed! ", cmdArray, stdErrContents);
97 e.exitValue = resultCode;
98 throw e;
99 }
100
101 //Now we need to parse the result line for the height
102 //and width information.
103 if ( stdOutContents.count() >= 2 ) {
104 //First line is width, second is height, because
105 //we asked the imagemagick 'identify' utility to
106 //output like that.
107 String widthStr = (String) stdOutContents.objectAtIndex(0);
108 String heightStr = (String) stdOutContents.objectAtIndex(1);
109 Integer width = new Integer( widthStr );
110 Integer height = new Integer( heightStr );
111
112 ImageProperties p = new ImageProperties();
113 p.width = width;
114 p.height = height;
115 return p;
116 }
117 else {
118 //Umm? Error condition.
119 throw new IMException("Unexpected output of imagemagick process", cmdArray, stdErrContents);
120 }
121 }
122
123 // An external image magick process will be run to resize the image. It's reccomended you
124 // check to make sure it's neccesary to resize the image first!
125 // Beware, if the sizes you pass in are LARGER than the existing size, the output image
126 // WILL be BIGGER than the input---this isn't just for resizing downward.
127 // Null outFilePath means to overwrite the source file path with the resized image.
128 // You can pass null for either maxWidth or maxHeight, but not both, that would be silly!
129 // [not implemented yet:] Returned is an object telling you the new resized size of the output image.
130 public static void resizeImage(String sourceFilePath, String outFilePath, int maxWidth, int maxHeight)
131 throws IMException {
132 if ( outFilePath == null ) {
133 //overwrite original file if neccesary
134 outFilePath = sourceFilePath;
135 }
136 /*else if ( NSPathUtilities.pathExtension( outFilePath ) == null ) {
137 //give the output file path the same extension as the in file path.
138 outFilePath = NSPathUtilities.stringByAppendingExtension( outFilePath,
139 NSPathUtilities.pathExtension(sourceFilePath));
140 }*/
141
142 StringBuffer dimensionBuffer = new StringBuffer();
143 if ( maxWidth != -1) {
144 dimensionBuffer.append(maxWidth);
145 }
146 dimensionBuffer.append( "x" );
147 if ( maxHeight != -1) {
148 dimensionBuffer.append( maxHeight );
149 }
150 String dimensionDirective = dimensionBuffer.toString();
151
152 //We include the ' +profile "*" ' argument to remove
153 //all profiles from the output. Not sure exactly what this means...
154 //but before we were doing this, we wound up with JPGs that
155 //caused problems for IE, for reasons I do not understand.
156 String[] cmdArray = new String[] {
157 imUtilLocationPath + imConvertCommand,
158 "-size", dimensionDirective,
159 sourceFilePath,
160 "-resize", dimensionDirective,
161 "+profile", "*",
162 outFilePath
163 };
164
165 NSMutableArray stdErrContents = new NSMutableArray();
166 NSMutableArray stdOutContents = new NSMutableArray();
167 int resultCode;
168 try {
169 resultCode = exec( cmdArray, stdOutContents, stdErrContents );
170 }
171 catch (IOException ioE ) {
172 //For some reason we couldn't exec the process! Convert it to an IMException.
173 //One reason this exception is thrown is if the path specified to the im
174 //executable isn't correct.
175 throw new IMException("Could not exec imagemagick process: " + ioE.getMessage(), cmdArray, null);
176 }
177 catch ( InterruptedException intE ) {
178 //re-throw it as an IMException.
179 //This exception should really never be thrown, as far as I know.
180 throw new IMException("imagemagick process interrupted! " + intE.getMessage(), cmdArray, null);
181 }
182 if ( resultCode != 0 ) {
183 //The external process reports failure!
184 IMException e = new IMException("Conversion failed! ", cmdArray, stdErrContents);
185 e.exitValue = resultCode;
186 throw e;
187 }
188 }
189
190 //Invokes the external process. Puts standard out and standard error into the
191 //arrays given in arguments (they can be null, in which case the err/out stream
192 //is just thrown out.
193 //Throws IOException if the exec of the external process doesn't work, for instance
194 //because the path to the command is no good.
195 //Throws the InterruptedException... not sure when, if ever. But it means that the exec
196 //didn't work completely.
197 public static int exec(String[] cmdArray, NSMutableArray stdOut, NSMutableArray stdErr) throws IOException, InterruptedException {
198 Process process = Runtime.getRuntime().exec( cmdArray );
199
200 //We are interested in what that process writes to standard
201 //output and standard error.
202 //Grab the contents of those in their own seperate threads!
203 //To avoid deadlock on the external process thread!
204
205 StreamGrabber errGrabber = new StreamGrabber( process.getErrorStream(), stdErr );
206 StreamGrabber outGrabber = new StreamGrabber( process.getInputStream(), stdOut );
207
208 errGrabber.start();
209 outGrabber.start();
210
211 return process.waitFor();
212 }
213
214
215 //Will launch a NEW THREAD and put the contents of the given stream into
216 //the NSMutableArray. Don't be accessing the array in another thread before
217 //we're done!
218 //If array is null, StreamGrabber reads the input stream to completion,
219 //but doesn't store results anywhere.
220 static class StreamGrabber extends Thread
221 {
222 InputStream inputStream;
223 NSMutableArray array;
224 boolean done = false;
225 Exception exceptionEncountered;
226
227
228 StreamGrabber(InputStream is, NSMutableArray a)
229 {
230 super("StreamGrabber");
231 this.inputStream = is;
232 this.array = a;
233 }
234
235 public void run()
236 {
237 try {
238 InputStreamReader isr = new InputStreamReader(inputStream);
239 BufferedReader br = new BufferedReader(isr);
240 String line=null;
241 while ( (line = br.readLine()) != null)
242 {
243 if ( array != null ) {
244 array.addObject(line);
245 }
246 }
247 }
248 catch ( java.io.IOException e) {
249 //hmm, what should we do?!?
250 setExceptionEncountered( e );
251 }
252 setDone( true );
253 }
254
255 public synchronized void setDone(boolean v) {
256 done = v;
257 }
258 //Can be used by parent thread to see if we're done yet.
259 public synchronized boolean done() {
260 return done;
261 }
262 public synchronized Exception exceptionEncountered() {
263 return exceptionEncountered;
264 }
265 public synchronized void setExceptionEncountered(Exception e) {
266 exceptionEncountered = e;
267 }
268 public boolean didEncounterException() {
269 return exceptionEncountered() != null;
270 }
271 }
272
273 //Exception thrown by utilty methods when the call to an external image magick
274 //process failed.
275 public static class IMException extends Exception {
276 protected int exitValue;
277 protected String processErrorMessage;
278 protected String invocationLine;
279 protected String message;
280
281 public IMException() {
282 super();
283 }
284 public IMException(String s) {
285 super();
286 message = s;
287 }
288 //Constructs a long message from all these parts
289 public IMException(String messagePrefix, String[] cmdArray, NSMutableArray stdErr) {
290 super();
291 if ( cmdArray != null ) {
292 invocationLine = new NSArray( cmdArray ).componentsJoinedByString(" ");
293 }
294 if ( stdErr != null ) {
295 processErrorMessage = stdErr.componentsJoinedByString("; ");
296 }
297
298 StringBuffer b = new StringBuffer();
299 b.append( messagePrefix );
300 b.append(". invocation line: ");
301 b.append( invocationLine );
302 b.append(". error output: " );
303 b.append( processErrorMessage );
304 message = b.toString();
305 }
306
307 //the return code from the image magick external invocation.
308 //I think it's probably always 1 in an error condition, so not so useful.
309 public int exitValue() {
310 return exitValue;
311 }
312 //The error message reported by image magick.
313 public String processErrorMessage() {
314 return processErrorMessage;
315 }
316 //The command line used to invoke the external im process that
317 //resulted in an error.
318 public String invocationLine() {
319 return invocationLine;
320 }
321 public void setInvocationLine( String[] cmdArray ) {
322 invocationLine = new NSArray(cmdArray).componentsJoinedByString(" ");
323 }
324 //over-riding
325 public String getMessage() {
326 return message;
327 }
328
329 }
330
331 //Object that encapsulates data returned by an image operation
332 public static class ImageProperties extends Object {
333 protected Integer height;
334 protected Integer width;
335
336 public ImageProperties() {
337 super();
338 }
339 public Integer height() {
340 return height;
341 }
342 public Integer width() {
343 return width;
344 }
345 }
346 }
347
348 {{/code}}
349
350 == JAI example ==
351
352 An example how to resize an image with Java Advanced Imaging ([[http:~~/~~/java.sun.com/products/java-media/jai/>>url:http://java.sun.com/products/java-media/jai/||shape="rect"]]). The jar files jai-codec.jar and jac-core.jar are in the NEXT_ROOT/Library/Java/Extensions folder and referenced in the classpath. This example uses logging capability from project wonder.
353
354 {{code}}
355
356 /* ImageResizer.java */
357
358 import java.awt.image.renderable.ParameterBlock;
359 import java.io.ByteArrayOutputStream;
360 import java.io.IOException;
361 import javax.media.jai.InterpolationNearest;
362 import javax.media.jai.JAI;
363 import javax.media.jai.OpImage;
364 import javax.media.jai.RenderedOp;
365 import com.sun.media.jai.codec.ByteArraySeekableStream;
366 import com.webobjects.foundation.NSData;
367 import er.extensions.ERXLogger;
368
369 public class ImageResizer {
370
371 private static final ERXLogger log = ERXLogger.getERXLogger(ImageResizer.class);
372
373 /**
374 * utility function to resize an image to either maxWidth or maxHeight with jai
375 * example: logo = new NSData(aFileContents1);
376 * logo = ImageResizer.resizeImage(logo, 128, 42);
377 * @param data image content in NSData array
378 * @param maxWidth maxWidth in pixels picture
379 * @param maxHeight maxHeight in pixels of picutre
380 * @return resized array in JPG format if succesfull, null otherwise
381 */
382 static public NSData resizeImage(NSData data, int maxWidth, int maxHeight) {
383 try {
384 ByteArraySeekableStream s = new ByteArraySeekableStream(data
385 .bytes());
386
387 RenderedOp objImage = JAI.create("stream", s);
388 ((OpImage) objImage.getRendering()).setTileCache(null);
389
390 if (objImage.getWidth() == 0 || objImage.getHeight() == 0) {
391 log.error("graphic size is zero");
392 return null;
393 }
394
395 float xScale = (float) (maxWidth * 1.0) / objImage.getWidth();
396 float yScale = (float) (maxHeight * 1.0) / objImage.getHeight();
397 float scale = xScale;
398 if (xScale > yScale) {
399 scale = yScale;
400 }
401
402 ParameterBlock pb = new ParameterBlock();
403 pb.addSource(objImage); // The source image
404 pb.add(scale); // The xScale
405 pb.add(scale); // The yScale
406 pb.add(0.0F); // The x translation
407 pb.add(0.0F); // The y translation
408 pb.add(new InterpolationNearest()); // The interpolation
409
410 objImage = JAI.create("scale", pb, null);
411
412 ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
413 JAI.create("encode", objImage, out, "JPEG");
414 return new NSData(out.toByteArray());
415 } catch (IOException e) {
416 log.error("io exception " + e);
417 } catch (RuntimeException e) {
418 log.error("runtime exception "+e);
419 }
420
421 return null;
422 }
423 }
424
425 {{/code}}