001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    http://www.geoapi.org
004 *
005 *    Copyright (C) 2012-2019 Open Geospatial Consortium, Inc.
006 *    All Rights Reserved. http://www.opengeospatial.org/ogc/legal
007 *
008 *    Permission to use, copy, and modify this software and its documentation, with
009 *    or without modification, for any purpose and without fee or royalty is hereby
010 *    granted, provided that you include the following on ALL copies of the software
011 *    and documentation or portions thereof, including modifications, that you make:
012 *
013 *    1. The full text of this NOTICE in a location viewable to users of the
014 *       redistributed or derivative work.
015 *    2. Notice of any changes or modifications to the OGC files, including the
016 *       date changes were made.
017 *
018 *    THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE
019 *    NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
020 *    TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT
021 *    THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY
022 *    PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
023 *
024 *    COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR
025 *    CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENTATION.
026 *
027 *    The name and trademarks of copyright holders may NOT be used in advertising or
028 *    publicity pertaining to the software without specific, written prior permission.
029 *    Title to copyright in this software and any associated documentation will at all
030 *    times remain with copyright holders.
031 */
032package org.opengis.test.coverage.image;
033
034import java.io.File;
035import java.io.Closeable;
036import java.io.IOException;
037import java.io.OutputStream;
038import java.io.ByteArrayInputStream;
039import java.io.ByteArrayOutputStream;
040import java.awt.image.DataBuffer;
041import java.awt.image.BufferedImage;
042import java.awt.image.RenderedImage;
043import javax.imageio.IIOException;
044import javax.imageio.IIOImage;
045import javax.imageio.ImageIO;
046import javax.imageio.ImageReader;
047import javax.imageio.ImageTypeSpecifier;
048import javax.imageio.ImageWriter;
049import javax.imageio.ImageWriteParam;
050import javax.imageio.spi.ImageReaderSpi;
051import javax.imageio.spi.ImageWriterSpi;
052import javax.imageio.metadata.IIOMetadata;
053import javax.imageio.stream.ImageInputStream;
054import javax.imageio.stream.ImageOutputStream;
055
056import org.junit.*;
057import static org.junit.Assert.*;
058import static org.junit.Assume.*;
059
060
061/**
062 * Base class for testing {@link ImageWriter} implementations. This test writes different regions
063 * and bands of an image at different sub-sampling levels, then read back the images and compare
064 * the sample values.
065 *
066 * <p>To use this test, subclasses need to set the {@link #writer} field to a non-null value
067 * in the {@link #prepareImageWriter(boolean)} method. Example:</p>
068 *
069 * <blockquote><pre>public class MyImageWriterTest extends ImageWriterTestCase {
070 *    &#64;Override
071 *    protected void prepareImageWriter(boolean optionallySetOutput) throws IOException {
072 *        if (writer == null) {
073 *            writer = new MyImageWriter();
074 *        }
075 *    }
076 *}</pre></blockquote>
077 *
078 * The {@linkplain #writer} shall accept at least one of the following
079 * {@linkplain ImageWriterSpi#getOutputTypes() output types}, in preference order:
080 *
081 * <ul>
082 *   <li>{@link ImageOutputStream} - mandatory according Image I/O specification.</li>
083 *   <li>{@link File} - fallback if the writer doesn't support {@code ImageOutputStream}.
084 *       This fallback exists because {@code ImageOutputStream} is hard to support when the
085 *       writer is implemented by a native library.</li>
086 * </ul>
087 *
088 * @author  Martin Desruisseaux (Geomatys)
089 * @version 3.1
090 * @since   3.1
091 */
092public abstract strictfp class ImageWriterTestCase extends ImageIOTestCase implements Closeable {
093    /**
094     * The prefix used for temporary files that may be created by this test case.
095     * Those files are created only if a writer can not write in an image output stream.
096     */
097    private static final String TEMPORARY_FILE_PREFIX = "geoapi";
098
099    /**
100     * The image writer to test. This field must be set by subclasses
101     * in the {@link #prepareImageWriter(boolean)} method.
102     */
103    protected ImageWriter writer;
104
105    /**
106     * The reader to use for verifying the writer output. By default, this field is {@code null}
107     * until a reader is first needed, in which case the field is assigned to a reader instance
108     * created by {@link ImageIO#getImageReader(ImageWriter)}. Subclasses can set explicitely a
109     * value to this field if they need the tests to use an other reader instead.
110     *
111     * <p>{@code ImageWriterTestCase} will use only the {@link ImageReader#read(int)} method.
112     * Consequently, this reader doesn't need to support {@code ImageReadParam} usage.</p>
113     */
114    protected ImageReader reader;
115
116    /**
117     * Creates a new test case using a default random number generator.
118     * The sub-regions, sub-samplings and source bands will be different
119     * for every test execution. If reproducible subsetting sequences are
120     * needed, use the {@link #ImageWriterTestCase(long)} constructor instead.
121     */
122    protected ImageWriterTestCase() {
123        super();
124    }
125
126    /**
127     * Creates a new test case using a random number generator initialized to the given seed.
128     *
129     * @param seed  the initial seed for the random numbers generator. Use a constant value if
130     *        the tests need to be reproduced with the same sequence of image write parameters.
131     */
132    protected ImageWriterTestCase(final long seed) {
133        super(seed);
134    }
135
136    /**
137     * Invoked when the image {@linkplain #writer} is about to be used for the first time.
138     * Subclasses need to create a new {@link ImageWriter} instance if needed.
139     *
140     * <p>If the {@code optionallySetOutput} argument is {@code true}, then subclasses can optionally
141     * {@linkplain ImageWriter#setOutput(Object) set the output} to a temporary file or other object
142     * suitable to the writer. This operation is optional: if no output has been explicitely set,
143     * {@code ImageWriterTestCase} will automatically set the output to an in-memory stream or to
144     * a temporary file.</p>
145     *
146     * <p><b>Example:</b></p>
147     * <blockquote><pre>&#64;Override
148     *protected void prepareImageWriter(boolean optionallySetOutput) throws IOException {
149     *    if (writer == null) {
150     *        writer = new MyImageWriter();
151     *    }
152     *    if (optionallySetOutput) {
153     *        writer.setOutput(<var>output</var>);                  // Optional operation.
154     *    }
155     *}</pre></blockquote>
156     *
157     * This method may be invoked with a {@code false} argument value when the methods to be
158     * tested do not need an output, for example {@link ImageWriter#canWriteRasters()}.
159     *
160     * @param  optionallySetOutput {@code true} if this method can {@linkplain ImageWriter#setOutput(Object)
161     *         set the writer output} (optional operation), or {@code false} if this is not yet necessary.
162     * @throws IOException if an error occurred while preparing the {@linkplain #writer}.
163     */
164    protected abstract void prepareImageWriter(boolean optionallySetOutput) throws IOException;
165
166    /**
167     * Completes stream or image metadata to be given to the tested {@linkplain #writer}.
168     * This method is invoked after the default metadata have been created, and before they
169     * are given to the tested image writer, as below:
170     *
171     * <p><b>For stream metadata:</b></p>
172     * <pre>IIOMetadata metadata = {@linkplain #writer}.{@linkplain ImageWriter#getDefaultStreamMetadata getDefaultStreamMetadata}(param);
173     *if (metadata != null) {
174     *    completeImageMetadata(metadata, null);
175     *}</pre>
176     *
177     * <p><b>For image metadata:</b></p>
178     * <pre>IIOMetadata metadata = {@linkplain #writer}.{@linkplain ImageWriter#getDefaultImageMetadata getDefaultImageMetadata}(ImageTypeSpecifier.{@linkplain ImageTypeSpecifier#createFromRenderedImage createFromRenderedImage}(image), param);
179     *if (metadata != null) {
180     *    completeImageMetadata(metadata, image);
181     *}</pre>
182     *
183     * The default implementation does nothing (note: this may change in a future version).
184     * Subclasses can override this method for providing custom metadata.
185     *
186     * @param  metadata  the stream or image metadata to complete before to be given to the tested image writer.
187     * @param  image     the image for which to create image metadata, or {@code null} for stream metadata.
188     * @throws IOException if the implementation needs to perform an I/O operation and that operation failed.
189     *
190     * @see ImageWriter#getDefaultStreamMetadata(ImageWriteParam)
191     * @see ImageWriter#getDefaultImageMetadata(ImageTypeSpecifier, ImageWriteParam)
192     */
193    protected void completeImageMetadata(final IIOMetadata metadata, final RenderedImage image) throws IOException {
194    }
195
196    /**
197     * Returns {@code true} if the given reader provider supports the given input type. If the
198     * given provider is {@code null}, then this method conservatively assumes that the type is
199     * supported on the assumption that the user provided an incomplete {@link ImageReader}
200     * implementation, but his reader input type is consistent with his writer output type.
201     *
202     * @see #closeAndRead(ByteArrayOutputStream)
203     */
204    private static boolean isSupportedInput(final ImageReaderSpi spi, final Class<?> type) {
205        if (spi == null) {
206            return true;
207        }
208        for (final Class<?> supportedType : spi.getInputTypes()) {
209            if (supportedType.isAssignableFrom(type)) {
210                return true;
211            }
212        }
213        return false;
214    }
215
216    /**
217     * Returns {@code true} if the given writer provider supports the given output type.
218     * If the given provider is {@code null}, then this method assumes that the standard
219     * {@link ImageOutputStream} type is expected as in the Image I/O specification.
220     *
221     * @see #open(int)
222     */
223    private static boolean isSupportedOutput(final ImageWriterSpi spi, final Class<?> type) {
224        if (spi == null) {
225            return ImageOutputStream.class.isAssignableFrom(type);
226        }
227        for (final Class<?> supportedType : spi.getOutputTypes()) {
228            if (supportedType.isAssignableFrom(type)) {
229                return true;
230            }
231        }
232        return false;
233    }
234
235    /**
236     * Returns {@code true} if the writer can writes the given image.
237     * If no writer provider is found, then this method assumes {@code true}.
238     *
239     * <p>This method also performs an opportunist validation of the image writer provider.</p>
240     */
241    private boolean canEncodeImage(final RenderedImage image) throws IOException {
242        prepareImageWriter(false);
243        if (writer != null) {
244            final ImageWriterSpi spi = writer.getOriginatingProvider();
245            validators.validate(spi);
246            if (spi != null) {
247                return spi.canEncodeImage(image);
248            }
249        }
250        return true;
251    }
252
253    /**
254     * Sets the image writer output to a temporary buffer or a temporary file. If the writer does
255     * not accept {@link ImageOutputStream} (which is illegal according Image I/O specification,
256     * but still happen with some formats like netCDF), then this method will try to set the output
257     * to a temporary file.
258     *
259     * @param  capacity  the initial capacity. This is an approximated value, since the actual capacity will growth as needed.
260     * @return the byte buffer, or {@code null} if this method created a temporary file instead.
261     * @throws IOException In an error occurred while setting the output.
262     */
263    private ByteArrayOutputStream open(final int capacity) throws IOException {
264        assertNotNull("The 'writer' field shall be set at construction time or in a method annotated by @Before.", writer);
265        if (writer.getOutput() != null) {
266            return null;                                // The output has been set by the user himself.
267        }
268        final ImageWriterSpi spi = writer.getOriginatingProvider();
269        if (isSupportedOutput(spi, OutputStream.class)) {
270            final ByteArrayOutputStream buffer = new ByteArrayOutputStream(capacity);
271            writer.setOutput(buffer);
272            return buffer;
273        } else if (isSupportedOutput(spi, ImageOutputStream.class)) {
274            final ByteArrayOutputStream buffer = new ByteArrayOutputStream(capacity);
275            writer.setOutput(ImageIO.createImageOutputStream(buffer));
276            return buffer;
277        } else if (isSupportedOutput(spi, File.class)) {
278            String suffix = null;
279            final String[] suffixes = spi.getFileSuffixes();
280            if (suffixes != null && suffixes.length != 0) {
281                suffix = suffixes[0];
282                if (!suffix.isEmpty() && suffix.charAt(0) != '.') {
283                    suffix = '.' + suffix;
284                }
285            }
286            final File file = File.createTempFile(TEMPORARY_FILE_PREFIX, suffix);
287            file.deleteOnExit();
288            writer.setOutput(file);
289            return null;
290        } else {
291            throw new IIOException("Unsupported output type.");
292        }
293    }
294
295    /**
296     * Closes the image writer output, then reads its content. This method is invoked after a test
297     * method has wrote an image with the {@linkplain #writer}.
298     *
299     * <p>This method will use only the {@link ImageReader#read(int)} method, so the reader doesn't
300     * need to support fully the {@code ImageReadParam}.</p>
301     *
302     * @param  buffer  the buffer returned by {@link #open(int)}, which may be {@code null}.
303     * @return the image.
304     * @throws IOException if an error occurred while closing the output or reading the image.
305     */
306    private RenderedImage closeAndRead(final ByteArrayOutputStream buffer) throws IOException {
307        Object input = writer.getOutput();
308        close(input);
309        writer.setOutput(null);
310        if (reader == null) {
311            reader = ImageIO.getImageReader(writer);
312            assertNotNull("The ImageWriter does not declare a compatible reader.", reader);
313        }
314        if (buffer != null) {
315            input = ImageIO.createImageInputStream(new ByteArrayInputStream(buffer.toByteArray()));
316        }
317        if (!isSupportedInput(reader.getOriginatingProvider(), input.getClass())) {
318            /*
319             * If the reader doesn't support the given input type, try to wrap it in an
320             * ImageInputStream - which is mandatory according Image I/O specification.
321             * If we can't wrap it, process anyway and let the reader throws the exception.
322             */
323            if (!(input instanceof ImageInputStream)) {
324                final ImageInputStream in = ImageIO.createImageInputStream(input);
325                if (in != null) {
326                    input = in;
327                }
328            }
329        }
330        reader.setInput(input);
331        try {
332            return reader.read(0);
333        } finally {
334            reader.setInput(null);
335            close(input);
336        }
337    }
338
339    /**
340     * Writes random subsets of the given image, reads back the image and compares the sample
341     * values. This method sets the {@link ImageWriteParam} parameters to random sub-regions,
342     * sub-samplings and source bands values and invokes {@link ImageWriter#write(IIOMetadata,
343     * IIOImage, ImageWriteParam)}.
344     *
345     * <p>The above method call is repeated {@code numIterations} time with different parameters.
346     * The kind of parameters to be tested is controlled by the {@code isXXXSupported} boolean
347     * fields in this class.</p>
348     *
349     * @param  image          the image to write.
350     * @param  numIterations  maximum number of iterations to perform.
351     * @throws IOException if an error occurred while writing the image or reading it back.
352     */
353    private void writeRandomSubsets(final RenderedImage image, final int numIterations) throws IOException {
354        for (int iterationCount=0; iterationCount<numIterations; iterationCount++) {
355            prepareImageWriter(true);       // Give a chance to subclasses to set their own output.
356            final ImageWriteParam param = writer.getDefaultWriteParam();
357            final PixelIterator expected = getIteratorOnRandomSubset(image, param);
358            final ByteArrayOutputStream buffer = open(1024);
359            final IIOMetadata streamMetadata = writer.getDefaultStreamMetadata(param);
360            if (streamMetadata != null) {
361                completeImageMetadata(streamMetadata, null);
362            }
363            final IIOMetadata imageMetadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), param);
364            if (imageMetadata != null) {
365                completeImageMetadata(imageMetadata, image);
366            }
367            writer.write(streamMetadata, new IIOImage(image, null, imageMetadata), param);
368            final RenderedImage actual = closeAndRead(buffer);
369            expected.assertSampleValuesEqual(new PixelIteratorForIO(actual, param), sampleToleranceThreshold);
370        }
371    }
372
373    /**
374     * Implementation of the {@code testFooWrite()} methods.
375     *
376     * @throws IOException if an error occurred while writing the image or reading it back.
377     */
378    private void testImageWrites(final RenderedImage image) throws IOException {
379        final boolean subregion   = isSubregionSupported;
380        final boolean subsampling = isSubsamplingSupported;
381        final boolean offset      = isSubsamplingOffsetSupported;
382        final boolean bands       = isSourceBandsSupported;
383        final boolean actualBands = bands && image.getSampleModel().getNumBands() > 1;
384        /*
385         * Writes the complete image.
386         */
387        isSubregionSupported         = false;
388        isSubsamplingSupported       = false;
389        isSubsamplingOffsetSupported = false;
390        isSourceBandsSupported       = false;
391        writeRandomSubsets(image, 1);
392        /*
393         * Tests writing sub-regions only (no subsampling).
394         */
395        if (subregion) {
396            isSubregionSupported = true;
397            writeRandomSubsets(image, DEFAULT_NUM_ITERATIONS);
398            isSubregionSupported = false;
399        }
400        /*
401         * Tests writing the complete region with various subsamplings.
402         */
403        if (subsampling) {
404            isSubsamplingSupported = true;
405            writeRandomSubsets(image, DEFAULT_NUM_ITERATIONS);
406            isSubsamplingSupported = false;
407        }
408        /*
409         * Tests writing the complete image with different source bands.
410         */
411        if (actualBands) {
412            isSourceBandsSupported = true;
413            writeRandomSubsets(image, DEFAULT_NUM_ITERATIONS);
414            isSourceBandsSupported = false;
415        }
416        /*
417         * Mixes all.
418         */
419        isSubregionSupported         = subregion;
420        isSubsamplingSupported       = subsampling;
421        isSubsamplingOffsetSupported = offset;
422        isSourceBandsSupported       = bands;
423        if (subregion | subsampling | offset | actualBands) {
424            writeRandomSubsets(image, DEFAULT_NUM_ITERATIONS);
425        }
426    }
427
428    /**
429     * Tests the {@link ImageWriter#write(IIOMetadata, IIOImage, ImageWriteParam) ImageWriter.write}
430     * method for a single band of byte values. First, this method creates an single-banded image
431     * filled with random byte values. Then, this method invokes write the image an arbitrary amount
432     * of time for the following configurations (note: any {@code isXXXSupported} field
433     * which was set to {@code false} prior the execution of this test will stay {@code false}):
434     *
435     * <ul>
436     *   <li>Writes the full image once (all {@code isXXXSupported} fields set to {@code false}).</li>
437     *   <li>Writes various sub-regions (only {@link #isSubregionSupported isSubregionSupported} may be {@code true})</li>
438     *   <li>Writes at various sub-sampling (only {@link #isSubsamplingSupported isSubsamplingSupported} may be {@code true})</li>
439     *   <li>Reads various bands (only {@link #isSourceBandsSupported isSourceBandsSupported} may be {@code true})</li>
440     *   <li>A mix of sub-regions, sub-sampling and source bands</li>
441     * </ul>
442     *
443     * Then the image is read again and the pixel values are compared with the corresponding
444     * pixel values of the original image.
445     *
446     * @throws IOException if an error occurred while writing the image or or reading it back.
447     */
448    @Test
449    public void testOneByteBand() throws IOException {
450        final BufferedImage image = createImage(DataBuffer.TYPE_BYTE, 180, 90, 1);
451        fill(image.getRaster(), random);
452        assumeTrue(canEncodeImage(image));
453        testImageWrites(image);
454    }
455
456    /**
457     * Same test than {@link #testOneByteBand()}, but using RGB values in three bands.
458     *
459     * @throws IOException if an error occurred while writing the image or or reading it back.
460     */
461    @Test
462    public void testThreeByteBands() throws IOException {
463        final BufferedImage image = createImage(DataBuffer.TYPE_BYTE, 180, 90, 3);
464        fill(image.getRaster(), random);
465        assumeTrue(canEncodeImage(image));
466        testImageWrites(image);
467    }
468
469    /**
470     * Same test than {@link #testOneByteBand()}, but using the signed {@code short} type.
471     *
472     * @throws IOException if an error occurred while writing the image or or reading it back.
473     */
474    @Test
475    public void testOneShortBand() throws IOException {
476        final BufferedImage image = createImage(DataBuffer.TYPE_SHORT, 180, 90, 1);
477        fill(image.getRaster(), random);
478        assumeTrue(canEncodeImage(image));
479        testImageWrites(image);
480    }
481
482    /**
483     * Same test than {@link #testOneByteBand()}, but using the unsigned {@code short} type.
484     *
485     * @throws IOException if an error occurred while writing the image or or reading it back.
486     */
487    @Test
488    public void testOneUnsignedShortBand() throws IOException {
489        final BufferedImage image = createImage(DataBuffer.TYPE_USHORT, 180, 90, 1);
490        fill(image.getRaster(), random);
491        assumeTrue(canEncodeImage(image));
492        testImageWrites(image);
493    }
494
495    /**
496     * Same test than {@link #testOneByteBand()}, but using the signed {@code int} type.
497     *
498     * @throws IOException if an error occurred while writing the image or or reading it back.
499     */
500    @Test
501    public void testOneIntBand() throws IOException {
502        final BufferedImage image = createImage(DataBuffer.TYPE_INT, 180, 90, 1);
503        fill(image.getRaster(), random);
504        assumeTrue(canEncodeImage(image));
505        testImageWrites(image);
506    }
507
508    /**
509     * Same test than {@link #testOneByteBand()}, but using the signed {@code float} type.
510     *
511     * @throws IOException if an error occurred while writing the image or or reading it back.
512     */
513    @Test
514    public void testOneFloatBand() throws IOException {
515        final BufferedImage image = createImage(DataBuffer.TYPE_FLOAT, 180, 90, 1);
516        fill(image.getRaster(), random);
517        assumeTrue(canEncodeImage(image));
518        testImageWrites(image);
519    }
520
521    /**
522     * Same test than {@link #testOneByteBand()}, but using the signed {@code double} type.
523     *
524     * @throws IOException if an error occurred while writing the image or or reading it back.
525     */
526    @Test
527    public void testOneDoubleBand() throws IOException {
528        final BufferedImage image = createImage(DataBuffer.TYPE_DOUBLE, 180, 90, 1);
529        fill(image.getRaster(), random);
530        assumeTrue(canEncodeImage(image));
531        testImageWrites(image);
532    }
533
534    /**
535     * Disposes the {@linkplain #reader} and the {@linkplain #writer} (if non-null) after each test.
536     * The default implementation performs the following cleanup:
537     *
538     * <ul>
539     *   <li>If the {@linkplain ImageWriter#getOutput() writer output} is {@linkplain Closeable closeable}, closes it.</li>
540     *   <li>Invokes {@link ImageWriter#reset()} for clearing the output and listeners.</li>
541     *   <li>Invokes {@link ImageWriter#dispose()} for performing additional resource disposal, if any.</li>
542     *   <li>Sets the {@link #writer} field to {@code null} for preventing accidental use.</li>
543     *   <li>Performs the same steps than above for the {@linkplain #reader}, if non-null.</li>
544     * </ul>
545     *
546     * @throws IOException if an error occurred while closing the output stream.
547     *
548     * @see ImageWriter#reset()
549     * @see ImageWriter#dispose()
550     */
551    @After
552    @Override
553    public void close() throws IOException {
554        if (writer != null) {
555            close(writer.getOutput());
556            writer.reset();
557            writer.dispose();
558            writer = null;
559        }
560        if (reader != null) {
561            close(reader.getInput());
562            reader.reset();
563            reader.dispose();
564            reader = null;
565        }
566    }
567}