Wiki source code of Programming__WebObjects-Web Applications-Development-Thumbnailing
Version 5.1 by smmccraw on 2007/07/08 09:46
Hide last authors
author | version | line-number | content |
---|---|---|---|
![]() |
2.1 | 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>>http://wocode.com/cgi-bin/WebObjects/WOCode.woa/wa/ShareCodeItem?itemId=430]] | ||
7 | * An all platforms, you can use Java2D with Java's ImageIO and BufferedImage | ||
8 | * On many platforms, you can run [[ImageMagick>>http://imagemagick.org/script/index.php]] and use Runtime.exec to call the commandline "convert" | ||
9 | * On many platforms, you can build and run [[JMagick>>http://www.yeo.id.au/jmagick/]], 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 | |||
![]() |
4.1 | 17 | {{panel}} |
![]() |
2.1 | 18 | |
![]() |
4.1 | 19 | /* ImageMagickUtil.java created by jrochkind on Thu 24-Apr-2003 */ |
![]() |
2.1 | 20 | |
![]() |
4.1 | 21 | import com.webobjects.foundation.*; |
22 | import com.webobjects.eocontrol.*; | ||
23 | import com.webobjects.eoaccess.*; | ||
24 | import com.webobjects.appserver.*; | ||
![]() |
2.1 | 25 | |
![]() |
4.1 | 26 | import java.io.IOException; |
27 | import java.io.InputStream; | ||
28 | import java.io.InputStreamReader; | ||
29 | import java.io.BufferedReader; | ||
![]() |
2.1 | 30 | |
![]() |
4.1 | 31 | /* Utility methods that deal with images by calling the ImageMagick software |
32 | as an external process */ | ||
![]() |
2.1 | 33 | |
![]() |
4.1 | 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",_"%wn%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 | } | ||
![]() |
2.1 | 346 | } |
347 | |||
![]() |
4.1 | 348 | {{/panel}} |
![]() |
2.1 | 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/). 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 | |||
![]() |
4.1 | 354 | {{panel}} |
![]() |
2.1 | 355 | |
![]() |
4.1 | 356 | /* ImageResizer.java */ |
![]() |
2.1 | 357 | |
![]() |
4.1 | 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() h1. 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 | } | ||
![]() |
2.1 | 423 | } |
424 | |||
![]() |
4.1 | 425 | {{/panel}} |
![]() |
2.1 | 426 | |
427 | Category:WebObjects |