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 * @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>@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}