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