001    /*
002    Copyright (c) 1996-2012, 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     * Created by IntelliJ IDEA.
031     * User: Administrator
032     * Date: 28-Dec-02
033     * Time: 22:24:51
034     * To change template for new class use
035     * Code Style | Class Templates options (Tools | IDE Options).
036     */
037    package org.hd.d.pg2k.test.dev;
038    
039    import java.awt.Dimension;
040    import java.awt.image.BufferedImage;
041    import java.awt.image.DataBuffer;
042    import java.awt.image.IndexColorModel;
043    import java.io.ByteArrayInputStream;
044    import java.io.DataInputStream;
045    import java.io.File;
046    import java.io.FileInputStream;
047    import java.io.IOException;
048    import java.util.Random;
049    import java.util.Set;
050    import java.util.TreeSet;
051    
052    import junit.framework.TestCase;
053    
054    import org.hd.d.pg2k.svrCore.AllExhibitProperties;
055    import org.hd.d.pg2k.svrCore.ExhibitStaticAttr;
056    import org.hd.d.pg2k.svrCore.ExhibitThumbnails;
057    import org.hd.d.pg2k.svrCore.ImageUtils;
058    import org.hd.d.pg2k.svrCore.Name;
059    import org.hd.d.pg2k.svrCore.TextUtils;
060    import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
061    import org.hd.d.pg2k.svrCore.MIME.Handler;
062    import org.hd.d.pg2k.svrCore.MIME.Quantize;
063    import org.hd.d.pg2k.svrCore.datasource.ExhibitDataFileSource;
064    import org.hd.d.pg2k.svrCore.props.LocalProps;
065    import org.w3c.dom.Node;
066    
067    /**Tests of media-generation classes, eg for making thumbnails.
068     * This will test features such as transcoding, etc.
069     * <p>
070     * Also tests some relevant features of the ExhibitMIME class.
071     */
072    public final class MediaHandlerTest extends TestCase
073        {
074        public MediaHandlerTest(final String name)
075            {
076            super(name);
077            }
078    
079        /**Test some basic features of ExhibitMIME.
080         * Including all exhibit type values being in range, in order, and unique.
081         */
082        public void testExhibitMIMEBasics()
083            {
084            // Make sure that all types within the max/min limits.
085            final ExhibitMIME.ExhibitTypeParameters[] allTypes = ExhibitMIME.getAllValidExhibitTypes();
086            assertTrue("ExhibitMIME.getAllValidExhibitTypes() must not return null", allTypes != null);
087    
088            int prevType = ExhibitMIME.ET__max + 1; // Previous type value.
089            for(int i = allTypes.length; --i >= 0; )
090                {
091                final ExhibitMIME.ExhibitTypeParameters etp = allTypes[i];
092                final int t = etp.type;
093                assertTrue("All ExhibitTypeParameters values must be in max/min range",
094                           ((t >= ExhibitMIME.ET__min) && (t <= ExhibitMIME.ET__max)));
095                assertTrue("All ExhibitTypeParameters values must unique and in order",
096                           t < prevType);
097                prevType = t;
098    //            assertTrue("Minimum format overhead should be no smaller than magic",
099    //                       (etp.handler == null) ||
100    //                       (etp.handler.get() >= etp.magic.length()));
101                }
102            }
103    
104        /**Does some basic tests on the JPG (JPEG) handler.
105         * For example, make sure that it always exists.
106         */
107        public void testJPGBasics()
108            throws Exception
109            {
110            // Check that we have a .jpg handler.
111            final ExhibitMIME.ExhibitTypeParameters type = ExhibitMIME.getParamsByType(ExhibitMIME.ET_JPEG);
112            final Handler h = type.handler;
113            assertTrue("Must always have a valid JPEG (.jpg) handler", h != null);
114            // Check that .jpg handler is prepared to try to make thumbnails.
115            assertTrue("JPEG (.jpg) handler should be prepared to make thumbnails", h.canMakeThumbnails());
116    
117            // Test that we can create a big image.
118            final BufferedImage bigImage = makeTestRGBTrueColourBufferedImage(
119                bigImageWidth, bigImageHeight);
120            final byte[] bigJPEGImage =
121                h.makeImageBinary(bigImage, 95);
122                //makeJPEGImage(bigImageWidth, bigImageHeight);
123            assertTrue("Must be able to create JPEG image from BufferedImage",
124                       bigJPEGImage != null);
125            assertTrue("Must be able to create valid JPEG image from BufferedImage",
126                       ExhibitMIME.magicOK(type, new ByteArrayInputStream(bigJPEGImage)));
127            assertTrue("Encoded image should be much bigger than 1kByte",
128                       bigJPEGImage.length > 1024);
129    
130            final ExhibitThumbnails tn = h.makeThumbnails(
131                new ByteArrayInputStream(bigJPEGImage),
132                bigJPEGImage.length
133                );
134            assertTrue("Must be able to make JPEG thumbnails of some sort",
135                       tn != null);
136            assertTrue("Must be able to make small JPEG thumbnail",
137                       tn.getSmall() != null);
138            assertTrue("Must be able to make standard JPEG thumbnail",
139                       tn.getStandard() != null);
140            assertTrue("Small thumbnail must be smaller than standard thumbnail",
141                       tn.getSmall().size() < tn.getStandard().size());
142            assertTrue("Standard thumbnail must be smaller than original",
143                       tn.getStandard().size() < bigJPEGImage.length);
144            assertTrue("Small thumbnail must be no larger than size limit",
145                       tn.getSmall().size() < ExhibitThumbnails.SML_ABS_MAX_BYTES);
146            assertTrue("Standard thumbnail must be no larger than size limit",
147                       tn.getStandard().size() < ExhibitThumbnails.STD_ABS_MAX_BYTES);
148    
149            // Fetch the metadata.
150            // We expect this to be non-null for JPEG.
151            final Node metadata = h.getMetadata(new ByteArrayInputStream(bigJPEGImage), Name.ExhibitFull.create("a/dummyName-X.jp2"));
152            assertNotNull("Standard meta-data should always exist for JPEG", metadata);
153            System.out.println(TextUtils.toXML(metadata, false, false));
154            }
155    
156        /**Does some basic tests on the JP2 (JPEG 2000) handler.
157         * For example, make sure that it always exists.
158         */
159        public void testJP2Basics()
160            throws Exception
161            {
162            // Check that we have a .jp2 handler.
163            final ExhibitMIME.ExhibitTypeParameters type = ExhibitMIME.getParamsByType(ExhibitMIME.ET_JP2);
164            final Handler h = type.handler;
165            assertTrue("Must always have a valid JPEG 2000 (.jp2) handler", h != null);
166    
167            // Test that we can create a big image.
168            final BufferedImage bigImage = makeTestRGBTrueColourBufferedImage(
169                bigImageWidth, bigImageHeight);
170            final byte[] bigJPEG2000Image =
171                h.makeImageBinary(bigImage, 95);
172            assertTrue("Must be able to create JPEG 2000 image from BufferedImage",
173                       bigJPEG2000Image != null);
174            assertTrue("Must be able to create valid JPEG 2000 image from BufferedImage",
175                       ExhibitMIME.magicOK(type, new ByteArrayInputStream(bigJPEG2000Image)));
176            assertTrue("Encoded image should be much bigger than 1kByte",
177                       bigJPEG2000Image.length > 1024);
178    
179            // Fetch the metadata.
180            // We expect this to be non-null for JPEG.
181            final Node metadata = h.getMetadata(new ByteArrayInputStream(bigJPEG2000Image), Name.ExhibitFull.create("a/dummyName-X.jp2"));
182            assertNotNull("Standard meta-data should always exist for JPEG 2000", metadata);
183            System.out.println(TextUtils.toXML(metadata, false, false));
184            }
185    
186        /**Does some basic tests on the GIF handler.
187         * Because of the Unisys LZW patent issues until 2004 this may not initially
188         * be prepared to make thumbnails.
189         */
190        public void testGIFBasics()
191            {
192            // Check that we have a .gif handler.
193            final Handler h = ExhibitMIME.getParamsByType(ExhibitMIME.ET_GIF).handler;
194            assertTrue("Should have a valid GIF (.gif) handler", h != null);
195            // Check that handler is prepared to try to make thumbnails.
196            assertTrue("GIF handler should be prepared to make thumbnails", h.canMakeThumbnails());
197            }
198    
199        /**Does some basic tests on the PNG handler.
200         */
201        public void testPNGBasics()
202            {
203            // Check that we have a .png handler.
204            final Handler h = ExhibitMIME.getParamsByType(ExhibitMIME.ET_PNG).handler;
205            assertTrue("Should have a valid PNG (.png) handler", h != null);
206            // Check that handler is prepared to try to make thumbnails.
207            assertTrue("PNG handler should be prepared to make thumbnails", h.canMakeThumbnails());
208            }
209    
210        /**Test thumbnail generation of a given set of exhibit types.
211         * In passing, tests that images can be generated at all.
212         */
213        public void testThumbnailGeneration()
214            throws Exception
215            {
216            final int typesToTest[] =
217                { ExhibitMIME.ET_PNG, ExhibitMIME.ET_JPEG, ExhibitMIME.ET_GIF };
218    
219            for(int i = typesToTest.length; --i >= 0; )
220                {
221                final int t = typesToTest[i];
222    
223                // Check that he have a handler.
224                final ExhibitMIME.ExhibitTypeParameters type = ExhibitMIME.getParamsByType(t);
225                final Handler h = type.handler;
226                assertTrue("Must always have a valid handler for t = " + t, h != null);
227                // Check that handler is prepared to try to make thumbnails.
228                assertTrue("Handler should be prepared to make thumbnails: " + type.suffixForInputFile, h.canMakeThumbnails());
229    
230                // Test that we can create a big image.
231                // Note that we use an indexed image for GIF tests.
232                final BufferedImage bigImage = (t == ExhibitMIME.ET_GIF) ?
233                        makeTestRGBIndexedColourBufferedImage(bigImageWidth, bigImageHeight) :
234                        makeTestRGBTrueColourBufferedImage(bigImageWidth, bigImageHeight);
235                final int bigImageLongestEdge = Math.max(bigImageWidth, bigImageHeight);
236                final byte[] bigEncodedImage =
237                    h.makeImageBinary(bigImage, 90);
238                    //makeJPEGImage(bigImageWidth, bigImageHeight);
239                assertTrue("Must be able to create image from BufferedImage",
240                           bigEncodedImage != null);
241                assertTrue("Must be able to create valid image from BufferedImage",
242                           ExhibitMIME.magicOK(type, new ByteArrayInputStream(bigEncodedImage)));
243                assertTrue("Encoded image should be much bigger than 1kByte",
244                           bigEncodedImage.length > 1024);
245    
246                final ExhibitThumbnails tn = h.makeThumbnails(
247                    new ByteArrayInputStream(bigEncodedImage),
248                    bigEncodedImage.length);
249                assertTrue("Must be able to make thumbnails of some sort",
250                           tn != null);
251                assertTrue("Must be able to make small thumbnail",
252                           tn.getSmall() != null);
253                assertTrue("Must be able to make standard thumbnail",
254                           tn.getStandard() != null);
255                assertTrue("Small thumbnail must be smaller than standard thumbnail",
256                           tn.getSmall().size() < tn.getStandard().size());
257                assertTrue("Standard thumbnail should be smaller than original",
258                           tn.getStandard().size() < bigEncodedImage.length);
259                assertTrue("Small thumbnail must be no larger than size limit",
260                           tn.getSmall().size() < ExhibitThumbnails.SML_ABS_MAX_BYTES);
261                assertTrue("Standard thumbnail must be no larger than size limit",
262                           tn.getStandard().size() < ExhibitThumbnails.STD_ABS_MAX_BYTES);
263                final Dimension stdTNDim = h.get2DImageDimensions(
264                    new ByteArrayInputStream(tn.getStandard().toByteArrray()));
265                final int stdTNLongestEdge = Math.max(stdTNDim.width, stdTNDim.height);
266                assertTrue("Standard thumbnail should have correct longest edge",
267                           stdTNLongestEdge == Math.min(ExhibitThumbnails.STD_STATIC_IMAGE_TN_LDIM_PX,
268                                                        bigImageLongestEdge));
269                final Dimension smlTNDim = h.get2DImageDimensions(
270                    new ByteArrayInputStream(tn.getSmall().toByteArrray()));
271                final int smlTNLongestEdge = Math.max(smlTNDim.width, smlTNDim.height);
272                assertTrue("Small thumbnail should have correct longest edge",
273                           smlTNLongestEdge == Math.min(ExhibitThumbnails.SML_STATIC_IMAGE_TN_LDIM_PX,
274                                                        bigImageLongestEdge));            }
275            }
276    
277        /**Test ability to accurately extract image dimensions from raw binaries.
278         * In passing, we will make sure that the overhead for each format is
279         * no larger than the (presumed valid, and very small) test images.
280         */
281        public void testDimensionExtraction()
282            throws IOException
283            {
284            // Dimensions for random-sized JPEG
285            // with the dimensions sometimes needing two bytes to hold.
286            final int jpegX = 64 + rnd.nextInt(512);
287            final int jpegY = 64 + rnd.nextInt(512);
288    
289            // Test data: exhibit type, XxY dimensions, raw data as byte array.
290            final class DataRow
291                {
292                public final int exhibitType;
293                public final Dimension xyPixels;
294                /**Raw exhibit data. */
295                public final byte data[];
296    
297                /**Construct a test case.
298                 * Does not defensively copy data as assumed to be in
299                 * benign environment.
300                 */
301                DataRow(final int type, final Dimension xy, final byte rawData[])
302                    {
303                    exhibitType = type;
304                    xyPixels = xy;
305                    data = rawData;
306                    }
307                }
308    
309            final DataRow testData[] =
310                {
311                // Simple 2x1 GIF image.
312                new DataRow(ExhibitMIME.ET_GIF, new Dimension(2, 1), new byte[]{
313    0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x2, 0x0,
314    0x1, 0x0, (byte) 0xb3, 0x0, 0x0, (byte) 0xff, (byte) 0x99, 0x66,
315    (byte) 0xff, (byte) 0x99, (byte) 0x99, (byte) 0xff, (byte) 0x99, (byte) 0xcc, (byte) 0xff, (byte) 0x99,
316    (byte) 0xff, (byte) 0xff, (byte) 0xcc, 0x0, (byte) 0xff, (byte) 0xcc, 0x33, (byte) 0xff,
317    (byte) 0xcc, 0x66, (byte) 0xff, (byte) 0xcc, (byte) 0x99, (byte) 0xff, (byte) 0xcc, (byte) 0xcc,
318    (byte) 0xff, (byte) 0xcc, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x0, (byte) 0xff, (byte) 0xff,
319    0x33, (byte) 0xff, (byte) 0xff, 0x66, (byte) 0xff, (byte) 0xff, (byte) 0x99, (byte) 0xff,
320    (byte) 0xff, (byte) 0xcc, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x2c, 0x0, 0x0,
321    0x0, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x4,
322    0x3, (byte) 0xf0, (byte) 0xbd, 0x8, 0x0, 0x3b
323                    }),
324    
325                // Simple 2x1 PNG image.
326                new DataRow(ExhibitMIME.ET_PNG, new Dimension(2, 1), new byte[]{
327    (byte) 0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa,
328    0x0, 0x0, 0x0, 0xd, 0x49, 0x48, 0x44, 0x52,
329    0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x1,
330    0x4, 0x3, 0x0, 0x0, 0x0, 0x6, 0xc, 0x62,
331    (byte) 0xb9, 0x0, 0x0, 0x0, 0x2c, 0x74, 0x45, 0x58,
332    0x74, 0x43, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f,
333    0x6e, 0x20, 0x54, 0x69, 0x6d, 0x65, 0x0, 0x54,
334    0x75, 0x65, 0x20, 0x32, 0x36, 0x20, 0x41, 0x75,
335    0x67, 0x20, 0x32, 0x30, 0x30, 0x33, 0x20, 0x32,
336    0x30, 0x3a, 0x31, 0x34, 0x3a, 0x35, 0x34, 0x20,
337    0x2d, 0x30, 0x30, 0x30, 0x30, (byte) 0xe5, (byte) 0xc3, 0x1c,
338    0x21, 0x0, 0x0, 0x0, 0x7, 0x74, 0x49, 0x4d,
339    0x45, 0x7, (byte) 0xd3, 0x8, 0x1a, 0x13, 0x14, 0x36,
340    (byte) 0xa8, (byte) 0xde, 0x67, (byte) 0xfe, 0x0, 0x0, 0x0, 0x9,
341    0x70, 0x48, 0x59, 0x73, 0x0, 0x0, 0xb, 0x12,
342    0x0, 0x0, 0xb, 0x12, 0x1, (byte) 0xd2, (byte) 0xdd, 0x7e,
343    (byte) 0xfc, 0x0, 0x0, 0x0, 0x4, 0x67, 0x41, 0x4d,
344    0x41, 0x0, 0x0, (byte) 0xb1, (byte) 0x8f, 0xb, (byte) 0xfc, 0x61,
345    0x5, 0x0, 0x0, 0x0, 0x3, 0x50, 0x4c, 0x54,
346    0x45, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xa7, (byte) 0xc4, 0x1b, (byte) 0xc8,
347    0x0, 0x0, 0x0, 0xa, 0x49, 0x44, 0x41, 0x54,
348    0x78, (byte) 0xda, 0x63, 0x60, 0x0, 0x0, 0x0, 0x2,
349    0x0, 0x1, (byte) 0xe5, 0x27, (byte) 0xde, (byte) 0xfc, 0x0, 0x0,
350    0x0, 0x0, 0x49, 0x45, 0x4e, 0x44, (byte) 0xae, 0x42,
351    0x60, (byte) 0x82
352                    }),
353    
354                // Simple 2x1 JPG image.
355                new DataRow(ExhibitMIME.ET_JPEG, new Dimension(2, 1), new byte[]{
356    (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x0, 0x10, 0x4a, 0x46,
357    0x49, 0x46, 0x0, 0x1, 0x1, 0x0, 0x1, 0x2c,
358    0x1, 0x2c, 0x0, 0x0, (byte) 0xff, (byte) 0xdb, 0x0, 0x43,
359    0x0, 0x6, 0x4, 0x5, 0x6, 0x5, 0x4, 0x6,
360    0x6, 0x5, 0x6, 0x7, 0x7, 0x6, 0x8, 0xa,
361    0x10, 0xa, 0xa, 0x9, 0x9, 0xa, 0x14, 0xe,
362    0xf, 0xc, 0x10, 0x17, 0x14, 0x18, 0x18, 0x17,
363    0x14, 0x16, 0x16, 0x1a, 0x1d, 0x25, 0x1f, 0x1a,
364    0x1b, 0x23, 0x1c, 0x16, 0x16, 0x20, 0x2c, 0x20,
365    0x23, 0x26, 0x27, 0x29, 0x2a, 0x29, 0x19, 0x1f,
366    0x2d, 0x30, 0x2d, 0x28, 0x30, 0x25, 0x28, 0x29,
367    0x28, (byte) 0xff, (byte) 0xdb, 0x0, 0x43, 0x1, 0x7, 0x7,
368    0x7, 0xa, 0x8, 0xa, 0x13, 0xa, 0xa, 0x13,
369    0x28, 0x1a, 0x16, 0x1a, 0x28, 0x28, 0x28, 0x28,
370    0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
371    0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
372    0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
373    0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
374    0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
375    0x28, 0x28, 0x28, 0x28, 0x28, 0x28, (byte) 0xff, (byte) 0xc0,
376    0x0, 0x11, 0x8, 0x0, 0x1, 0x0, 0x2, 0x3,
377    0x1, 0x22, 0x0, 0x2, 0x11, 0x1, 0x3, 0x11,
378    0x1, (byte) 0xff, (byte) 0xc4, 0x0, 0x15, 0x0, 0x1, 0x1,
379    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
380    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8,
381    (byte) 0xff, (byte) 0xc4, 0x0, 0x14, 0x10, 0x1, 0x0, 0x0,
382    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
383    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, (byte) 0xff, (byte) 0xc4,
384    0x0, 0x14, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0,
385    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
386    0x0, 0x0, 0x0, 0x0, (byte) 0xff, (byte) 0xc4, 0x0, 0x14,
387    0x11, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
388    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
389    0x0, 0x0, (byte) 0xff, (byte) 0xda, 0x0, 0xc, 0x3, 0x1,
390    0x0, 0x2, 0x11, 0x3, 0x11, 0x0, 0x3f, 0x0,
391    (byte) 0xaa, 0x40, 0x7, (byte) 0xff, (byte) 0xd9
392                    }),
393    
394                // Random-sized JPG image.
395                new DataRow(ExhibitMIME.ET_JPEG, new Dimension(jpegX, jpegY),
396                            ExhibitMIME.getParamsByType(ExhibitMIME.ET_JPEG).handler.
397                                makeImageBinary(
398                                    makeTestRGBTrueColourBufferedImage(jpegX, jpegY), 50)),
399    
400                // Random-sized PNG image.
401                new DataRow(ExhibitMIME.ET_PNG, new Dimension(jpegX, jpegY),
402                            ExhibitMIME.getParamsByType(ExhibitMIME.ET_PNG).handler.
403                                makeImageBinary(
404                                    makeTestRGBTrueColourBufferedImage(jpegX, jpegY), 50)),
405                };
406    
407            // Passes data to given handler (which must exist)
408            // to test that it can correctly extract (X,Y) pixel dimensions.
409            for(int i = testData.length; --i >= 0; )
410                {
411                final DataRow dr = testData[i];
412                final ExhibitMIME.ExhibitTypeParameters etp = ExhibitMIME.getParamsByType(dr.exhibitType);
413                assertTrue("ExhibitMIME.getParamsByType() must find parameters for type: " + dr.exhibitType,
414                           etp != null);
415                assertTrue("ExhibitMIME.getParamsByType() must find parameters for correct type",
416                           etp.type == dr.exhibitType);
417                assertTrue("HandlerBase must exist for type: " + etp, etp.handler != null);
418                final Dimension dim = etp.handler.get2DImageDimensions(new ByteArrayInputStream(dr.data));
419                assertTrue("Wrong Dimension extracted",
420                           dr.xyPixels.equals(dim));
421    //            assertTrue("Minimum overhead is smaller than magic for: " + etp,
422    //                       etp.handler.approxMinOverheadBytes() >= etp.magic.length());
423    //            assertTrue("Minimum overhead is larger than test exhibit for: " + etp,
424    //                       etp.handler.approxMinOverheadBytes() <= dr.data.length);
425                }
426            }
427    
428    
429        /**Width of image big enough to make both thumbnails of; several times longest large thumbnail dimension.
430         * Package-visible to be available to other tests.
431         */
432        static final int bigImageWidth = ExhibitThumbnails.STD_STATIC_IMAGE_TN_LDIM_PX * 2;
433    
434        /**Height of image big enough to make both thumbnails of; several times longest large thumbnail dimension.
435         * Package-visible to be available to other tests.
436         */
437        static final int bigImageHeight = ExhibitThumbnails.STD_STATIC_IMAGE_TN_LDIM_PX * 3;
438    
439    
440        /**Create a test RGB fully-opaque true-colour image in memory.
441         * This image will be suitable to make thumbnails from, for example,
442         * if large enough.
443         * <p>
444         * This is an RGB image with a mixture of slowly-varying
445         * colours and noise.
446         * <p>
447         * Package-visible to be available to other tests.
448         */
449        static BufferedImage makeTestRGBTrueColourBufferedImage(final int width,
450                                                                final int height)
451            {
452            if((width < 1) || (height < 1))
453                { throw new IllegalArgumentException(); }
454    
455            // Create a blank RGB (buffered) image.
456            final BufferedImage bi = new BufferedImage(width,
457                                                       height,
458                                                       BufferedImage.TYPE_INT_ARGB);
459            // Set this image up to have a relatively smoothly-changing backdrop
460            // with some noise imposed on it.
461            // Efficiency is not a huge issue here...
462            for(int y = height; --y >= 0; )
463                {
464                final int r = Math.round((y / (float) height) * 128)
465                    + rnd.nextInt(16);
466                for(int x = width; --x >= 0; )
467                    {
468                    final int g = Math.round((x / (float) width) * 128)
469                        + rnd.nextInt(16);
470                    final int b = ((x+y) & 0xff);
471                    final int rgb = 0xff000000 | (r << 16) | (g << 8) | b;
472                    bi.setRGB(x, y, rgb);
473                    }
474                }
475    
476            return(bi);
477            }
478    
479        /**Create a byte-indexed fully-opaque RGB test image in memory.
480         * This image will be suitable to make thumbnails from, for example,
481         * if large enough.
482         * <p>
483         * This is an RGB image some geometric patterns
484         * in a small number of colours (but more than one colour),#
485         * but that should be colour-reducable.
486         * <p>
487         * In passing this also tests a technique for generating indexed-colour
488         * images given RGB values.
489         */
490        private static BufferedImage makeTestRGBIndexedColourBufferedImage(final int width, final int height)
491            {
492            if((width < 1) || (height < 1))
493                { throw new IllegalArgumentException(); }
494    
495            // Create a small palette of colours to use in a byte-indexed model.
496            // These are ARGB values with no transparency.
497            final int palette[] =
498                {
499                0xfffffff0, 0xff0f0f00, 0xffff000f, 0xff0f0f0f,
500                0xffffffff, 0xffffff00, 0xffff00ff, rnd.nextInt() | 0xff000000
501                };
502    
503            final IndexColorModel icm = new IndexColorModel(8, // 8-bit index.
504                                                            palette.length,
505                                                            palette,
506                                                            0, // Offset into palette[].
507                                                            false, // No alpha.
508                                                            -1, // No transparent colour.
509                                                            DataBuffer.TYPE_BYTE);
510    
511            // Create a blank RGB (buffered) image.
512            final BufferedImage bi = new BufferedImage(width,
513                                                       height,
514                                                       BufferedImage.TYPE_BYTE_INDEXED,
515                                                       icm);
516    
517            // Set this image up to have a regular pattern with some noise.
518            // We ensure that even a 2-pixel image will have two colours.
519            for(int y = height; --y >= 0; )
520                {
521                // Have vertical stripes with an non-uniform selection of
522                // colours from the palette (biased towards low-numbered entries).
523                final int yindex = rnd.nextInt(rnd.nextInt(palette.length) + 1);
524                for(int x = width; --x >= 0; )
525                    {
526                    // Have an alternating pixel pattern horizontally.
527                    final int index = (yindex + ((x^y)&1)) % palette.length;
528                    bi.setRGB(x, y, palette[index]);
529                    }
530                }
531    
532    
533            // Check that result is a multi-colour image if more than 1 pixel.
534            if(width * height > 1)
535                {
536                // Checks that the generated image has more than one colour
537                // (ignoring alpha).
538                final int firstPixelRGB = bi.getRGB(0, 0) & 0xffffff;
539                boolean oneColour = true; // Only one colour.
540                for(int y = height; --y >= 0; )
541                    {
542                    for(int x = width; --x >= 0; )
543                        {
544                        final int thisRGB = bi.getRGB(x, y) & 0xffffff;
545                        if(thisRGB != firstPixelRGB)
546                            {
547                            oneColour = false;
548                            break;
549                            }
550                        }
551                    }
552                assertTrue("Generated test image should have more than one colour if more than one pixel",
553                           !oneColour);
554                }
555    
556            return(bi);
557            }
558    
559    
560        /**Make test JPEG encoded image of reasonably typical size; never null.
561         * The image is bigger than any thumbnail,
562         * and encoded at a typical quality.
563         * <p>
564         * Will throw a jUnit exception if something unexpected happens.
565         * <p>
566         * Package-visible to be available to other tests.
567         *
568         * @throws IOException if the JPEG encoder routines have difficulty
569         */
570        static byte[] makeTestRGBTrueColourJPEGImage()
571            throws IOException
572            {
573            // Choose JPEG type...
574            final int t = ExhibitMIME.ET_JPEG;
575    
576            // Check that he have a handler.
577            final ExhibitMIME.ExhibitTypeParameters type = ExhibitMIME.getParamsByType(t);
578            final Handler h = type.handler;
579            assertTrue("Must always have a valid handler for t = " + t, h != null);
580            // Check that handler is prepared to try to make thumbnails.
581            assertTrue("Handler should be prepared to make thumbnails", h.canMakeThumbnails());
582    
583            // Test that we can create a big image.
584            final BufferedImage bigImage = makeTestRGBTrueColourBufferedImage(
585                bigImageWidth, bigImageHeight);
586    //        final int bigImageLongestEdge = Math.max(bigImageWidth, bigImageHeight);
587            final byte[] bigEncodedImage =
588                h.makeImageBinary(bigImage, 75);
589                //makeJPEGImage(bigImageWidth, bigImageHeight);
590            assertTrue("Must be able to create image from BufferedImage",
591                       bigEncodedImage != null);
592            assertTrue("Must be able to create valid image from BufferedImage",
593                       ExhibitMIME.magicOK(type, new ByteArrayInputStream(bigEncodedImage)));
594            assertTrue("Encoded image should be much bigger than 1kByte",
595                       bigEncodedImage.length > 1024);
596    
597            return(bigEncodedImage);
598            }
599    
600    
601    
602    
603        /**Test colour reduction of RGB and indexed images using ImageUtils routine.
604         * This tests that we don't get back more colours than we start with,
605         * and that this works regardless of source image type.
606         */
607        public void testColourReduction()
608            {
609            // Try alternately with indexed and non-indexed images
610            // at realistic thumbnail size.
611            for(int srcFormat = 2; --srcFormat >= 0; )
612                {
613                // Create a typical thumbnail-sized image.
614                final BufferedImage srcImage = (srcFormat == 0) ?
615                    makeTestRGBTrueColourBufferedImage(64, 48) :
616                    makeTestRGBIndexedColourBufferedImage(48, 64);
617    
618                // Try alternately forcing the output to indexed format and not.
619                for(int dstFormat = 2; --dstFormat >= 0; )
620                    {
621                    final boolean forceByteIndexModel = (dstFormat == 0);
622    
623                    // Try making successively smaller colour spaces.
624                    // This may not be able to do successful colour reduction
625                    // to very small numbers of colours.
626                    for(int colorBits = 17; --colorBits > 0; )
627                        {
628                        final int maxColours = (1 << colorBits);
629    
630                        // Extract copy of pixels from source image in TYPE_INT_ARGB format.
631                        // Set scansize to be width to efficiently pack the result.
632                        final int width = srcImage.getWidth();
633                        final int height = srcImage.getHeight();
634                        final int pixels[] =
635                            srcImage.getRGB(0, 0, width, height, null, 0, width);
636    
637                        // Count the colours in the original image (ignoring alpha).
638                        final Set<Integer> originalDistinctColours = new TreeSet<Integer>();
639                        for(int i = pixels.length; --i >= 0; )
640                            { originalDistinctColours.add(new Integer(pixels[i] & 0xffffff)); }
641    //System.out.println("testColourReduction(): original colour count: " + originalDistinctColours.size());
642    
643                        // Create a colour-reduced image.
644                        final BufferedImage reducedColImage =
645                            ImageUtils.makeColourReducedBufferedImage(srcImage,
646                                                                      maxColours,
647                                                                      forceByteIndexModel);
648                        // Count the colours in the new image (ignoring alpha).
649                        final int newColours =
650                            ImageUtils.countDistinct24BitRGBColours(reducedColImage);
651    //System.out.println("testColourReduction(): new colour count (maxColours:"+maxColours+"): " + newColours);
652    
653                        assertTrue("Must produce an output image", reducedColImage != null);
654                        assertTrue("Must produce an output image the same dimensions as the input",
655                                   (reducedColImage.getWidth() == width) &&
656                                   (reducedColImage.getHeight() == height));
657                        assertTrue("Must not increase number of colours",
658                                   newColours <= originalDistinctColours.size());
659                        assertTrue("Must leave no more than requested colours in result",
660                                   newColours <= maxColours);
661    //System.out.println("output colour model: " + reducedColImage.getColorModel());
662                        assertTrue("Must have at least two colours in palette if original did: new colour map is: " + reducedColImage.getColorModel(),
663                                   (originalDistinctColours.size() < 2) || (newColours >= 2));
664                        }
665                    }
666                }
667            }
668    
669    
670    
671        /**Test that we can do colour reduction with the ImageMagik-derived Quantizer.
672         */
673        public void testQuantizer()
674            {
675            // Create a typical thumbnail-sized image.
676            final BufferedImage bigImage = makeTestRGBTrueColourBufferedImage(
677                256, 192);
678    
679            // Try making successively smaller colour spaces.
680            for(int colorBits = 9; --colorBits > 0; )
681                {
682                final int maxColours = (1 << colorBits);
683    
684                // Extract copy of pixels from source image in TYPE_INT_ARGB format.
685                // Set scansize to be width to efficiently pack the result.
686                final int pixels[] =
687                    bigImage.getRGB(0, 0, bigImage.getWidth(), bigImage.getHeight(),
688                                null, 0, bigImage.getWidth());
689    
690                // Count the colours in the original image (ignoring alpha).
691                final Set<Integer> originalDistinctColours = new TreeSet<Integer>();
692                for(int i = pixels.length; --i >= 0; )
693                    { originalDistinctColours.add(new Integer(pixels[i] & 0xffffff)); }
694    //System.out.println("testQuantizer(): original colour count: " + originalDistinctColours.size());
695    
696                // Now do the colour reduction...
697                final int[] palette = Quantize.quantizeImage(
698                    new int[][]{pixels}, maxColours);
699    //System.out.println("testQuantizer(): new colour count: " + palette.length);
700                assertTrue("Must leave at least two colours in palette if orgininal had at least two",
701                           (originalDistinctColours.size() < 2) || (palette.length >= 2));
702    
703                // Create a new ARGB image and copy the new pixels in there.
704                final BufferedImage reducedColImage = new BufferedImage(
705                    bigImage.getWidth(), bigImage.getHeight(),
706                    BufferedImage.TYPE_INT_ARGB);
707                reducedColImage.setRGB(0, 0, bigImage.getWidth(), bigImage.getHeight(),
708                                pixels, 0, bigImage.getWidth());
709    
710                assertTrue("Must produce an output image", reducedColImage != null);
711                assertTrue("Must produce an output image the same dimensions as the input",
712                           (reducedColImage.getWidth() == bigImage.getWidth()) &&
713                           (reducedColImage.getHeight() == bigImage.getHeight()));
714                assertTrue("Must not increase number of colours",
715                           palette.length <= originalDistinctColours.size());
716                }
717            }
718    
719        /**Simple tests of exhibit metadata.
720         * For example, check that it is safe to attempt to extract metadata from all exhibits with a handler,
721         * even though there legitimately be no metadata to extract
722         * though we in fact expect to always extract some metadata from every object where we have a handler.
723         * <p/>
724         * We check that we can extract EXIF data from at least one (JPEG) exhibit.
725         */
726        public void testMetadataBasics()
727            throws Exception
728            {
729            // Set up a simple instance of a file data source using our sample data.
730            final ExhibitDataFileSource edfs = new ExhibitDataFileSource(null);
731    
732            // Collect the raw exhibit info from the underlying file system.
733            final AllExhibitProperties aepRaw = edfs.getAllExhibitProperties(-1);
734    
735            // Note if we found EXIF data.
736            boolean foundEXIF = false;
737            // We look for the existence of the following path.
738            final String EXIFPath[] = { "metadata", "native", "EXIF", "tag" };
739    
740            // For every exhibit that has a handler,
741            // try to extract available metadata,
742            // ensuring that no failure (eg exception) is caused,
743            // and that metadata is definitely available where it should be.
744            for(final ExhibitStaticAttr esa : aepRaw.aeid.getAllStaticAttrs())
745                {
746                final ExhibitMIME.ExhibitTypeParameters et = (ExhibitMIME.getInputFileType(esa.getCharSequence()));
747                assertNotNull("All exhibits should have a type: " + esa, et);
748                final Handler h = et.handler;
749                if(h == null) { continue; }
750    
751                // Read the data into memory in one go for simplicity.
752                // None of our sample exhibits is vast.
753                final byte data[] = new byte[(int) esa.length];
754                final File rawDataLoc = new File(LocalProps.getDataDir(), esa.getFilePath());
755                final DataInputStream dis = new DataInputStream(new FileInputStream(rawDataLoc));
756                try { dis.readFully(data); }
757                finally { dis.close(); }
758    
759                // Fetch the metadata directly; we expect there to be some for every type that has a handler...
760                final Node metadata = h.getMetadata(new ByteArrayInputStream(data), esa.getExhibitFullName());
761                assertNotNull("Standard meta-data should be extractable where we have a handler: " + et.suffixForInputFile, metadata + " " + esa);
762    //            System.out.println(TextUtils.toXML(metadata, false, false));
763    
764                if(metadata != null)
765                    {
766                    // Check that top-level metadata node name is correct.
767                    assertSame("Top-level metadata node incorrect", "metadata", metadata.getNodeName());
768    
769                    // Fetch the metadata from the EPC; should be equivalent to getting it directly.
770                    final Node metadataEPC = aepRaw.getExhibitPropsComputable(esa.getExhibitFullName()).getMetadata();
771                    assertNotNull("Metadata should be available via the EPC if available directly: "+metadata, metadataEPC);
772                    // FIXME: test that we get an equivalent tree...
773    
774                    // Check if this exhibit has EXIF metadata...
775                    Node n = metadata;
776                    for(int i = 0; ++i < EXIFPath.length; )
777                        {
778                        final String name = EXIFPath[i];
779                        boolean foundCorrectChild = false;
780                        for(Node child = n.getFirstChild(); child != null; child = child.getNextSibling())
781                            {
782                            if(name.equals(child.getNodeName()))
783                                {
784                                n = child;
785                                foundCorrectChild = true;
786                                if(i + 1 == EXIFPath.length)
787                                    {
788                                    System.out.println("Found EXIF metadata for: " + esa.getCharSequence());
789                                    foundEXIF = true;
790                                    }
791                                break;
792                                }
793                            }
794                        if(!foundCorrectChild) { break; }
795                        }
796                    }
797                }
798    
799            assertTrue("At least one (JPEG) exhibit must have EXIF metadata", foundEXIF);
800            }
801    
802    
803        /**Reasonable shared random-number generator. */
804        private static final Random rnd = new Random();
805        }