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}