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.Closeable;
035import java.io.IOException;
036import java.awt.image.RenderedImage;
037import javax.imageio.ImageReader;
038import javax.imageio.ImageReadParam;
039import javax.imageio.metadata.IIOMetadata;
040import javax.imageio.metadata.IIOMetadataNode;
041import javax.imageio.metadata.IIOMetadataFormatImpl;
042import org.w3c.dom.NodeList;
043import org.w3c.dom.Node;
044
045import org.opengis.coverage.grid.Grid;
046import org.opengis.coverage.grid.GridEnvelope;
047import org.opengis.metadata.Metadata;
048import org.opengis.metadata.extent.Extent;
049import org.opengis.metadata.identification.Identification;
050import org.opengis.metadata.identification.DataIdentification;
051
052import org.junit.*;
053import static org.junit.Assert.*;
054import static org.junit.Assume.*;
055
056
057/**
058 * Base class for testing {@link ImageReader} implementations. This test reads different regions and
059 * bands of an image at different sub-sampling levels, and compares the results with the complete image.
060 * If the image reader can also provide GeoAPI metadata objects, then this class will verify
061 * the consistency of some basic attributes. For example it will verify that the {@linkplain
062 * GridEnvelope grid envelope} (if any) is consistent with the image
063 * {@linkplain ImageReader#getWidth(int) width} and {@linkplain ImageReader#getHeight(int) height}.
064 *
065 * <p>To use this test, subclasses need to set the {@link #reader} field to a non-null value in the
066 * {@link #prepareImageReader(boolean)} method. The {@linkplain ImageReader#getInput() reader input}
067 * shall be set by the subclass when requested by the caller. Example:</p>
068 *
069 * <blockquote><pre>public class MyImageReaderTest extends ImageReaderTestCase {
070 *    &#64;Override
071 *    protected void prepareImageReader(boolean setInput) throws IOException {
072 *        if (reader == null) {
073 *            reader = new MyImageReader();
074 *        }
075 *        if (setInput) {
076 *            reader.setInput(ImageIO.createImageInputStream(new File("MyTestImage")));
077 *        }
078 *    }
079 *}</pre></blockquote>
080 *
081 * <p>Subclasses inherit the following tests:</p>
082 * <table class="ogc" summary="Inherited tests">
083 *   <tr><th>Inherited method</th>                   <th>Tested method</th></tr>
084 *   <tr><td>{@link #testStreamMetadata()}</td>      <td>{@link ImageReader#getStreamMetadata()}</td></tr>
085 *   <tr><td>{@link #testImageMetadata()}</td>       <td>{@link ImageReader#getImageMetadata(int)}</td></tr>
086 *   <tr><td>{@link #testReadAsBufferedImage()}</td> <td>{@link ImageReader#read(int, ImageReadParam)}</td></tr>
087 *   <tr><td>{@link #testReadAsRenderedImage()}</td> <td>{@link ImageReader#readAsRenderedImage(int, ImageReadParam)}</td></tr>
088 *   <tr><td>{@link #testReadAsRaster()}</td>        <td>{@link ImageReader#readRaster(int, ImageReadParam)}</td></tr>
089 * </table>
090 *
091 * <p>In addition, subclasses may consider to override the following methods:</p>
092 * <ul>
093 *   <li>{@link #getMetadata(Class, IIOMetadata)} - to extract metadata objects from specific nodes.</li>
094 *   <li>{@link #close()} - to modify the policy of {@linkplain #reader} disposal.</li>
095 * </ul>
096 *
097 * @author  Martin Desruisseaux (Geomatys)
098 * @version 3.1
099 * @since   3.1
100 */
101public abstract strictfp class ImageReaderTestCase extends ImageIOTestCase implements Closeable {
102    /**
103     * The {@link ImageReader} API to use for testing read operations.
104     *
105     * @author  Martin Desruisseaux (Geomatys)
106     * @version 3.1
107     * @since   3.1
108     */
109    private static enum API {
110        /**
111         * Use the {@link ImageReader#read(int, ImageReadParam)} method.
112         */
113        READ,
114
115        /**
116         * Use the {@link ImageReader#readAsRenderedImage(int, ImageReadParam)} method.
117         */
118        READ_AS_RENDERED_IMAGE,
119
120        /**
121         * Use the {@link ImageReader#readRaster(int, ImageReadParam)} method.
122         */
123        READ_RASTER
124    }
125
126    /**
127     * The image reader to test. This field must be set by subclasses
128     * in the {@link #prepareImageReader(boolean)} method.
129     */
130    protected ImageReader reader;
131
132    /**
133     * Creates a new test case using a default random number generator.
134     * The sub-regions, sub-samplings and source bands will be different
135     * for every test execution. If reproducible subsetting sequences are
136     * needed, use the {@link #ImageReaderTestCase(long)} constructor instead.
137     */
138    protected ImageReaderTestCase() {
139        super();
140    }
141
142    /**
143     * Creates a new test case using a random number generator initialized to the given seed.
144     *
145     * @param seed  the initial seed for the random numbers generator. Use a constant value if
146     *        the tests need to be reproduced with the same sequence of image read parameters.
147     */
148    protected ImageReaderTestCase(final long seed) {
149        super(seed);
150    }
151
152    /**
153     * Asserts that the {@linkplain ImageReader#getInput() reader input} is set to a non-null value.
154     */
155    private static void assertInputSet(final ImageReader reader) {
156        assertNotNull("The 'reader' field shall be set in the 'prepareImageReader' method.", reader);
157        assertNotNull("reader.setInput(Object) shall be invoked before any test is run.", reader.getInput());
158    }
159
160    /**
161     * Invokes {@link #prepareImageReader(boolean)} with a value of {@code true}, ensures that
162     * the input is set, then validate the provider.
163     *
164     * @throws IOException if an error occurred while preparing the {@linkplain #reader}.
165     */
166    private void prepareImageReader() throws IOException {
167        prepareImageReader(true);
168        assertInputSet(reader);
169        validators.validate(reader.getOriginatingProvider());
170    }
171
172    /**
173     * Invoked when the image {@linkplain #reader} is about to be used for the first time, or when
174     * its {@linkplain ImageReader#getInput() input} needs to be reinitialized. Subclasses need to
175     * create a new {@link ImageReader} instance if needed and set its input in this method.
176     *
177     * <p>This method may be invoked more than once during the same test if the input became invalid.
178     * This may occur because the tests will read the same image many time in different ways, and not
179     * all input streams can seek back to the beginning of the image stream. In such case,
180     * {@code ImageReaderTestCase} will {@linkplain java.io.Closeable#close() close} the input and
181     * invoke this method in order to get a fresh {@link javax.imageio.stream.ImageInputStream}.</p>
182     *
183     * <p><b>Example:</b></p>
184     * <blockquote><pre>&#64;Override
185     *protected void prepareImageReader(boolean setInput) throws IOException {
186     *    if (reader == null) {
187     *        reader = new MyImageReader();
188     *    }
189     *    if (setInput) {
190     *        reader.setInput(ImageIO.createImageInputStream(new File("MyTestImage")));
191     *    }
192     *}</pre></blockquote>
193     *
194     * This method may be invoked with a {@code false} argument value when the methods to be
195     * tested don't need an input, for example {@link ImageReader#canReadRaster()}.
196     *
197     * @param  setInput {@code true} if this method shall {@linkplain ImageReader#setInput(Object)
198     *         set the reader input}, or {@code false} if this is not yet necessary.
199     * @throws IOException if an error occurred while preparing the {@linkplain #reader}.
200     */
201    protected abstract void prepareImageReader(boolean setInput) throws IOException;
202
203    /**
204     * Returns the user object of the given type found in the given Image I/O metadata, or
205     * {@code null} if none. The default implementation {@linkplain IIOMetadata#getAsTree(String)
206     * gets the tree of nodes} for all {@linkplain IIOMetadata#getMetadataFormatNames() metadata
207     * formats} except the {@linkplain IIOMetadataFormatImpl#standardMetadataFormatName standard
208     * format}, then iterates down the tree in search for a {@linkplain IIOMetadataNode#getUserObject()
209     * user object} of the given type. If an ambiguity is found, this method conservatively returns
210     * {@code true}.
211     *
212     * <p>Implementors are encouraged to override this method if they can look for metadata in a
213     * more accurate way.</p>
214     *
215     * <p>See {@link #testStreamMetadata()} and {@link #testImageMetadata()} for a list of types
216     * requested by the default {@code ImageReaderTestCase} implementation.</p>
217     *
218     * @param  <T>       the compile-time type of the object to search.
219     * @param  type      the type of the object to search.
220     * @param  metadata  the metadata where to search for the object.
221     * @return the user object of the given type, or {@code null} if not found.
222     * @throws IOException if this method requires an I/O operation and that operation failed.
223     */
224    protected <T> T getMetadata(final Class<T> type, final IIOMetadata metadata) throws IOException {
225        T found = null;
226        for (final String format : metadata.getMetadataFormatNames()) {
227            if (!format.equals(IIOMetadataFormatImpl.standardMetadataFormatName)) {
228                final T userObject = getMetadata(type, metadata.getAsTree(format));
229                if (userObject != null) {
230                    if (found == null) {
231                        found = userObject;
232                    } else if (!found.equals(userObject)) {
233                        return null;
234                    }
235                }
236            }
237        }
238        return found;
239    }
240
241    /**
242     * Returns the user object of the given type in the given node, or in one of its children.
243     * If the user object is found in the given node, we will returns it directly. If we have
244     * to scan the children, we will return it only if we find only one object, in order to
245     * avoid ambiguity.
246     *
247     * @param  <T>   the compile-time type of the object to search.
248     * @param  type  the type of the object to search.
249     * @param  node  the node where to search for the object, or {@code null} if none.
250     * @return the user object of the given type, or {@code null} if not found.
251     */
252    private static <T> T getMetadata(final Class<T> type, final Node node) {
253        if (node == null) { // Because IIOMetadata.getAsTree(String) may return null.
254            return null;
255        }
256        if (node instanceof IIOMetadataNode) {
257            final Object userObject = ((IIOMetadataNode) node).getUserObject();
258            if (type.isInstance(userObject)) {
259                return type.cast(userObject);
260            }
261        }
262        T found = null;
263        final NodeList childs = node.getChildNodes();
264        final int length = childs.getLength();
265        for (int i=0; i<length; i++) {
266            final T userObject = getMetadata(type, childs.item(i));
267            if (userObject != null) {
268                if (found == null) {
269                    found = userObject;
270                } else if (!found.equals(userObject)) {
271                    return null;
272                }
273            }
274        }
275        return found;
276    }
277
278    /**
279     * Verifies the validity of metadata attributes as documented in the
280     * {@link #testStreamMetadata()} and {@link #testImageMetadata()} methods.
281     */
282    private void validate(final IIOMetadata metadata) throws IOException {
283        final Metadata md = getMetadata(Metadata.class, metadata);
284        if (md != null) {
285            for (final Identification identification : md.getIdentificationInfo()) {
286                validators.validate(identification.getCitation());
287                for (final Extent extent : identification.getExtents()) {
288                    validators.validate(extent);
289                }
290            }
291        }
292    }
293
294    /**
295     * Tests {@link ImageReader#getStreamMetadata()}. The default implementation invokes
296     * <code>{@linkplain #getMetadata(Class, IIOMetadata) getMetadata}({@linkplain Metadata}.class,</code>
297     * <var>stream metadata</var><code>)</code>, then validates the following properties:
298     *
299     * <ul>
300     *   <li>{@link Metadata#getIdentificationInfo()}<ul>
301     *     <li>{@link DataIdentification#getCitation()}</li>
302     *     <li>{@link DataIdentification#getExtents()}</li>
303     *   </ul></li>
304     * </ul>
305     *
306     * @throws IOException if an error occurred while reading the metadata.
307     */
308    @Test
309    public void testStreamMetadata() throws IOException {
310        prepareImageReader();
311        final IIOMetadata metadata = reader.getStreamMetadata();
312        if (metadata != null) {
313            validate(metadata);
314        }
315    }
316
317    /**
318     * Tests {@link ImageReader#getImageMetadata(int)}. The default implementation invokes
319     * {@link #getMetadata(Class, IIOMetadata)} for the types listed below, then validates
320     * their properties:
321     *
322     * <ul>
323     *   <li>{@link Metadata#getIdentificationInfo()}: see {@link #testStreamMetadata()}</li>
324     *   <li>{@link Extent}:
325     *       {@linkplain org.opengis.test.metadata.ExtentValidator#validate(Extent) Validate}
326     *       the spatial extent, if any.</li>
327     *   <li>{@link Grid#getExtent()}<ul>
328     *     <li>{@link GridEnvelope}: Verify that the {@linkplain GridEnvelope#getSize(int) span}
329     *         in at least one dimension is equals to the {@linkplain ImageReader#getWidth(int)
330     *         image width}, and a span in another dimension is equals to the
331     *         {@linkplain ImageReader#getHeight(int) image height}.</li>
332     *   </ul></li>
333     * </ul>
334     *
335     * @throws IOException if an error occurred while reading the metadata.
336     */
337    @Test
338    public void testImageMetadata() throws IOException {
339        prepareImageReader();
340        final int numImages = reader.getNumImages(true);
341        for (int imageIndex=0; imageIndex<numImages; imageIndex++) {
342            final IIOMetadata metadata = reader.getImageMetadata(imageIndex);
343            if (metadata != null) {
344                validate(metadata);
345                final GridEnvelope extent;
346                final Grid grid = getMetadata(Grid.class, metadata);
347                if (grid != null) {
348                    extent = grid.getExtent();
349                } else {
350                    extent = getMetadata(GridEnvelope.class, metadata);
351                }
352                if (extent != null) {
353                    final int width  = reader.getWidth (imageIndex);
354                    final int height = reader.getHeight(imageIndex);
355                    boolean foundWidth = false, foundHeight = false;
356                    final int dimension = extent.getDimension();
357                    for (int i=0; i<dimension; i++) {
358                        final long span = extent.getSize(i);
359                        if (span == width) {
360                            foundWidth = true;
361                        } else if (span == height) {
362                            foundHeight = true;
363                        }
364                    }
365                    if (!foundWidth || !foundHeight) {
366                        fail("Expected an image of size " + width + '×' + height +
367                                " but found a grid extent of " + extent);
368                    }
369                }
370            }
371        }
372    }
373
374    /**
375     * Reads random subsets of the image at the given index, and compares the result with the
376     * given complete image. This method sets the {@link ImageReadParam} parameters to random
377     * sub-regions, sub-samplings and source bands values and invokes one of the following
378     * methods as determined by the {@code api} argument:
379     *
380     * <ul>
381     *   <li><code>{@link ImageReader#read(int, ImageReadParam)}</code></li>
382     *   <li><code>{@link ImageReader#readAsRenderedImage(int, ImageReadParam)}</code></li>
383     *   <li><code>{@link ImageReader#readRaster(int, ImageReadParam)}</code></li>
384     * </ul>
385     *
386     * The above method call is repeated {@code numIterations} time with different parameters.
387     * The kind of parameters to be tested is controlled by the {@code isXXXSupported} boolean
388     * fields in this class.
389     *
390     * <p>The pixel values for each image resulting from the above read operations are
391     * compared with the corresponding pixel values of the given complete image.</p>
392     *
393     * @param  completeImage  the complete image as returned by <code>{@linkplain #reader}.{@link ImageReader#read(int) read}(imageIndex)</code> without read parameters.
394     * @param  api            the API to use for reading the images.
395     * @param  imageIndex     index of the images to read.
396     * @param  numIterations  maximum number of iterations to perform.
397     * @throws IOException if an error occurred while reading the image.
398     */
399    private void readRandomSubsets(final RenderedImage completeImage, final API api,
400            final int imageIndex, final int numIterations) throws IOException
401    {
402        final ImageReader reader = this.reader;                                         // Protect from changes.
403        assertInputSet(reader);
404        for (int iterationCount=0; iterationCount<numIterations; iterationCount++) {
405            if (reader.getMinIndex() > imageIndex) {
406                close(reader.getInput());
407                reader.setInput(null);
408                prepareImageReader(true);
409            }
410            final ImageReadParam param = reader.getDefaultReadParam();
411            final PixelIterator expected = getIteratorOnRandomSubset(completeImage, param);
412            final RenderedImage image;
413            switch (api) {
414                case READ: {
415                    image = reader.read(imageIndex, param);
416                    break;
417                }
418                case READ_AS_RENDERED_IMAGE: {
419                    image = reader.readAsRenderedImage(imageIndex, param);
420                    break;
421                }
422                case READ_RASTER: {
423                    image = new RasterImage(reader.readRaster(imageIndex, param));
424                    break;
425                }
426                default: throw new IllegalArgumentException(api.toString());
427            }
428            expected.assertSampleValuesEqual(new PixelIteratorForIO(image, param), sampleToleranceThreshold);
429        }
430    }
431
432    /**
433     * Tests the {@link ImageReader#read(int, ImageReadParam) ImageReader.read} method.
434     * First, this method reads the full image with a call to {@link ImageReader#read(int)}.
435     * Then, this method invokes {@link ImageReader#read(int, ImageReadParam)} an arbitrary
436     * amount of time for the following configurations (note: any {@code isXXXSupported} field
437     * which was set to {@code false} prior the execution of this test will stay {@code false}):
438     *
439     * <ul>
440     *   <li>Reads the full image once (all {@code isXXXSupported} fields set to {@code false}).</li>
441     *   <li>Reads various sub-regions (only {@link #isSubregionSupported isSubregionSupported} may be {@code true})</li>
442     *   <li>Reads at various sub-sampling (only {@link #isSubsamplingSupported isSubsamplingSupported} may be {@code true})</li>
443     *   <li>Reads various bands (only {@link #isSourceBandsSupported isSourceBandsSupported} may be {@code true})</li>
444     *   <li>A mix of sub-regions, sub-sampling and source bands</li>
445     * </ul>
446     *
447     * The pixel values for each image resulting from the above read operations are
448     * compared with the corresponding pixel values of the complete image.
449     *
450     * <div class="note"><b>Note:</b>
451     * in the spirit of JUnit, this test should have been splitted in smaller test cases:
452     * one for sub-regions, one for sub-samplings, <i>etc</i>. However in this particular case,
453     * consolidation of those tests in a single method provides the following benefits:
454     * <ul>
455     *   <li>The potentially large complete image is read only once.</li>
456     *   <li>If the tests for subregions or subsamplings fails, avoid the test mixing both.</li>
457     *   <li>Less methods to override if the implementor want to provide his own test.</li>
458     * </ul>
459     * </div>
460     *
461     * @throws IOException if an error occurred while reading the image.
462     */
463    @Test
464    public void testReadAsBufferedImage() throws IOException {
465        testImageReads(API.READ);
466    }
467
468    /**
469     * Tests the {@link ImageReader#readAsRenderedImage(int, ImageReadParam) ImageReader.readAsRenderedImage} method.
470     * This method performs the same test than {@link #testReadAsBufferedImage()}, except that the
471     * {@link ImageReader#readAsRenderedImage(int, ImageReadParam)} method is invoked instead than
472     * {@code ImageReader.read(int, ImageReadParam)}.
473     *
474     * @throws IOException if an error occurred while reading the image.
475     */
476    @Test
477    public void testReadAsRenderedImage() throws IOException {
478        testImageReads(API.READ_AS_RENDERED_IMAGE);
479    }
480
481    /**
482     * Tests the {@link ImageReader#readRaster(int, ImageReadParam) ImageReader.readRaster} method.
483     * This method performs the same test than {@link #testReadAsBufferedImage()}, except that the
484     * {@link ImageReader#readRaster(int, ImageReadParam)} method is invoked instead than
485     * {@code ImageReader.read(int, ImageReadParam)}.
486     *
487     * <p>This test is ignored if {@link ImageReader#canReadRaster()} returns {@code false}.</p>
488     *
489     * @throws IOException if an error occurred while reading the raster.
490     */
491    @Test
492    public void testReadAsRaster() throws IOException {
493        prepareImageReader(false);
494        assumeTrue(reader.canReadRaster());
495        testImageReads(API.READ_RASTER);
496    }
497
498    /**
499     * Implementation of the {@link #testReadAsBufferedImage()}, {@link #testReadAsRenderedImage()}
500     * and {@link #testReadAsRaster()} methods.
501     *
502     * @param  api  the API to use for reading images.
503     * @throws IOException if an error occurred while reading the image.
504     */
505    private void testImageReads(final API api) throws IOException {
506        prepareImageReader();
507        final boolean subregion   = isSubregionSupported;
508        final boolean subsampling = isSubsamplingSupported;
509        final boolean offset      = isSubsamplingOffsetSupported;
510        final boolean bands       = isSourceBandsSupported;
511        final int numImages = reader.getNumImages(true);
512        for (int imageIndex=0; imageIndex<numImages; imageIndex++) {
513            final RenderedImage completeImage = reader.read(imageIndex);
514            final boolean actualBands = bands && completeImage.getSampleModel().getNumBands() > 1;
515            /*
516             * Reads the complete image again.
517             */
518            isSubregionSupported         = false;
519            isSubsamplingSupported       = false;
520            isSubsamplingOffsetSupported = false;
521            isSourceBandsSupported       = false;
522            readRandomSubsets(completeImage, api, imageIndex, 1);
523            /*
524             * Tests reading sub-regions only (no subsampling).
525             */
526            if (subregion) {
527                isSubregionSupported = true;
528                readRandomSubsets(completeImage, api, imageIndex, DEFAULT_NUM_ITERATIONS);
529                isSubregionSupported = false;
530            }
531            /*
532             * Tests reading the complete region with various subsamplings.
533             */
534            if (subsampling) {
535                isSubsamplingSupported = true;
536                readRandomSubsets(completeImage, api, imageIndex, DEFAULT_NUM_ITERATIONS);
537                isSubsamplingSupported = false;
538            }
539            /*
540             * Tests reading the complete image with different source bands.
541             */
542            if (actualBands) {
543                isSourceBandsSupported = true;
544                readRandomSubsets(completeImage, api, imageIndex, DEFAULT_NUM_ITERATIONS/2);
545                isSourceBandsSupported = false;
546            }
547            /*
548             * Mixes all.
549             */
550            isSubregionSupported         = subregion;
551            isSubsamplingSupported       = subsampling;
552            isSubsamplingOffsetSupported = offset;
553            isSourceBandsSupported       = bands;
554            if (subregion | subsampling | offset | actualBands) {
555                readRandomSubsets(completeImage, api, imageIndex, DEFAULT_NUM_ITERATIONS*2);
556            }
557        }
558    }
559
560    /**
561     * Disposes the {@linkplain #reader} (if non-null) after each test.
562     * The default implementation performs the following cleanup:
563     *
564     * <ul>
565     *   <li>If the {@linkplain ImageReader#getInput() reader input} is {@linkplain Closeable closeable}, closes it.</li>
566     *   <li>Invokes {@link ImageReader#reset()} for clearing the input and listeners.</li>
567     *   <li>Invokes {@link ImageReader#dispose()} for performing additional resource disposal, if any.</li>
568     *   <li>Sets the {@link #reader} field to {@code null} for preventing accidental use.</li>
569     * </ul>
570     *
571     * @throws IOException if an error occurred while closing the input stream.
572     *
573     * @see ImageReader#reset()
574     * @see ImageReader#dispose()
575     */
576    @After
577    @Override
578    public void close() throws IOException {
579        if (reader != null) {
580            close(reader.getInput());
581            reader.reset();
582            reader.dispose();
583            reader = null;
584        }
585    }
586}