001 /*
002 Copyright (c) 1996-2011, Damon Hart-Davis
003 All rights reserved.
004
005 Redistribution and use in source and binary forms, with or without
006 modification, are permitted provided that the following conditions are
007 met:
008
009 * Redistributions of source code must retain the above copyright
010 notice, this list of conditions and the following disclaimer.
011
012 * Redistributions in binary form must reproduce the above copyright
013 notice, this list of conditions and the following disclaimer in the
014 documentation and/or other materials provided with the
015 distribution.
016
017 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028 */
029
030 package org.hd.d.pg2k.svrCore.MIME;
031
032 import java.awt.Dimension;
033 import java.awt.geom.AffineTransform;
034 import java.awt.image.AffineTransformOp;
035 import java.awt.image.BufferedImage;
036 import java.io.ByteArrayInputStream;
037 import java.io.ByteArrayOutputStream;
038 import java.io.IOException;
039 import java.io.InputStream;
040 import java.util.Iterator;
041 import java.util.concurrent.atomic.AtomicReferenceArray;
042
043 import javax.imageio.IIOImage;
044 import javax.imageio.ImageIO;
045 import javax.imageio.ImageReadParam;
046 import javax.imageio.ImageReader;
047 import javax.imageio.ImageWriteParam;
048 import javax.imageio.ImageWriter;
049 import javax.imageio.metadata.IIOMetadata;
050 import javax.imageio.metadata.IIOMetadataFormatImpl;
051 import javax.imageio.stream.ImageInputStream;
052 import javax.imageio.stream.ImageOutputStream;
053 import javax.media.jai.JAI;
054 import javax.xml.parsers.DocumentBuilder;
055 import javax.xml.parsers.DocumentBuilderFactory;
056 import javax.xml.parsers.ParserConfigurationException;
057
058 import org.hd.d.pg2k.svrCore.AllExhibitProperties;
059 import org.hd.d.pg2k.svrCore.ExhibitPropsComputable;
060 import org.hd.d.pg2k.svrCore.ExhibitStaticAttr;
061 import org.hd.d.pg2k.svrCore.ExhibitThumbnails;
062 import org.hd.d.pg2k.svrCore.ImageUtils;
063 import org.hd.d.pg2k.svrCore.MemoryTools;
064 import org.hd.d.pg2k.svrCore.Name;
065 import org.hd.d.pg2k.svrCore.Name.ExhibitFull;
066 import org.hd.d.pg2k.svrCore.TextUtils;
067 import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME.ExhibitTypeParameters;
068 import org.w3c.dom.Document;
069 import org.w3c.dom.Element;
070 import org.w3c.dom.Node;
071
072 /**Base class with useful default behaviour for image exhibit media-handler classes.
073 * By default this uses ImageIO (iio) routines to extract an image and metadata from
074 * an exhibit stream,
075 * and will allow the generation of thumbnails if an encoder and thumbnail parameters
076 * can be found at run-time.
077 * <p>
078 * Various routines can be overridden in deriving classes to change this behaviour.
079 * <p>
080 * Potentially-memory-intensive routines may be synchronised in this class (on a private lock)
081 * in order to reduce the chance of blowing up the VM by accident,
082 * but not if this may cause deadlock elsewhere.
083 */
084 public abstract class AbstractImageHandler extends AbstractHandler
085 {
086 /**Make thumbnails/samples for the specified exhibit; null if thumbnails cannot (currently) be made.
087 * This may fail with an IOException or return null
088 * to indicate that it is currently unable to create
089 * thumbnails/samples (though this condition may be temporary).
090 * <p>
091 * If this wishes to indicate that it cannot ever make one or more
092 * thumbnails/samples for a given exhibit then this should return a
093 * ExhibitThumbnails object with one or both thumbnails set to null.
094 * <p>
095 * (If canMakeThumbnails() returns false, this should return null.)
096 * <p>
097 * This <strong>does not close its input stream</strong> when done.
098 * <p>
099 * This will only work correctly if the exhibit is of the correct type,
100 * eg its magic number must already have been tested.
101 * <p>
102 * This assumes enough memory and other resource is available.
103 * <p>
104 * By default, returns null, ie thumbnails cannot be made now.
105 * <p>
106 * Sophisticated handlers may wish to override this.
107 * <p>
108 * Whatever thumbnails are returned,
109 * any small thumbnail is smaller than any standard thumbnail, and
110 * any standard thumbnail is smaller than the original.
111 *
112 * @param is the whole raw image positioned at its start
113 * @param originalLength is the length of the encoded original in bytes;
114 * always positive and must reflect the stream input
115 * and will be constrained to Integer.MAX_VALUE if larger
116 * @return thumbnails, else null if not possible now;
117 * ExhibitThumbnails.NO_THUMBNAILS may be returned if it looks
118 * like it never be possible/sensible to make thumbnails for
119 * this exhibit
120 */
121 @Override
122 public ExhibitThumbnails makeThumbnails(final InputStream is,
123 final long originalLength)
124 throws IOException
125 {
126 if(is == null)
127 { throw new IllegalArgumentException(); }
128 if(originalLength <= 0)
129 { throw new IllegalArgumentException(); }
130
131 if(!canMakeThumbnails())
132 { return(null); }
133
134 // Compute max size of thumbnails,
135 // constrained by size of original.
136 final int maxTNBytes =
137 (int) Math.min(originalLength - 1, Integer.MAX_VALUE);
138
139 // If we can't decode the image
140 // then give up for now, or permanently if appropriate.
141 BufferedImage bi = null;
142 try { bi = decodeImage(is); }
143 // Absorb IllegalArgumentException decoding some images, eg GIF/PNG.
144 // Do not try again in this case.
145 catch(final IllegalArgumentException e)
146 {
147 e.printStackTrace();
148 System.err.println("ERROR: could not decode image: will not try to build thumbnails again");
149 return(ExhibitThumbnails.NO_THUMBNAILS); /* Permanent error. */
150 }
151 // JAI has problems with some GIFs: convert to permanent errors.
152 // eg: "javax.imageio.IIOException: Unexpected block type 0!"
153 catch(final javax.imageio.IIOException e)
154 {
155 e.printStackTrace();
156 final String message = e.getMessage();
157 // JAI bug work-around.
158 if((null != message) && message.startsWith("Unexpected block type"))
159 {
160 System.err.println("ERROR: could not decode image: will not try to build thumbnails again");
161 return(ExhibitThumbnails.NO_THUMBNAILS); /* Permanent error. */
162 }
163 throw e; // Treat as transient I/O error.
164 }
165 if(bi == null) { return(null); /* Transient error; retry may be OK. */ }
166
167 // Try first to make the small thumbnail.
168 // We must always try to have this if possible,
169 // and we will then have a lower bound for the size of the standard one.
170 final byte sml[] = makeThumbnailImage(bi, false, 0, maxTNBytes);
171
172 // Try next to make the large thumbnail.
173 // It must be larger (in file size) than the small thumbnail.
174 final byte std[] = makeThumbnailImage(bi, true,
175 (sml == null) ? 0 : sml.length + 1,
176 maxTNBytes);
177
178 // If neither small nor standard thumbnails could be made
179 // (and no exception was thrown indicating a transient problem)
180 // then assume the error is permanent to avoid wasting time later retrying.
181 if((sml == null) && (std == null))
182 { return(ExhibitThumbnails.NO_THUMBNAILS); }
183
184 // Create and return the compound result with one/both thumbnails.
185 return(ExhibitThumbnails.createExhibitThumbnails(
186 ((sml == null) ? null :
187 new ExhibitThumbnails.Thumbnail(sml,
188 get2DImageDimensions(new ByteArrayInputStream(sml)))),
189 ((std == null) ? null :
190 new ExhibitThumbnails.Thumbnail(std,
191 get2DImageDimensions(new ByteArrayInputStream(std))))));
192 }
193
194 /**Make thumbnails/samples for the specified exhibit; null if thumbnails cannot (currently) be made.
195 * A data source for the exhibit must be supplied,
196 * along with all available properties of that exhibit.
197 * <p>
198 * This may fail with an IOException or return null
199 * to indicate that it is currently unable to create
200 * thumbnails/samples (though this condition may be temporary).
201 * <p>
202 * If this wishes to indicate that it cannot make one or more
203 * thumbnails/samples for a given exhibit then this should return a
204 * ExhibitThumbnails object with one or both thumbnails set to null.
205 * <p>
206 * (If canMakeThumbnails() returns false, this should return null.)
207 * <p>
208 * By default, returns null, ie thumbnails cannot be made.
209 * <p>
210 * This routine regulates memory use, rejecting the attempt
211 * to make the thumbnails if it cannot find/reserve sufficient memory
212 * using MemoryTools.runMemoryIntensiveOperation().
213 * <p>
214 * Calls the InputStream version of makeThumbnails().
215 *
216 * @param unlimitedResources if true, the generation routine is allowed
217 * to try to use unlimited resources (especially memory)
218 * @return thumbnails, else null if not possible now
219 * or this format does not support thumbnails, eg a sound clip;
220 * ExhibitThumbnails.NO_THUMBNAILS may be returned if it looks
221 * like it never be possible/sensible to make thumbnails for
222 * this particular exhibit
223 */
224 @Override
225 public final ExhibitThumbnails makeThumbnails(
226 final ExhibitStaticAttr esa,
227 final AllExhibitProperties.ExhibitDataSource eds,
228 final AllExhibitProperties aep,
229 final boolean unlimitedResources)
230 throws IOException
231 {
232 if((esa == null) ||
233 (eds == null) ||
234 (aep == null))
235 { throw new IllegalArgumentException(); }
236
237 //if(ORG.hd.d.IsDebug.isDebug) { System.err.println("makeThumbnails(): " + esa.filePath); }
238
239 if(!canMakeThumbnails())
240 { return(null); }
241
242 // If the exhibit is impossibly small, so we can't be smaller, give up permanently.
243 if(esa.length < 2) { return(ExhibitThumbnails.NO_THUMBNAILS); }
244
245 // Get the computed properties of the image.
246 final ExhibitPropsComputable epc = aep.getExhibitPropsComputable(esa.getExhibitFullName());
247 // Can't/won't compute a thumbnail
248 // if we can't get at the image's X,Y dimensions.
249 //
250 // This shouldn't really happen
251 // so we'll treat this as a temporary failure (!)
252 // and return null!
253 //
254 // (If the computable properties cannot be computed an IOException
255 // will get thrown by getExhibitPropsComputable() in the same spirit.)
256 if(epc == null)
257 { return(null); }
258 final Dimension imageXY = epc.getXyDimensions();
259 if(imageXY == null)
260 { return(null); }
261
262 // The class in which we will build our results
263 // if resources permit.
264 final class MakeIt implements Runnable
265 {
266 /**Computed result: null unless successfully completed. */
267 ExhibitThumbnails result;
268
269 /**Set true if we failed to generate thumbnails but a retry may be OK. */
270 boolean retry;
271
272 public void run()
273 {
274 try
275 {
276 // Return our definitive answer.
277 // This might be ExhibitThumbnails.NO_THUMBNAILS,
278 // but that simply tells us that
279 // they definitely cannot be produced,
280 // and so we need not try again.
281 result = makeThumbnails(eds.getInputStream(esa),
282 esa.length);
283 retry = true; // Retry later may well be OK!
284 }
285 catch(final OutOfMemoryError e)
286 {
287 // OutOfMemoryError should be regarded as transient,
288 // and the caller should be able to try again later.
289 // Don't bother showing the stack-trace (which may not even be available).
290 System.err.println("ERROR in AbstractImageHandler.makeThumbnails() (OutOfMemoryError, can retry) generating thumbnails for: " + esa.getCharSequence());
291 retry = true;
292 }
293 catch(final Error e)
294 {
295 // Log any other Error that we encounter,
296 // and don't encourage retries.
297 System.err.println("ERROR in AbstractImageHandler.makeThumbnails() (Error, no retry) generating thumbnails for: " + esa.getCharSequence());
298 e.printStackTrace();
299 }
300 catch(final IllegalArgumentException e)
301 {
302 // Log any IllegalArgumentException that we encounter,
303 // and don't encourage retries.
304 System.err.println("ERROR in AbstractImageHandler.makeThumbnails() (IllegalArgumentException, no retry) generating thumbnails for: " + esa.getCharSequence());
305 e.printStackTrace();
306 }
307 catch(final Exception e)
308 {
309 // (IO)Exceptions are often transient,
310 // and the caller should be able to try again later.
311 System.err.println("ERROR in AbstractImageHandler.makeThumbnails() (Exception, can retry) generating thumbnails for: " + esa.getCharSequence());
312 e.printStackTrace();
313 retry = true;
314 }
315 }
316 }
317
318 final MakeIt task = new MakeIt();
319 if(!MemoryTools.runMemoryIntensiveOperation(task,
320 unlimitedResources,
321 estimateWorkingMemoryToCreateThumbnails(imageXY)))
322 { return(null); } // Failed, maybe temporarily due to lack of resources.
323
324 // Iff we had the resources but could not generate thumbnails
325 // then mark this down as a permanent failure.
326 if(!task.retry && (task.result == null))
327 {
328 System.err.println("WARNING: permanently failing generation of thumbnails for: " + esa.getCharSequence());
329 return(ExhibitThumbnails.NO_THUMBNAILS);
330 }
331
332 // Return the result.
333 return(task.result);
334 }
335
336
337 /**Image size tolerance as a fraction of the target size; strictly positive.
338 * The tolerance is targetSize/SIZE_TOL, possibly circumscribed by
339 * other absolute limits.
340 * <p>
341 * The larger this number the tighter in conformance to the target size
342 * the final image will have to be, which will make convergence
343 * take longer or in extreme cases prevent generation of a thumbnail
344 * altogether.
345 * <p>
346 * A reasonable value for this is probably in the region 3--50.
347 */
348 private static final int THUMBNAIL_SIZE_TOL = 5;
349
350 /**Return individual thumbnail image as file-format byte array in same format as original; null if not possible.
351 * This is used for still images and possible animations
352 * or movies where the bounding rectangle and the contained data
353 * is scaled to the given bounds.
354 * <p>
355 * This uses a binary chop algorithm on the "quality" parameter
356 * calling the underlying makeScaledImage() routine
357 * to try to get the output file size to reasonable bounds.
358 * <strong>This may fail to work well, or at all, if the image size
359 * is not fixed or monotonically increasing with quality,
360 * or fails to produce any output at all some some values of
361 * the quality parameter.</strong> We treat the failure to generate
362 * an image as an indication that the quality parameter needs to be
363 * set higher.
364 * <p>
365 * The output image will, if possible, use the same colour scheme
366 * and other characteristics as the input, though some optional
367 * features that usually consume extra space, such as interlacing,
368 * may be disabled.
369 * <p>
370 * If no output can be generated this returns null,
371 * which is the default behaviour.
372 * <p>
373 * This should adjust the image "quality" with the detail
374 * value (adjusted within the bounds supplied by the handler class)
375 * to try to tune the output size. For a lossy encoding format
376 * such as JPEG this may be the "quality" factor or compression.
377 * For a lossless format the may have to be the number of
378 * bits-per-pixel that a colour map is reduced to, for example.
379 * <p>
380 * This base-class implementation can be overridden by sophisticated
381 * handlers.
382 * <p>
383 * This first scales the image using a (fast) nearest-pixel method,
384 * which should be good for true-colour and palette images.
385 *
386 * @param imageIn source image; must not be null and must be suitable
387 * for the target format
388 * @param std if true we are making a standard thumbnail,
389 * else we are making a small thumbnail
390 * @param minSize minimum (floor) size of thumbnail in bytes;
391 * must be non-negative
392 * (can be zero if not required)
393 * @param maxSize maximum (ceiling) size of thumbnail in bytes;
394 * must be non-negative and no smaller than minSize
395 * (can me Integer.MAX_VALUE if not required)
396 */
397 protected byte[] makeThumbnailImage(final BufferedImage imageIn,
398 final boolean std,
399 final int minSize,
400 final int maxSize)
401 throws IOException
402 {
403 if((imageIn == null) || (minSize < 0) || (maxSize < minSize))
404 { throw new IllegalArgumentException(); }
405
406 // Get the thumbnail parameters.
407 final ThumbnailParams tp = getThumbnailParams();
408 if(tp == null) { return(null); } // Give up if no parameters.
409
410 // Capture the source X,Y.
411 final Dimension srcXyDim = new Dimension(imageIn.getWidth(),
412 imageIn.getHeight());
413
414 // Compute the thumbnail X,Y.
415 final Dimension thumbnailXyDim =
416 ExhibitThumbnails.computeThumbnailDimensions(srcXyDim, std);
417
418 // We make the scaled image in memory once,
419 // then call the handler routine to build the output image,
420 // binary-chopping the "quality" to get as close as possible
421 // to our target size reasonably quickly.
422
423 // We use nearest-neighbour scaling because:
424 // * It is fast.
425 // * It should work for both true-colour and palette images.
426 final AffineTransformOp op = new AffineTransformOp(
427 AffineTransform.getScaleInstance(
428 thumbnailXyDim.width / (double) srcXyDim.width,
429 thumbnailXyDim.height / (double) srcXyDim.height),
430 AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
431 final BufferedImage scaledImage = op.filter(imageIn, null);
432
433 // Get the upper limit for this thumbnail size.
434 final int absMaxSize = Math.min(maxSize, std ?
435 ExhibitThumbnails.STD_ABS_MAX_BYTES :
436 ExhibitThumbnails.SML_ABS_MAX_BYTES);
437
438 // Target size in bytes for thumbnail; capped by the absolute limit.
439 final int targetBytes = Math.min(absMaxSize,
440 tp.approxMinOverheadBytes +
441 Math.max(1,
442 (tp.thumbnailBppHint *
443 thumbnailXyDim.width *
444 thumbnailXyDim.height + 7) / 8));
445
446 // Compute target max/min boundaries.
447 // We'll stop immediately if we hit one of these.
448 final int targetMax = Math.min(absMaxSize,
449 targetBytes + targetBytes / THUMBNAIL_SIZE_TOL);
450 final int targetMin = Math.max(minSize,
451 targetBytes - targetBytes / THUMBNAIL_SIZE_TOL);
452
453 return(makeSizeConstrainedEncodedImage(tp.minQuality,
454 tp.initialQualityHint,
455 tp.maxQuality,
456 scaledImage,
457 targetMin, targetMax,
458 minSize, absMaxSize,
459 targetBytes));
460 }
461
462 /**Make a byte[]-encoded image of this type constrained above and below by size; null if not possible.
463 * This uses a binary-chop algorithm to attempt to quickly
464 * find the optimal "quality" to make the image at,
465 * lower quality values asking the encoder to discard more information,
466 * eg by doing colour reduction or other quantisation.
467 * <p>
468 * This is not possible for all exhibit types.
469 *
470 * @param upperQualityBound maximum value of quality to use; non-negative
471 * @param lowerQualityBound minimum value of quality to use; non-negative
472 * and no greater than upperQualityBound
473 * @param initialQualityHint initial suggested quality hint; non-negative,
474 * no greater than upperQualityBound, no less than lowerQualityBound
475 * @param inputImage the input image to encode
476 * @param targetMin target lower bound, should be higher than absMinSize
477 * @param targetMax target upper bound, should be lower than absMaxSize
478 * @param absMinSize absolute minimum number of bytes
479 * @param absMaxSize absolute maximum number of bytes
480 * @param targetBytes target number of bytes
481 *
482 * @return encoded image, or null if the image cannot be generated
483 * with given constraints
484 *
485 * @throws IOException in case of difficulty generating the image
486 * @throws IllegalArgumentException if the arguments are invalid
487 */
488 @Override
489 public byte[] makeSizeConstrainedEncodedImage(int lowerQualityBound,
490 final int initialQualityHint,
491 int upperQualityBound,
492 final BufferedImage inputImage,
493 final int targetMin,
494 final int targetMax,
495 final int absMinSize,
496 final int absMaxSize,
497 final int targetBytes)
498 throws IOException,
499 IllegalArgumentException
500 {
501 if((lowerQualityBound < 0) ||
502 (upperQualityBound < 0))
503 { throw new IllegalArgumentException("lower or upper quality bounds negative"); }
504 if(lowerQualityBound > upperQualityBound)
505 { throw new IllegalArgumentException("quality bounds reversed"); }
506 if((initialQualityHint < lowerQualityBound) ||
507 (initialQualityHint > upperQualityBound))
508 { throw new IllegalArgumentException("initialQualityHint out of bounds"); }
509
510 if((absMinSize < 0) ||
511 (absMaxSize < 0))
512 { throw new IllegalArgumentException("lower or upper size bounds negative"); }
513 if(absMinSize > absMaxSize)
514 { throw new IllegalArgumentException("size bounds reversed"); }
515
516 // Note the best image so far, or null if none.
517 // We will use this even if we don't find one near enough to
518 // the target to stop early.
519 //
520 // Best means:
521 // * Within the absolute bounds on size.
522 // * Arithmetically closest in size to the target.
523 byte bestImageSoFar[] = null;
524
525 // Attempt to get reasonably close to the optimal value by binary chop.
526 for(int qual = initialQualityHint;
527 (qual >= lowerQualityBound) && (qual <= upperQualityBound);
528 /* Re-init happens at loop end. */ )
529 {
530 final byte[] image = makeImageBinary(inputImage, qual);
531
532 // Assume that the failure to generate an image
533 // means that the quality parameter was unusably low.
534 final int tfl = (image == null) ? -1 : image.length;
535
536 //System.err.println(" makeThumbnailImage(): image size = "+tfl+" at quality: " + qual);
537
538 if(image != null)
539 {
540 // If image size is close to target, return image immediately.
541 if((tfl >= targetMin) && (tfl <= targetMax))
542 {
543 // We have an acceptable size; return it!
544 //System.err.println(" makeThumbnailImage(): done: within tolerance at quality: " + qual);
545 return(image);
546 }
547
548 // If target is within absolute limits,
549 // then remember it iff it is the best so far.
550 if((tfl >= absMinSize) && (tfl <= absMaxSize))
551 {
552 if((bestImageSoFar == null) ||
553 (Math.abs(image.length - targetBytes) <
554 Math.abs(bestImageSoFar.length - targetBytes)))
555 {
556 bestImageSoFar = image;
557 //System.err.println(" makeThumbnailImage(): best so far at quality: " + qual);
558 }
559 }
560 }
561
562 // Do we have to bump quality (and size) down?
563 final boolean goDown = (tfl > absMaxSize);
564 //System.err.println(" makeThumbnailImage(): goDown = "+goDown+" with quality: " + qual);
565
566 // Round again...
567 // Adjust the bounds and the new value to test.
568 final int oldQual = qual;
569 qual = goDown ? ((oldQual-1 + lowerQualityBound) / 2)
570 : ((oldQual+2 + upperQualityBound) / 2);
571 if(goDown) { upperQualityBound = Math.min(upperQualityBound, oldQual - 1); }
572 else { lowerQualityBound = Math.max(lowerQualityBound, oldQual + 1); }
573 }
574
575 //if(bestImageSoFar == null) { System.err.println(" makeThumbnailImage(): failed"); }
576 //else { System.err.println(" makeThumbnailImage(): done"); }
577
578 // If we got something acceptable, return it, else null.
579 return(bestImageSoFar);
580 }
581
582
583 /**Estimates bytes of working memory required to create thumbnails for an image.
584 * This includes the space to load the old image,
585 * plus one extra working copy of the original image,
586 * make one of the thumbnails at a time,
587 * and hold both the standard and small thumbnails in binary format.
588 * <p>
589 * This assumes still images amongst other things;
590 * for movies or animations this may, depending on the processing
591 * method, need to be multiplied by the number of frames, for example.
592 * <p>
593 * Fudge factors, such as guessed overheads, are liberally sprinkled in.
594 * <p>
595 * This assumes that the JVM and the codecs are reasonably efficient
596 * in their use of memory, but this is at best a wild guess.
597 * <p>
598 * A sophisticated handler may need to override this.
599 * <p>
600 * Should not be called if getThumbnailParams() returns null.
601 *
602 * @param xyDim pixel dimensions of original image; must be non-null
603 * @return estimated working memory in heap in bytes
604 */
605 protected int estimateWorkingMemoryToCreateThumbnails(final Dimension xyDim)
606 {
607 if((xyDim == null) || (xyDim.width < 1) || (xyDim.height < 1))
608 { throw new IllegalArgumentException(); }
609
610 final ThumbnailParams thumbnailParams = getThumbnailParams();
611 // Make a conservative guess at bytes-per-pixel if need be.
612 final int rawBytesPerPixel = (thumbnailParams == null) ? 4 :
613 thumbnailParams.estimatedBytesPerImagePixelInMemory;
614 if(rawBytesPerPixel <= 0)
615 { throw new IllegalArgumentException(); }
616
617 // This will give us the dimensions of the larger of the thumbnails
618 // and thus the larger of the two images to be created in memory
619 // before being converted to (compressed) file format.
620 final Dimension stdTnXyDim =
621 ExhibitThumbnails.computeThumbnailDimensions(xyDim, true);
622
623 // Guess at overhead in memory for Image instance for
624 // colour maps, encoding parameters, etc.
625 final int fixedImageOverhead = 10240;
626
627 // Estimated memory (bytes) used to load original image into memory,
628 // plus an additional working copy.
629 final int origImageMemory = fixedImageOverhead +
630 2 * (1+rawBytesPerPixel) * xyDim.width * xyDim.height;
631
632 // Estimated memory (bytes) used to make one of the thumbnails in memory
633 // (assumed to be the larger, ie standard one).
634 final int maxThumbnailImageMemory = fixedImageOverhead +
635 (1+rawBytesPerPixel) * stdTnXyDim.width * stdTnXyDim.height;
636
637 // Estimated memory (bytes) used to hold the final ExhibitThumbnails object,
638 // including the two binary file-format thumbnail images.
639 final int binaryThumbnailFormatMemory =
640 ExhibitThumbnails.STD_ABS_MAX_BYTES * 2;
641
642 return(origImageMemory +
643 maxThumbnailImageMemory +
644 binaryThumbnailFormatMemory);
645 }
646
647
648 /**Decode image using generic image IO routines; null if cannot be done.
649 * It should already have been established that the image is of the correct type.
650 * <p>
651 * Individual handlers may override this default (JAI) implementation if desired.
652 * <p>
653 * This <strong>does not close its input stream</strong> when done.
654 * <p>
655 * Calls to this implementation may be serialised
656 * because they can be very memory-intensive.
657 * <p>
658 * If the underlying decoder routine throws an (unchecked) exception,
659 * we absorb (and log) it and return null,
660 * thus making us robust in the face of some underlying handler weaknesses.
661 *
662 * @throw IOException in case of I/O problems during decode
663 */
664 @Override
665 public BufferedImage decodeImage(final InputStream is)
666 throws IOException
667 {
668 if(null == is) { throw new IllegalArgumentException(); }
669
670 // synchronized(_mem_lock)
671 {
672 ImageInputStream iis = null;
673 try
674 {
675 iis = javax.imageio.ImageIO.createImageInputStream(is);
676 // Give up if we can't create a wrapped stream.
677 if(null == iis) { return(null); }
678
679 setJAICacheParams(); // (Re)set JAI cache parameters.
680
681 final Iterator<ImageReader> iter = javax.imageio.ImageIO.getImageReaders(iis);
682 // Give up if no suitable readers...
683 if(!iter.hasNext()) { return(null); }
684
685 // Attempt to decode the image.
686 final ImageReader reader = iter.next();
687 final BufferedImage bi;
688 try
689 {
690 final ImageReadParam param = reader.getDefaultReadParam();
691 reader.setInput(iis, true, true);
692 bi = reader.read(0, param);
693 }
694 finally { reader.dispose(); } // Free resources ASAP.
695
696 return(bi);
697 }
698 // Pass up I/O exceptions unmolested.
699 catch(final IOException e) { throw e; }
700 // Log other exceptions and return null.
701 catch(final Exception e) { e.printStackTrace(); return(null); }
702 finally
703 {
704 // Ensure that resources are released promptly.
705 // Assumed NOT to close the underlying InputStream.
706 if(null != iis) { iis.close(); }
707 }
708 }
709 }
710
711 /**Get dimensions X and Y of an image by decoding entire image, else null if dimensions cannot be computed.
712 * This may be extremely slow and inefficient and should be overridden by handlers
713 * if at all possible.
714 * <p>
715 * This <strong>does not close its input stream</strong> when done.
716 * <p>
717 * Calls to this implementation may be serialised
718 * because they may be very memory-intensive.
719 *
720 * @param is the exhibit as a binary data stream
721 *
722 * @throws IOException in case of problems with corrupt data
723 * (or a broken exhibit)
724 */
725 @Override public Dimension get2DImageDimensions(final InputStream is)
726 throws IOException
727 {
728 // synchronized(_mem_lock)
729 {
730 final BufferedImage bi = decodeImage(is);
731 if(bi == null) { return(null); }
732 return(new Dimension(bi.getWidth(), bi.getHeight()));
733 }
734 }
735
736
737 /**If true, only get "standard" (lossy) metadata to save space. */
738 private static final boolean ONLY_GET_STD_METADATA = true;
739
740 /**Standard metadata format name.
741 * This seems to be rather badly hidden in the API: yeuck!
742 */
743 private static final String STD_METADATA_FORMAT_NAME = IIOMetadataFormatImpl.standardMetadataFormatName;
744
745 /**Get specific (image) metadata for one particular image type; null if none.
746 * This does not have to be provided, but if so enables such things as
747 * EXIF and IPTC extraction from JPEG images.
748 * <p>
749 * This is passed the metadata for the first available image.
750 * <p>
751 * This may be a mix of stream and image/frame metadata.
752 *
753 * @param exhibitName full exhibit name for diagnostics; never null
754 */
755 protected Node extractSpecificImageMetaData(final IIOMetadata imageMetadata, final Name.ExhibitFull exhibitName)
756 { return(null); }
757
758 /**If true, trim bulky and not-very-useful metadata (eg palette entries). */
759 private static final boolean TRIM_METADATA = true;
760
761 /**Maximum number of children of a single node before culling/trimming it; strictly positive. */
762 private static final int TRIM_CHILDREN_MAX = 127;
763
764 /**Removes bulky and not-very-useful metadata sub-trees from below the given node.
765 * This removes the following:
766 * <ul>
767 * <li>Any "Palette" subtree.
768 * <li>Any hugely-branched subtree (a very large number of children of one node).
769 * (This may be reported on System.out.)
770 * </ul>
771 * @param exhibitName non-null full exhibit name for diagnostics
772 */
773 protected void _trimMetadata(final Node n, final Name.ExhibitFull exhibitName)
774 {
775 if(n == null) { return; } // Ignore missing node.
776
777 for(Node child = n.getFirstChild(); child != null; child = child.getNextSibling())
778 {
779 final String childName = child.getNodeName();
780 final int size = child.getChildNodes().getLength();
781
782 // Remove bulky palette sub-tree...
783 if("Palette".equals(childName))
784 {
785 //System.out.println("[INFO: AbstractImageHandler._trimMetadata(): removed Palette node of size "+size+" from "+exhibitName+".]");
786 n.removeChild(child);
787 continue;
788 }
789
790 // Trim very large nodes (with many children).
791 if(size > TRIM_CHILDREN_MAX)
792 {
793 //System.out.println("[INFO: AbstractImageHandler._trimMetadata(): removed (large) '"+childName+"' node of size "+size+" from "+exhibitName+".]");
794 n.removeChild(child);
795 continue;
796 }
797
798 // Recursively trim child nodes as needed.
799 _trimMetadata(child, exhibitName);
800 }
801 }
802
803 // /**Lock to serialise assumed-memory-intensive operations. */
804 // private static final Object _mem_lock = new Object();
805
806 /**Gets all available exhibit metadata as a single XML DOM tree; null if none.
807 * We do not (yet) have an XML schema for this and may never do so,
808 * since its structure and content will depend on various external sources
809 * and parts of the ImageIO and JAI subsystems and others.
810 * <p>
811 * This tries to fetch any stream data and any metadata from the first available image.
812 * <p>
813 * Note that this has to turn off the ImageIO file cache to avoid failing
814 * since there seems to be no cache clearing.
815 * <p>
816 * By default we trim some of the most bulky and least useful data from the metadata,
817 * mainly the palette information for indexed images.
818 * <p>
819 * This implementation is serialised as it may be very memory-intensive.
820 *
821 * @param is input stream containing exhibit data; never null
822 * @param exhibitName full exhibit name (primarily to help with debugging); never null
823 * @return top-level node "metadata" with captured metadata beneath, else null
824 */
825 @Override
826 public Node getMetadata(final InputStream is,
827 final ExhibitFull exhibitName)
828 {
829 // synchronized(_mem_lock)
830 {
831 Node result = null;
832
833 // We have problems running out of space on disc, etc, without the ImageIO cache disabled.
834 setJAICacheParams(); // (Re)set JAI cache parameters.
835
836 try
837 {
838 final ImageInputStream iis = ImageIO.createImageInputStream(is);
839 if(iis == null) { return(null); }
840 try
841 {
842 // Try and find a reader for this image type (and use the first available).
843 final Iterator<ImageReader> readerIt =
844 ImageIO.getImageReadersByMIMEType(getExhibitType().mimeType);
845 // If no reader, give up.
846 if(!readerIt.hasNext()) { return(null); }
847
848 final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
849 final DocumentBuilder db = dbf.newDocumentBuilder();
850 final Document doc = db.newDocument();
851
852 // Create a provisional root node,
853 // but only assign it to the result (ie to get a non-null result)
854 // when we know that there is some real data in there.
855 final Element root = doc.createElement(TAG_NAME_METADATA_TOP);
856 doc.appendChild(root);
857
858 final ImageReader ir = readerIt.next();
859 try
860 {
861 ir.setInput(iis);
862
863 // Try to extract (stream) metadata, if any.
864 final IIOMetadata mdStream = ir.getStreamMetadata();
865
866 if((mdStream != null) &&
867 (!ONLY_GET_STD_METADATA || mdStream.isStandardMetadataFormatSupported()))
868 {
869 final String[] mdFormatNames = !ONLY_GET_STD_METADATA ? mdStream.getMetadataFormatNames() :
870 new String[]{ STD_METADATA_FORMAT_NAME };
871 if(mdFormatNames != null)
872 {
873 final Element item = doc.createElement("stream");
874 root.appendChild(item);
875 for(final String formatName : mdFormatNames)
876 {
877 TextUtils.importCopy(item, mdStream.getAsTree(formatName));
878 if(TRIM_METADATA) { _trimMetadata(item, exhibitName); }
879 }
880 // { item.appendChild(doc.importNode(mdImage.getAsTree(formatName), true)); }
881 result = root; // We have some real metadata.
882 }
883 }
884
885 // Try to extract metadata, if any, from first image.
886 final IIOMetadata mdImage = ir.getImageMetadata(ir.getMinIndex());
887
888 if((mdImage != null) &&
889 (!ONLY_GET_STD_METADATA || mdImage.isStandardMetadataFormatSupported()))
890 {
891 final String[] mdFormatNames = !ONLY_GET_STD_METADATA ? mdImage.getMetadataFormatNames() :
892 new String[]{ STD_METADATA_FORMAT_NAME };
893 if(mdFormatNames != null)
894 {
895 final Element item = doc.createElement("image");
896 root.appendChild(item);
897 for(final String formatName : mdFormatNames)
898 {
899 TextUtils.importCopy(item, mdImage.getAsTree(formatName));
900 if(TRIM_METADATA) { _trimMetadata(item, exhibitName); }
901 }
902 // { item.appendChild(doc.importNode(mdImage.getAsTree(formatName), true)); }
903 result = root; // We have some real metadata.
904 }
905 }
906
907 // Try to extract additional format-specific metadata (eg EXIF from JPEG).
908 if(mdImage != null)
909 {
910 final Node n = extractSpecificImageMetaData(mdImage, exhibitName);
911 if(n != null)
912 { TextUtils.importCopy(root, n); }
913 }
914 }
915 finally
916 { ir.dispose(); } // Free (non-JVM) resources ASAP.
917 }
918 finally
919 { iis.close(); } // Free (non-JVM) resources ASAP.
920 }
921 catch(final IOException e)
922 {
923 e.printStackTrace();
924 return(null); // Cannot read stream...
925 }
926 catch(final ParserConfigurationException e)
927 {
928 e.printStackTrace();
929 return(null); // Cannot build document...
930 }
931
932 //System.err.println("getMetadata(): " + TextUtils.toXML(result, false, true));
933 return(result);
934 }
935 }
936
937 /**Gets an encoder/writer for this image type if one is available, else returns null.
938 * Uses IIO facilities to locate an available writer.
939 * <p>
940 * Any non-null value returned should be dispose()d of when finished with.
941 */
942 protected ImageWriter _getEncoder()
943 {
944 final ExhibitTypeParameters exhibitType = getExhibitType();
945
946 // Once we know that we can't get an encoder for this (MIME) type
947 // then don't try again.
948 if(Boolean.FALSE == encoderExists.get(exhibitType.type))
949 { return(null); }
950
951 final Iterator<ImageWriter> wit =
952 ImageIO.getImageWritersByMIMEType(exhibitType.mimeType);
953
954 // Return null if no encoder available.
955 if((wit == null) || !wit.hasNext())
956 {
957 encoderExists.set(exhibitType.type, Boolean.FALSE); // Remember that no encoder is available.
958 return(null);
959 }
960
961 // Return first writer/encoder.
962 final ImageWriter result = wit.next();
963 assert(result != null);
964 encoderExists.set(exhibitType.type, Boolean.TRUE); // Remember that an encoder is available.
965 return(result);
966 }
967
968 /**Thread-safe bounded-size cache for canMakeThumbnails() and _getEncoder() as to whether an encoder can be created; never null.
969 * This is a mapping from the exhibit type number to a tri-state value:
970 * <ul>
971 * <li>null means it is not known if we can generate an encoder for this type</li>
972 * <li>TRUE means that we can generate an encoder for this type</li>
973 * <li>FALSE means that we cannot generate an encoder for this type</li>
974 * </ul>
975 * <p>
976 * Updated by _getEncoder() but also used by canMakeThumbnails()
977 * to avoid calling _getEncoder() if possible
978 * for speed and to save possible resource leaks.
979 * </p>
980 * This size of this is limited by the number of exhibit types we support.
981 */
982 private static final AtomicReferenceArray<Boolean> encoderExists = new AtomicReferenceArray<Boolean>(ExhibitMIME.ET__max + 1);
983
984 /**Claim that we can make thumbnails for this image type if we can find parameters and an encoder at run-time. */
985 @Override public boolean canMakeThumbnails()
986 {
987 if(null == getThumbnailParams()) { return(false); }
988
989 // If we know the availability of an encoder for sure
990 // then we need not actually fetch one.
991 final Boolean available = encoderExists.get(getExhibitType().type);
992 if(null != available) { return(available); }
993 // OK, actually try to get an encoder.
994 final ImageWriter iw = _getEncoder();
995 if(null == iw) { return(false); }
996 iw.dispose(); // Release resources.
997 return(true);
998 }
999
1000 // /**Encode the given BufferedImage for this image type in a simple way (using IIO); never null.
1001 // * @param imageIn must have colour space (etc) compatible with image type; non-null
1002 // * @return the encoded file-format byte array
1003 // * @throws java.io.IOException in case of difficulty with the encoding
1004 // */
1005 // protected byte[] _makeImageBinarySimple(final BufferedImage imageIn)
1006 // throws IOException
1007 // {
1008 // final ImageWriter wr = _getEncoder();
1009 //
1010 // // If no writer available then give up immediately.
1011 // if(wr == null) { return(null); }
1012 //
1013 // final ImageWriteParam writeParam = wr.getDefaultWriteParam();
1014 // if(writeParam != null) { writeParam.setCompressionQuality(quality); }
1015 //
1016 // try
1017 // {
1018 // final ByteArrayOutputStream baos = new ByteArrayOutputStream();
1019 // ImageIO.setUseCache(false); // Avoid using the (disc) cache.
1020 // final ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
1021 // if(ios == null) { return(null); }
1022 //
1023 // try
1024 // {
1025 // wr.setOutput(ios);
1026 // wr.write(null, new IIOImage(imageIn, null, null), writeParam);
1027 // }
1028 // finally
1029 // { ios.close(); }
1030 //
1031 // final byte[] result = baos.toByteArray();
1032 //
1033 // // Do not allow zero-length return value.
1034 // if(result.length == 0)
1035 // { throw new IOException("generated zero-length result"); }
1036 //
1037 // return result;
1038 // }
1039 // finally
1040 // { wr.dispose(); }
1041 // }
1042
1043 /**Guide colour-reduction to reduce quality and size of lossless image formats.
1044 * Defaults to -1, implying that the image format is a lossy format
1045 * for which the ImageWriteParam.setCompressionQuality() mechanism
1046 * is enough to trade encoded image size for quality.
1047 * <p>
1048 * Else this is a positive value n, typically 8 or 24,
1049 * which sets a quality level at/below which the number of colours in the image
1050 * is reduced to (capped at) 2^n, and the image representation may be tweaked too
1051 * (eg converted to a palette/indexed format) as a strong hint to the encoder.
1052 */
1053 protected int _reduceColoursQualityThreshold() { return(-1); }
1054
1055 /**Creates output file-formatted; null for a permanent failure.
1056 * Can return null for a permanent error handling a particular image,
1057 * eg in the ImageIO GIF/PNG drivers for some palette sizes.
1058 * <p>
1059 * We are careful to dispose() of our write when done to release resources.
1060 *
1061 * @param imageIn single image to encode, never null
1062 * @param quality desired encoding quality in range 0--100 inclusive,
1063 * with 0 interpreted as "high compression is important,"
1064 * and 100 (or over) interpreted as "high image quality is important.";
1065 * this parameter is ignored where it does not make sense for the format
1066 */
1067 @Override
1068 public byte[] makeImageBinary(BufferedImage imageIn, int quality)
1069 throws IOException
1070 {
1071 if(imageIn == null)
1072 { throw new IllegalArgumentException(); }
1073 if(quality < 0)
1074 { throw new IllegalArgumentException("quality param must be in range [0--100]"); }
1075
1076 /**Coerce "quality" into range if need be. */
1077 if(quality > 100)
1078 { quality = 100; }
1079
1080 final ImageWriter wr = _getEncoder(); // This must be dispose()d if non-null.
1081 // If no writer available then give up immediately.
1082 if(wr == null) { return(null); }
1083 try
1084 {
1085 final ImageWriteParam writeParam = wr.getDefaultWriteParam();
1086
1087 if(writeParam != null)
1088 {
1089 try { writeParam.setCompressionQuality(quality / 100.0f); }
1090 catch(final Exception e) { /* Quietly ignore where this is not allowed/understood. */ }
1091 }
1092
1093 // If appropriate then try colour reduction
1094 // and conversion to palette/indexed form.
1095 final int threshold = _reduceColoursQualityThreshold();
1096 if(quality <= threshold)
1097 {
1098 // Chose a maximum number of colours that is a power of two
1099 // to try to maximise the encoding efficiency.
1100 final int maxColours = (1 << (Math.min(30, Math.max(1, quality))));
1101
1102 // System.out.println("quality-reduced encoding (quality = "+quality+"): maxColours = " + maxColours);
1103
1104 // Force the output to a byte-indexed model to
1105 // maximise chance of compression
1106 // if no more than 8 bits.
1107 final boolean forceToByteIndexModel = (quality <= 8);
1108 imageIn = ImageUtils.makeColourReducedBufferedImage(imageIn,
1109 maxColours,
1110 forceToByteIndexModel);
1111 }
1112
1113 // Now generate the binary/file encoded image.
1114 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
1115 setJAICacheParams(); // (Re)set JAI cache parameters.
1116 final ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
1117 if(ios == null) { return(null); }
1118 try
1119 {
1120 wr.setOutput(ios);
1121 wr.write(null, new IIOImage(imageIn, null, null), writeParam);
1122 }
1123 finally
1124 { ios.close(); }
1125
1126 final byte[] result = baos.toByteArray();
1127
1128 // Do not allow zero-length return value.
1129 if(result.length == 0)
1130 { throw new IOException("generated zero-length result"); }
1131
1132 return(result);
1133 }
1134 catch(final IllegalArgumentException e)
1135 {
1136 System.err.println("ERROR: could not makeImageBinary(): returning null to indicate permanent error for supplied parameters");
1137 e.printStackTrace();
1138 return(null); /* Permanent error. */
1139 }
1140 finally
1141 { wr.dispose(); }
1142 }
1143
1144 /**Set/reset JAI cache/memory parameters.
1145 * This may be particularly important for small heap instances.
1146 * <p>
1147 * See: http://java.sun.com/products/java-media/jai/forDevelopers/jaifaq.html#memory1
1148 */
1149 private static void setJAICacheParams()
1150 {
1151 ImageIO.setUseCache(false); // Avoid using the (disc) cache.
1152 try
1153 {
1154 // Limit the JAI cache to be a capped small fraction of (free) memory,
1155 // but always at least 1MB...
1156 JAI.getDefaultInstance().getTileCache().setMemoryCapacity(Math.max(1024*1024,
1157 Math.min(32*1024*1024, Runtime.getRuntime().freeMemory()/16)));
1158 }
1159 catch(final Exception e) { e.printStackTrace(); } // Absorb but log any errors...
1160 }
1161 // Set JAI cache parameters for the first time at class initialisation...
1162 static { setJAICacheParams(); }
1163 }