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        }