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