/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.test.image; import java.awt.EventQueue; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.awt.image.ColorModel; import java.awt.image.ImagingOpException; import java.lang.reflect.InvocationTargetException; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import javax.imageio.ImageIO; import javax.imageio.IIOException; import org.opengis.coverage.Coverage; import org.geotoolkit.test.Commons; import org.geotoolkit.test.TestBase; import org.geotoolkit.test.gui.SwingTestBase; import org.junit.AfterClass; import static org.junit.Assume.*; import static org.junit.Assert.*; import static java.lang.StrictMath.*; /** * Base class for tests applied on images. This base class provides a {@link #viewEnabled} * field initialized to {@code false}. If this field is set to {@code true}, then calls to * the {@link #view(String)} method will show the {@linkplain #image}. * * @author Martin Desruisseaux (Geomatys) * @version 3.19 * * @since 3.16 (derived from 3.00) */ public abstract strictfp class ImageTestBase extends TestBase { /** * Small value for comparison of sample values. Since most grid coverage implementations in * Geotk 2 store geophysics values as {@code float} numbers, this {@code SAMPLE_TOLERANCE} * value must be of the order of {@code float} relative precision, not {@code double}. */ public static final float SAMPLE_TOLERANCE = 1E-5f; /** * Invokes {@link org.geotoolkit.image.jai.Registry#setDefaultCodecPreferences()} * in order to improve consistency between different execution of test suites. */ static { try { Class.forName("org.geotoolkit.image.jai.Registry") .getMethod("setDefaultCodecPreferences", (Class<?>[]) null) .invoke(null, (Object[]) null); } catch (ReflectiveOperationException e) { System.err.println(e); } } /** * The image being tested. */ protected RenderedImage image; /** * Set to {@code true} for enabling the display of test images. * The default value is determined by: * * {@preformat java * Boolean.getBoolean(SHOW_PROPERTY_KEY); * } * * @see SwingTestBase#SHOW_PROPERTY_KEY */ protected boolean viewEnabled; /** * The image viewer, which can be created only if {@link #viewEnabled} is {@code true}. */ private static volatile Viewer viewer; /** * Creates a new test suite for the given class. * * @param testing The class to be tested. */ protected ImageTestBase(final Class<?> testing) { assertTrue(testing.desiredAssertionStatus()); viewEnabled = Boolean.getBoolean(SwingTestBase.SHOW_PROPERTY_KEY); } /** * Returns the file of the given name in the {@code "Geotoolkit.org/Tests"} directory. * This directory contains data too big for inclusion in the source code repository. * The file is tested for existence using: * * {@code java * assumeTrue(file.canRead()); * } * * Consequently if the file can not be read (typically because the users did not installed * those data on its local directory), then the tests after the call to this method are * completely skipped. * * @param filename The name of the file to get, or {@code null}. * @return The name of directory of the given name in the {@code "Geotoolkit.org/Tests"} * directory (never {@code null}). * * @since 3.19 */ public static File getLocallyInstalledFile(final String filename) { Path file; try { final Class<?> c = Class.forName("org.geotoolkit.internal.io.Installation"); file = (Path) c.getMethod("directory", Boolean.TYPE).invoke(c.getField("TESTS").get(null), Boolean.TRUE); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } if (filename != null) { file = file.resolve(filename); } assumeTrue(Files.isReadable(file)); return file.toFile(); } /** * Asserts that the {@linkplain #image} checksum is equals to one of the specified values. * * @param name The name of the image being tested, or {@code null} if none. * @param expected The expected checksum value. */ protected final synchronized void assertCurrentChecksumEquals(final String name, final long... expected) { final long c = Commons.checksum(image); for (final long e : expected) { if (e == c) return; } final StringBuilder buffer = new StringBuilder("Unexpected image checksum"); if (name != null) { buffer.append(" for \"").append(name).append('"'); } fail(buffer.append(": ").append(c).toString()); } /** * Returns a copy of the current image. * * @return A copy of the current image. */ protected final synchronized BufferedImage copyCurrentImage() { assertNotNull("No image currently defined.", image); final ColorModel cm = image.getColorModel(); return new BufferedImage(cm, image.copyData(null), cm.isAlphaPremultiplied(), null); } /** * Saves the current image as a PNG image in the given file. This is sometime useful for visual * check purpose, and is used only as a helper tools for tuning the test suites. Floating-point * images are converted to grayscale before to be saved. * * @param filename The name (optionally with its path) of the file to create. * @throws ImagingOpException If an error occurred while writing the file. * * @since 3.19 */ protected final synchronized void saveCurrentImage(final String filename) throws ImagingOpException { try { savePNG(image, new File(filename)); } catch (IOException e) { throw new ImagingOpException(e.toString()); } } /** * Implementation of {@link #saveCurrentImage(String)}, to be shared by the widget * shown by {@link #showCurrentImage(String)}. */ static void savePNG(final RenderedImage image, final File file) throws IOException { assertNotNull("An image must be set.", image); if (!ImageIO.write(image, "png", file)) { savePNG(image.getData(), file); } } /** * Saves the first band of the given raster as a PNG image in the given file. * This is sometime useful for visual check purpose, and is used only as a helper * tools for tuning the test suites. The image is converted to grayscale before to * be saved. * * @param raster The raster to write in PNG format. * @param file The file to create. * @throws IOException If an error occurred while writing the file. */ private static void savePNG(final Raster raster, final File file) throws IOException { float min = Float.POSITIVE_INFINITY; float max = Float.NEGATIVE_INFINITY; final int xmin = raster.getMinX(); final int ymin = raster.getMinY(); final int width = raster.getWidth(); final int height = raster.getHeight(); for (int y=0; y<height; y++) { for (int x=0; x<width; x++) { final float value = raster.getSampleFloat(x + xmin, y + ymin, 0); if (value < min) min = value; if (value > min) max = value; } } final float scale = 255 / (max - min); final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); final WritableRaster dest = image.getRaster(); for (int y=0; y<height; y++) { for (int x=0; x<width; x++) { final double value = raster.getSampleDouble(x + xmin, y + ymin, 0); dest.setSample(x, y, 0, round((value - min) * scale)); } } if (!ImageIO.write(image, "png", file)) { throw new IIOException("No suitable PNG writer found."); } } /** * Displays the {@linkplain #image} if {@link #viewEnabled} is set to {@code true}, * otherwise does nothing. This method is mostly for debugging purpose. * * @param title The window title. */ @SuppressWarnings("deprecation") protected final synchronized void showCurrentImage(final String title) { final RenderedImage image = this.image; assertNotNull("An image must be set.", image); if (viewEnabled) { final String classname = getClass().getSimpleName(); try { EventQueue.invokeAndWait(new Runnable() { @Override public void run() { Viewer v = viewer; if (v == null) { viewer = v = new Viewer(classname); } v.addImage(image, String.valueOf(title)); } }); } catch (InterruptedException | InvocationTargetException e) { throw new AssertionError(e); } } } /** * Shows the default rendering of the specified coverage. * This is used for debugging only. * * @param coverage The coverage to display. */ protected final synchronized void show(final Coverage coverage) { if (!viewEnabled) { return; } final RenderedImage image = coverage.getRenderableImage(0,1).createDefaultRendering(); try { Class.forName("org.geotoolkit.gui.swing.image.OperationTreeBrowser") .getMethod("show", new Class<?>[] {RenderedImage.class}) .invoke(null, new Object[]{image}); } catch (RuntimeException e) { throw e; } catch (Exception e) { /* * The OperationTreeBrowser is not part of Geotk's core. It is optional and this * class should not fails if it is not presents. This is only a helper for debugging. */ System.err.println(e); } } /** * If a frame has been created by {@link #view}, wait for its disposal * before to move to the next test. */ @AfterClass public static void waitForFrameDisposal() { final Viewer v = viewer; if (v != null) { v.waitForFrameDisposal(); } } }