/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2007-2008, Open Source Geospatial Foundation (OSGeo) * * 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.geotools.image.io.mosaic; import java.awt.Point; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.net.URL; import java.net.URI; import java.net.URISyntaxException; import java.io.*; // We use a lot of those imports. import java.util.Arrays; import java.util.Iterator; import java.util.Collection; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.FileImageInputStream; import org.geotools.io.TableWriter; import org.geotools.util.Utilities; import org.geotools.util.logging.Logging; import org.geotools.resources.XArray; import org.geotools.resources.Classes; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; import static java.lang.Math.min; import static java.lang.Math.max; /** * A tile to be read by {@link MosaicImageReader}. Each tile must contains the following: * <p> * <ul> * <li><p><b>An {@link ImageReaderSpi} instance</b>. The same provider is typically used for every * tiles, but this is not mandatory. An {@linkplain ImageReader image reader} will be instantiated * and the {@linkplain #getInput input} will be assigned to it before a tile is read.</p></li> * * <li><p><b>An input</b>, typically a {@linkplain File file}, {@linkplain URL}, {@linkplain URI} * or {@linkplain String}. The input is typically different for every tile to be read, but this * is not mandatory. For example different tiles could be stored at different * {@linkplain #getImageIndex image index} in the same file.</p></li> * * <li><p><b>An image index</b> to be given to {@link ImageReader#read(int)} for reading the * tile. This index is often 0.</p></li> * * <li><p><b>The upper-left corner</b> in the destination image as a {@linkplain Point point}, * or the upper-left corner together with the image size as a {@linkplain Rectangle rectangle}. * If the upper-left corner has been given as a {@linkplain Point point}, then the * {@linkplain ImageReader#getWidth width} and {@linkplain ImageReader#getHeight height} will * be obtained from the image reader when first needed, which may have a slight performance cost. * If the upper-left corner has been given as a {@linkplain Rectangle rectangle} instead, then * this performance cost is avoided but the user is responsible for the accuracy of the * information provided. * * <blockquote><font size=2> * <b>NOTE:</b> The upper-left corner is the {@linkplain #getLocation location} of this tile * in the {@linkplain javax.imageio.ImageReadParam#setDestination destination image} when no * {@linkplain javax.imageio.ImageReadParam#setDestinationOffset destination offset} are * specified. If the user specified a destination offset, then the tile location will be * translated accordingly for the image being read. * </font></blockquote></p></li> * * <li><p><b>The subsampling relative to the tile having the best resolution.</b> This is not * the subsampling to apply when reading this tile, but rather the subsampling that we would * need to apply on the tile having the finest resolution in order to produce an image equivalent * to this tile. The subsampling is (1,1) for the tile having the finest resolution, (2,3) for an * overview having half the width and third of the height for the same geographic extent, * <cite>etc.</cite> (note that overviews are not required to have the same geographic extent - * the above is just an example).</p> * * <blockquote><font size=2> * <b>NOTE 1:</b> The semantic assumes that overviews are produced by subsampling, not by * interpolation or pixel averaging. The later are not prohibed, but doing so introduce * some subsampling-dependant variations in images produced by {@link MosaicImageReader}, * which would not be what we would expect from a strictly compliant {@link ImageReader}. * <br><br> * <b>NOTE 2:</b> Tile {@linkplain #getLocation location} and {@linkplain #getRegion region} * coordinates should be specified in the overview pixel units - they should <em>not</em> be * pre-multiplied by subsampling. This multiplication will be performed automatically by * {@link TileManager} when comparing regions from tiles at different subsampling levels. * </font></blockquote></p></li> * </ul> * <p> * The tiles are not required to be arranged on a regular grid, but performances may be * better if they are. {@link TileManagerFactory} is responsible for analysing the layout * of a collection of tiles and instantiate {@link TileManager} subclasses optimized for * the layout geometry. * <p> * {@code Tile}s can be considered as immutable after construction. However some properties * may be available only after this tile has been given to a {@link TileManagerFactory}. * <p> * {@code Tile}s are {@linkplain Serializable serializable} if their {@linkplain #getInput input} * given at construction time are serializable too. The {@link ImageReaderSpi} doesn't need to be * serializable, but its class must be known to {@link IIORegistry} at deserialization time. * * @since 2.5 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux */ public class Tile implements Comparable<Tile>, Serializable { /** * For cross-version compatibility during serialization. */ private static final long serialVersionUID = -5417183834232374962L; /* * IMPLEMENTATION NOTE: Try to keep Tile as compact as possible memory-wise (i.e. put as few * non-static fields as possible). Big mosaics may contain thousands of Tile instances, and * OutOfMemoryError tends to occur. The GridTileManager subclass can keep the number of Tile * instances low (generating them on the fly as needed), but sometime we have to fallback on * the more generic TreeTileManager, which stores a reference to every Tiles. */ /** * Mask to apply on every unsigned short values (16 bits) in order to get the 32 bits * integer to work with. This is also the maximal value allowed. */ static final int MASK = 0xFFFF; /** * The provider to use. The same provider is typically given to every {@code Tile} objects * to be given to the same {@link TileManager} instance, but this is not mandatory. * <p> * Consider this field as final. It is not because it needs to be set by {@link #readObject}. * If this field become public or protected in a future version, then we should make it final * and use reflection like {@link org.geotools.coverage.grid.GridCoverage2D#readObject}. */ private transient ImageReaderSpi provider; /** * The input to be given to the image reader. If the reader can not read that input * directly, it will be wrapped in an {@linkplain ImageInputStream image input stream}. * Note that this field must stay the <em>unwrapped</em> input. If the wrapped input is * wanted, use {@link ImageReader#getInput} instead. */ private final Object input; /** * The image index to be given to the image reader for reading this tile. * Stored as a unsigned short (i.e. must be used with {@code & MASK}). */ private final short imageIndex; /** * The subsampling relative to the tile having the finest resolution. If this tile is the * one with finest resolution, then the value shall be 1. Should never be 0 or negative, * except if its value has not yet been computed. * <p> * Values are stored as unsigned shorts (i.e. must be used with {@code & MASK}). * <p> * This field should be considered as final. It is not final only because * {@link RegionCalculator} may computes its value automatically. */ private short xSubsampling, ySubsampling; /** * The upper-left corner in the destination image. Should be considered as final, since * this class is supposed to be mostly immutable. However the value can be changed by * {@link #translate} before an instance is made public. */ private int x, y; /** * The size of the image to be read, or 0 if not yet computed. Values are stored * as <strong>unsigned</strong> shorts: they must be casted to {@code int} with * {@code s & MASK}. We assume that the {@code [0 .. 65535]} range is suffisient * on the basis that tiles need to be reasonably small for being useful. Furthermore * tiles are usually square and an image of size 32767×32767 reachs the limit * of Java Image I/O library anyway, since image area must hold in an {@code int}. */ private short width, height; /** * The "grid to real world" transform, used by {@link RegionCalculator} in order to compute * the {@linkplain #getRegion region} for this tile. This field is set to {@code null} when * {@link RegionCalculator}'s work is in progress, and set to a new value on completion. * <p> * <b>Note:</b> {@link RegionCalculator} really needs a new instance for each tile. * No caching allowed before {@code RegionCalculator} processing. Caching allowed * <em>after</em> {@code RegionCalculator} processing. */ private AffineTransform gridToCRS; /** * Creates a new tile which is a copy of the given one except for input and region. * The subsampling (and consequently the <cite>grid to CRS</cite> transform), image * index and image reader SPI are copied unchanged. * <p> * This method is not public because copying the field values may not be suffisient * if the given class is not of the same class than {@code this}. For example if it * is a {@link LargeTile}, then the width and height may be too small. This is okay * for {@link OverviewLevel} which use this constructor only with {@link Tile} instances. * * @param tile * The tile to copy. * @param input * The input to be given to the image reader, or {@code null} for the same input * than the given tile. * @param region * The region in the destination image, or {@code null} for the same region than * the given tile. If non-null, then the {@linkplain Rectangle#width width} and * {@linkplain Rectangle#height height} should match the image size. * @throws IllegalArgumentException * If a required argument is {@code null} or some argument has an invalid value. */ Tile(final Tile tile, final Object input, final Rectangle region) throws IllegalArgumentException { ensureNonNull("tile", tile); if (region != null) { if (region.isEmpty()) { throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_RECTANGLE_$1, region)); } x = region.x; y = region.y; setSize(region.width, region.height); } else { x = tile.x; y = tile.y; width = tile.width; height = tile.height; } this.input = (input != null) ? input : tile.input; provider = tile.provider; imageIndex = tile.imageIndex; xSubsampling = tile.xSubsampling; ySubsampling = tile.ySubsampling; gridToCRS = tile.gridToCRS; } /** * Creates a tile for the given provider, input and location. This constructor can be used when * the size of the image to be read by the supplied reader is unknown. This size will be * fetched automatically the first time {@link #getRegion} is invoked. * * @param provider * The image reader provider to use. The same provider is typically given to every * {@code Tile} objects to be given to the same {@link TileManager} instance, but * this is not mandatory. If {@code null}, the provider will be inferred from the * input. If it can't be inferred, then an exception is thrown. * @param input * The input to be given to the image reader. * @param imageIndex * The image index to be given to the image reader for reading this tile. * @param location * The upper-left corner in the destination image. * @param subsampling * The subsampling relative to the tile having the finest resolution, or {@code null} * if none. If non-null, width and height should be strictly positive. This argument * if of {@linkplain Dimension dimension} kind because it can also be understood as * relative "pixel size". * * @throws IllegalArgumentException * If a required argument is {@code null} or some argument has an invalid value. */ public Tile(ImageReaderSpi provider, final Object input, final int imageIndex, final Point location, final Dimension subsampling) throws IllegalArgumentException { if (provider == null) { provider = getImageReaderSpi(input); } ensureNonNull("provider", provider); ensureNonNull("input", input); ensureNonNull("location", location); this.provider = provider; this.input = input; this.imageIndex = ensurePositive(imageIndex); this.x = location.x; this.y = location.y; if (subsampling != null) { xSubsampling = ensureStrictlyPositive(subsampling.width); ySubsampling = ensureStrictlyPositive(subsampling.height); } else { xSubsampling = ySubsampling = 1; } } /** * Creates a tile for the given provider, input and region. This constructor can be used when * the size of the image to be read by the supplied reader is known. It avoid the cost of * fetching the size from the reader when {@link #getRegion} will be invoked. * * @param provider * The image reader provider to use. The same provider is typically given to every * {@code Tile} objects to be given to the same {@link TileManager} instance, but * this is not mandatory. If {@code null}, the provider will be inferred from the * input. If it can't be inferred, then an exception is thrown. * @param input * The input to be given to the image reader. * @param imageIndex * The image index to be given to the image reader for reading this tile. * @param region * The region in the destination image. The {@linkplain Rectangle#width width} and * {@linkplain Rectangle#height height} should match the image size. * @param subsampling * The subsampling relative to the tile having the finest resolution, or {@code null} * if none. If non-null, width and height should be strictly positive. This argument * if of {@linkplain Dimension dimension} kind because it can also be understood as * relative "pixel size". * * @throws IllegalArgumentException * If a required argument is {@code null} or some argument has an invalid value. */ public Tile(ImageReaderSpi provider, final Object input, final int imageIndex, final Rectangle region, final Dimension subsampling) throws IllegalArgumentException { if (provider == null) { provider = getImageReaderSpi(input); } ensureNonNull("provider", provider); ensureNonNull("input", input); ensureNonNull("region", region); if (region.isEmpty()) { throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_RECTANGLE_$1, region)); } this.provider = provider; this.input = input; this.imageIndex = ensurePositive(imageIndex); this.x = region.x; this.y = region.y; setSize(region.width, region.height); if (subsampling != null) { xSubsampling = ensureStrictlyPositive(subsampling.width); ySubsampling = ensureStrictlyPositive(subsampling.height); } else { xSubsampling = ySubsampling = 1; } } /** * Creates a tile for the given provider, input and "<cite>grid to real world</cite>" transform. * This constructor can be used when the {@linkplain #getLocation location} of the image to be * read by the supplied reader is unknown. The definitive location and the subsampling will be * computed automatically when this tile will be given to a {@link TileManagerFactory}. * <p> * When using this constructor, the {@link #getLocation}, {@link #getRegion} and * {@link #getSubsampling} methods will throw an {@link IllegalStateException} until this tile * has been given to a {@link TileManager}, which will compute those values automatically. * * @param provider * The image reader provider to use. The same provider is typically given to every * {@code Tile} objects to be given to the same {@link TileManager} instance, but * this is not mandatory. If {@code null}, the provider will be inferred from the * input. If it can't be inferred, then an exception is thrown. * @param input * The input to be given to the image reader. * @param imageIndex * The image index to be given to the image reader for reading this tile. * @param region * The tile region, or {@code null} if unknown. The (<var>x</var>,<var>y</var>) * location of this region is typically (0,0). The definitive location will be * computed when this tile will be given to a {@link TileManagerFactory}. * @param gridToCRS * The "<cite>grid to real world</cite>" transform. * * @throws IllegalArgumentException * If a required argument is {@code null} or some argument has an invalid value. */ public Tile(ImageReaderSpi provider, final Object input, final int imageIndex, final Rectangle region, final AffineTransform gridToCRS) throws IllegalArgumentException { if (provider == null) { provider = getImageReaderSpi(input); } ensureNonNull("provider", provider); ensureNonNull("input", input); ensureNonNull("gridToCRS", gridToCRS); this.provider = provider; this.input = input; this.imageIndex = ensurePositive(imageIndex); if (region != null) { this.x = region.x; this.y = region.y; if (!region.isEmpty()) { setSize(region.width, region.height); } } this.gridToCRS = new AffineTransform(gridToCRS); // Really needs a new instance - no cache } /** * Creates a tile for the given region with default subsampling. This constructor is * provided for avoiding compile-tile ambiguity between null <cite>subsampling</cite> * and null <cite>affine transform</cite> (the former is legal, the later is not). * * @param provider * The image reader provider to use. The same provider is typically given to every * {@code Tile} objects to be given to the same {@link TileManager} instance, but * this is not mandatory. If {@code null}, the provider will be inferred from the * input. If it can't be inferred, then an exception is thrown. * @param input * The input to be given to the image reader. * @param imageIndex * The image index to be given to the image reader for reading this tile. * @param region * The region in the destination image. The {@linkplain Rectangle#width width} and * {@linkplain Rectangle#height height} should match the image size. * * @throws IllegalArgumentException * If a required argument is {@code null} or some argument has an invalid value. */ public Tile(final ImageReaderSpi provider, final Object input, final int imageIndex, final Rectangle region) throws IllegalArgumentException { this(provider, input, imageIndex, region, (Dimension) null); } /** * Ensures that the given argument is non-null. */ static void ensureNonNull(final String argument, final Object value) { if (value == null) { throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, argument)); } } /** * Ensures that the given value is positive and in the range of 16 bits number. * Returns the value casted to an unsigned {@code short} type. */ private static short ensurePositive(final int n) throws IllegalArgumentException { if (n < 0 || n > MASK) { throw new IllegalArgumentException(Errors.format( ErrorKeys.VALUE_OUT_OF_BOUNDS_$3, n, 0, MASK)); } return (short) n; } /** * Ensures that the subsampling is strictly positive. This method is invoked for checking * user-supplied arguments, as opposed to {@link #checkGeometryValidity} which checks if * the subsampling has been computed. Both methods differ in exception type for that reason. */ static short ensureStrictlyPositive(final int n) throws IllegalArgumentException { if (n < 1) { throw new IllegalArgumentException(Errors.format(ErrorKeys.NOT_GREATER_THAN_ZERO_$1, n)); } return ensurePositive(n); } /** * Checks if the location, region, and subsampling can be returned. Throw an exception if this * tile has been {@linkplain #Tile(ImageReaderSpi, Object, int, Dimension, AffineTransform) * created without location} and not yet processed by {@link TileManagerFactory}. * <p> * <b>Note:</b> It is not strictly necessary to synchronize this method since update to a * {@code int} field is atomic according Java language specification, the {@link #xSubsampling} and * {@link #ySubsampling} fields do not change anymore as soon as they have a non-zero value (this is * checked by setSubsampling(Dimension) implementation) and this method succed only if both * fields are set. Most callers are already synchronized anyway, except {@link TileManager} * constructor which invoke this method only has a sanity check. It is okay to conservatively * get the exception in situations where a synchronized block would not have thrown it. * * @todo Localize the exception message. */ final void checkGeometryValidity() throws IllegalStateException { if (xSubsampling == 0 || ySubsampling == 0) { throw new IllegalStateException("Tile must be processed by TileManagerFactory."); } } /** * Closes the specified stream, if it is closeable. * * @param input The stream to close. * @throws IOException if an error occured while closing the input stream. */ static void close(final Object input) throws IOException { if (input instanceof ImageInputStream) { ((ImageInputStream) input).close(); } else if (input instanceof Closeable) { ((Closeable) input).close(); } } /** * Dispose the given reader after closing its {@linkplain ImageReader#getInput input stream}. * * @param reader The reader to dispose. * @throws IOException if an error occured while closing the input stream. */ static void dispose(final ImageReader reader) throws IOException { final Object input = reader.getInput(); reader.dispose(); close(input); } /** * Returns {@code true} if the specified input is valid for the given array of input types. */ private static boolean isValidInput(final Class<?>[] types, final Object input) { if (types != null) { for (final Class<?> type : types) { if (type!=null && type.isInstance(input)) { return true; } } } return false; } /** * Returns a reader created by the {@linkplain #getImageReaderSpi provider} and setup for * reading the image from the {@linkplain #getInput input}. If a reader is already setup with * the right input, then it is returned immediately. Otherwise if the image reader can accept * the {@linkplain #getInput input} directly, than that input is given to the image reader. * Otherwise the input is wrapped in an {@linkplain ImageInputStream image input stream}. * <p> * This method is invoked automatically by {@link MosaicImageReader} and should not needs * to be invoked directly. If an {@linkplain ImageInputStream image input stream} has been * created, it will be closed automatically when needed. * <p> * Note that this method will typically returns an instance to be shared by every tiles in * the given {@link MosaicImageReader}. Callers should not {@linkplain ImageReader#dispose * dispose} the reader or change its configuration, unless the {@code mosaic} argument was * null. * * @param mosaic The caller, or {@code null} if none. * @param seekForwardOnly If {@code true}, images and metadata may only be read * in ascending order from the input source. * @param ignoreMetadata If {@code true}, metadata may be ignored during reads. * @return An image reader with its {@linkplain ImageReader#getInput input} set. * @throws IOException if the image reader can't be initialized. */ protected ImageReader getImageReader(final MosaicImageReader mosaic, final boolean seekForwardOnly, final boolean ignoreMetadata) throws IOException { final ImageReaderSpi provider = getImageReaderSpi(); final ImageReader reader; final Object currentInput; if (mosaic != null) { reader = mosaic.getTileReader(provider); currentInput = mosaic.getRawInput(reader); } else { reader = provider.createReaderInstance(); currentInput = null; } /* * If the current reader input is suitable, we will keep it in order to preserve * any data that may be cached in the ImageReader instance. Only if the input is * not suitable, we will invoke ImageReader.setInput(...). */ final Object input = getInput(); final boolean sameInput = Utilities.equals(input, currentInput); if ( !sameInput || ( getImageIndex() < reader.getMinIndex()) || (!seekForwardOnly && reader.isSeekForwardOnly()) || (!ignoreMetadata && reader.isIgnoringMetadata())) { Object actualInput = reader.getInput(); reader.setInput(null); // Necessary for releasing the stream, in case it holds it. if (mosaic != null) { mosaic.setRawInput(reader, null); // For keeping the map consistent. } ImageInputStream stream = null; if (actualInput instanceof ImageInputStream) { stream = (ImageInputStream) actualInput; } final ImageReaderSpi spi = reader.getOriginatingProvider(); if (spi == null || isValidInput(spi.getInputTypes(), input)) { // We are allowed to use the input directly. Closes the stream // as a paranoiac safety (it should not be opened anyway). if (stream != null) { stream.close(); stream = null; } actualInput = input; } else { // We are not allowed to use the input directly. Creates a new input // stream, or reuse the previous one if it still useable. if (stream != null) { if (sameInput) try { stream.seek(0); } catch (IndexOutOfBoundsException e) { // We tried to reuse the same stream in order to preserve cached data, but it was // not possible to seek to the begining. Closes it; we will open a new one later. Logging.recoverableException(Tile.class, "getImageReader", e); stream.close(); stream = null; } else { stream.close(); stream = null; } } if (stream == null) { stream = getInputStream(); } actualInput = stream; } reader.setInput(actualInput, seekForwardOnly, ignoreMetadata); if (mosaic != null) { mosaic.setRawInput(reader, input); } } return reader; } /** * Returns a new reader created by the {@linkplain #getImageReaderSpi provider} and setup for * reading the image from the {@linkplain #getInput input}. This method returns a new reader * on each invocation. * <p> * It is the user's responsability to close the {@linkplain ImageReader#getInput reader input} * after usage. * * @return An image reader with its {@linkplain ImageReader#getInput input} set. * @throws IOException if the image reader can't be initialized. */ public ImageReader getImageReader() throws IOException { return getImageReader(null, true, true); } /** * Returns the image reader provider (never {@code null}). This is the provider used for * creating the {@linkplain ImageReader image reader} to be used for reading this tile. * * @return The image reader provider. * * @see ImageReaderSpi#createReaderInstance() */ public ImageReaderSpi getImageReaderSpi() { return provider; } /** * Returns an image reader provider inferred from the given input, * or {@code null} if none can be found without ambiguity. */ static ImageReaderSpi getImageReaderSpi(final Object input) { ImageReaderSpi provider = null; final String path = getInputName(input); if (path != null) { final int split = path.indexOf('.', path.lastIndexOf('/') + 1); if (split >= 0) { final String suffix = path.substring(split + 1).trim(); String[] suffixes = null; final Iterator<ImageReaderSpi> it = IIORegistry.getDefaultInstance() .getServiceProviders(ImageReaderSpi.class, true); while (it.hasNext()) { final ImageReaderSpi candidate = it.next(); final String[] candidateSuffixes = candidate.getFileSuffixes(); if (XArray.containsIgnoreCase(candidateSuffixes, suffix)) { if (provider != null) { if (Arrays.equals(candidateSuffixes, suffixes)) { // E.g. we may have both CLIB and JSE version of PNG reader. continue; } // We have an ambiguity - Returns null so we don't make a choice. return null; } provider = candidate; suffixes = candidateSuffixes; // Continue the search for making sure that we don't have an ambiguity. } } } } return provider; } /** * Creates an image input stream from the input. If no suitable input stream can be created, * then this method throws an exception. This method never returns {@code null}. * * @return The image input stream. * @throws IOException if an error occured while creating the input stream. */ private ImageInputStream getInputStream() throws IOException { final Object input = getInput(); ImageInputStream stream = ImageIO.createImageInputStream(input); if (stream != null) { return stream; } /* * We tried the input directly in case the user provided some SPI for String * objects. If we have not been able to create a stream from a plain string, * create a URL or a File object from the string and try again. */ if (input instanceof CharSequence) { final String path = input.toString(); final Object url; if (path.indexOf("://") > 0) { url = new URL(path); } else { url = new File(path); } stream = ImageIO.createImageInputStream(url); if (stream != null) { return stream; } } /* * In theory ImageIO.createImageInputStream(Object) should have accepted a File input, * so the following check is useless. However if ImageIO.createImageInputStream(Object) * failed, it just returns null; we have no idea why it failed. One possible cause is * "Too many open files", in which case throwing a FileNotFoundException is misleading. * So we try here to create a FileImageInputStream directly, which is likely to fail as * well but this time with a more accurate error message. */ if (input instanceof File) { stream = new FileImageInputStream((File) input); return stream; } throw new FileNotFoundException(Errors.format( ErrorKeys.FILE_DOES_NOT_EXIST_$1, input)); } /** * Returns the input to be given to the image reader for reading this tile. * * @return The image input. * * @see ImageReader#setInput */ public Object getInput() { return input; } /** * Returns a short string representation of the {@linkplain #getInput input}. The * default implementation returns the following: * <p> * <ul> * <li>For {@linkplain CharSequence Character sequence}, returns the * {@linkplain CharSequence#toString string} form.</li> * <li>For {@linkplain File}, returns only the {@linkplain File#getName name} part.</li> * <li>For {@linkplain URL} or {@linkplain URI}, returns the path without the protocol or * query parts.</li> * <li>For other classes, returns {@code "class"} followed by the unqualified class name.</li> * </ul> * * @return A short string representation of the input (never {@code null}). */ public String getInputName() { String name = getInputName(getInput()); if (name == null) { name = "class " + Classes.getShortClassName(input); } return name; } /** * Returns a short string representation of the given input, * or {@code null} if the input can not be formatted. */ private static String getInputName(final Object input) { if (input instanceof File) { return ((File) input).getName(); } if (input instanceof URI) { return ((URI) input).getPath(); } if (input instanceof URL) { return ((URL) input).getPath(); } if (input instanceof CharSequence) { return input.toString(); } return null; } /** * Returns a format name inferred from the {@linkplain #getImageReaderSpi provider}. * * @return The format name. */ public String getFormatName() { return toString(getImageReaderSpi()); } /** * Returns the image index to be given to the image reader for reading this tile. * * @return The image index, numbered from 0. * * @see ImageReader#read(int) */ public int getImageIndex() { return imageIndex & MASK; } /** * If the user-supplied transform is waiting for a processing by {@link RegionCalculator}, * returns it. Otherwise returns {@code null}. This method is for internal usage by * {@link RegionCalculator} only. * <p> * See {@link #checkGeometryValidity} for a note about synchronization. When {@code clear} * is {@code false} (i.e. this method is invoked just in order to get a hint), it is okay * to conservatively return a non-null value in situations where a synchronized block would * have returned {@code null}. * * @param clear If {@code true}, clears the {@link #gridToCRS} field before to return. This * is a way to tell that processing is in progress, and also a safety against * transform usage while it may become invalid. * @return The transform, or {@code null} if none. This method does not clone the returned * value - {@link RegionCalculator} will reference and modify directly that transform. */ final AffineTransform getPendingGridToCRS(final boolean clear) { assert !clear || Thread.holdsLock(this); // Lock required only if 'clear' is true. if (xSubsampling != 0 || ySubsampling != 0) { // No transform waiting to be processed. return null; } final AffineTransform gridToCRS = this.gridToCRS; if (clear) { this.gridToCRS = null; } return gridToCRS; } /** * Returns the "<cite>grid to real world</cite>" transform, or {@code null} if unknown. * This transform is derived from the value given to the constructor, but may not be * identical since it may have been {@linkplain AffineTransform#translate translated} * in order to get a uniform grid geometry for every tiles in a {@link TileManager}. * * @return The "grid to real world" transform, or {@code null} if undefined. * @throws IllegalStateException If this tile has been {@linkplain #Tile(ImageReaderSpi, * Object, int, Dimension, AffineTransform) created without location} and not yet * processed by {@link TileManagerFactory}. * * @see TileManager#getGridGeometry */ public synchronized AffineTransform getGridToCRS() throws IllegalStateException { checkGeometryValidity(); return gridToCRS; // No need to clone since TileManagerFactory assigned an immutable instance. } /** * Sets the new "<cite>grid to real world</cite>" transform to use after the translation * performed by {@link #translate}, if any. Should be an immutable instance because it will * not be cloned. * * @throws IllegalStateException if an other transform was already assigned to this tile. */ final synchronized void setGridToCRS(final AffineTransform at) throws IllegalStateException { if (gridToCRS != null) { if (!gridToCRS.equals(at)) { throw new IllegalStateException(); } } else { gridToCRS = at; } } /** * Returns the subsampling relative to the tile having the finest resolution. This method never * returns {@code null}, and the width & height shall never be smaller than 1. The return type * is of {@linkplain Dimension dimension} kind because the value can also be interpreted as * relative "pixel size". * * @return The subsampling along <var>x</var> and <var>y</var> axis. * @throws IllegalStateException If this tile has been {@linkplain #Tile(ImageReaderSpi, * Object, int, Dimension, AffineTransform) created without location} and not yet * processed by {@link TileManagerFactory}. * * @see javax.imageio.ImageReadParam#setSourceSubsampling */ public synchronized Dimension getSubsampling() throws IllegalStateException { checkGeometryValidity(); return new Dimension(xSubsampling & MASK, ySubsampling & MASK); } /** * Invoked by {@link RegionCalculator} only. No other caller allowed. */ final void setSubsampling(final Dimension subsampling) throws IllegalStateException { assert Thread.holdsLock(this); if (xSubsampling != 0 || ySubsampling != 0) { throw new IllegalStateException(); // Should never happen. } xSubsampling = ensureStrictlyPositive(subsampling.width); ySubsampling = ensureStrictlyPositive(subsampling.height); } /** * Returns the highest subsampling that this tile can handle, not greater than the given * subsampling. Special cases: * <p> * <ul> * <li>If the given subsampling is {@code null}, then this method returns {@code null}.</li> * <li>Otherwise if the given subsampling is {@code (0,0)}, then this method returns the * same {@code subsampling} reference unchanged. Callers can test using the identity * ({@code ==}) operator.</li> * <li>Otherwise if this tile can handle exactly the given subsampling, then this method * returns the same {@code subsampling} reference unchanged. Callers can test using * the identity ({@code ==}) operator.</li> * <li>Otherwise if there is no subsampling that this tile could handle, * then this method returns {@code null}.</li> * <li>Otherwise this method returns a new {@link Dimension} set to the greatest subsampling * that this tile can handle, not greater than the given subsampling.</li> * </ul> * * @param subsampling The subsampling along <var>x</var> and <var>y</var> axis. * @return A subsampling equals or finer than the given one. * @throws IllegalStateException If this tile has been {@linkplain #Tile(ImageReaderSpi, * Object, int, Dimension, AffineTransform) created without location} and not yet * processed by {@link TileManagerFactory}. */ public Dimension getSubsamplingFloor(final Dimension subsampling) throws IllegalStateException { if (subsampling != null) { final int dx, dy; try { dx = subsampling.width % (xSubsampling & MASK); dy = subsampling.height % (ySubsampling & MASK); } catch (ArithmeticException e) { throw new IllegalStateException("Tile must be processed by TileManagerFactory.", e); } if (dx != 0 || dy != 0) { final int sourceXSubsampling = subsampling.width - dx; final int sourceYSubsampling = subsampling.height - dy; if (sourceXSubsampling != 0 && sourceYSubsampling != 0) { return new Dimension(sourceXSubsampling, sourceYSubsampling); } else { return null; } } } return subsampling; } /** * Returns {@code true} if this tile subsampling is finer than the specified value * for at least one dimension. For internal usage by {@link RTree#searchTiles} only. */ final boolean isFinerThan(final Dimension subsampling) { return (xSubsampling & MASK) < subsampling.width || (ySubsampling & MASK) < subsampling.height; } /** * Returns {@code true} if the subsampling of this tile is equals to the subsampling of * the given tile, but this tile cover a greater area than the given tile. * * @param other The other tile to compare with. * @return {@code true} if both tiles have the same subsampling and this tile is larger. */ final boolean isLargerThan(final Tile other) { return xSubsampling == other.xSubsampling && ySubsampling == other.ySubsampling && (width & MASK) * (height & MASK) > (other.width & MASK) * (other.height & MASK); } /** * Returns the upper-left corner in the * {@linkplain javax.imageio.ImageReadParam#setDestination destination image}. This is the * location when no {@linkplain javax.imageio.ImageReadParam#setDestinationOffset destination * offset} are specified. If the user specified a destination offset, then the tile location * will be translated accordingly for the image being read. * * @return The tile upper-left corner. * @throws IllegalStateException If this tile has been {@linkplain #Tile(ImageReaderSpi, * Object, int, Dimension, AffineTransform) created without location} and not yet * processed by {@link TileManagerFactory}. * * @see javax.imageio.ImageReadParam#setDestinationOffset */ public synchronized Point getLocation() throws IllegalStateException { checkGeometryValidity(); return new Point(x,y); } /** * Returns {@code true} if the tile size is equals to the given dimension. * This method should be invoked when we know that this instance is not a * subclass of {@link Tile}, otherwise we should use {@link #getRegion} in * case the user overriden the method. */ final boolean isSizeEquals(final int dx, final int dy) { assert getClass().equals(Tile.class) && (width != 0) && (height != 0) : this; return (width & MASK) == dx && (height & MASK) == dy; } /** * Returns the upper-left corner in the destination image, with the image size. If this tile * has been created with the {@linkplain #Tile(ImageReader,Object,int,Rectangle,Dimension) * constructor expecting a rectangle}, a copy of the specified rectangle is returned. * Otherwise the image {@linkplain ImageReader#getWidth width} and * {@linkplain ImageReader#getHeight height} are read from the image reader and cached for * future usage. * * @return The region in the destination image. * @throws IllegalStateException If this tile has been {@linkplain #Tile(ImageReaderSpi, * Object, int, Dimension, AffineTransform) created without location} and not yet * processed by {@link TileManagerFactory}. * @throws IOException if it was necessary to fetch the image dimension from the * {@linkplain #getImageReader reader} and this operation failed. * * @see javax.imageio.ImageReadParam#setSourceRegion */ public synchronized Rectangle getRegion() throws IllegalStateException, IOException { checkGeometryValidity(); if (width == 0 && height == 0) { final int imageIndex = getImageIndex(); final ImageReader reader = getImageReader(); setSize(reader.getWidth(imageIndex), reader.getHeight(imageIndex)); dispose(reader); } return new Rectangle(x, y, width & MASK, height & MASK); } /** * Returns the {@linkplain #getRegion region} multiplied by the subsampling. * This is this tile coordinates in the units of the tile having the finest * resolution, as opposed to the default public methods which are always in * units relative to this tile. */ final Rectangle getAbsoluteRegion() throws IOException { final Rectangle region = getRegion(); final int sx = xSubsampling & MASK; final int sy = ySubsampling & MASK; region.x *= sx; region.y *= sy; region.width *= sx; region.height *= sy; return region; } /** * Invoked by {@link RegionCalculator} only. No other caller allowed. * {@link #setSubsampling} must be invoked prior this method. * <p> * Note that invoking this method usually invalidate {@link #gridToCRS}. Calls to this method * should be closely followed by calls to {@link #translate} for fixing the "gridToCRS" value. * * @param region The region to assign to this tile. * @throws ArithmeticException if {@link #setSubsampling} has not be invoked. */ final void setAbsoluteRegion(final Rectangle region) throws ArithmeticException { assert Thread.holdsLock(this); final int sx = xSubsampling & MASK; final int sy = ySubsampling & MASK; assert (region.width % sx) == 0 && (region.height % sy) == 0 : region; x = region.x / sx; y = region.y / sy; setSize(region.width / sx, region.height / sy); } /** * Sets the tile size to the given values, making sure that they can be stored as unsigned * short. This method is overriden by {@link LargeTile} but should never been invoked by * anyone else than {@link Tile}. * * @param dx The tile width. * @param dy The tile height. * @throws IllegalArgumentException if the given size can't be stored as unsigned short. */ void setSize(final int dx, final int dy) throws IllegalArgumentException { width = (short) dx; height = (short) dy; final String name; final int value; if ((width & MASK) != dx) { name = "width"; value = dx; } else if ((height & MASK) != dy) { name = "height"; value = dy; } else { return; } width = 0; height = 0; throw new IllegalArgumentException(Errors.format( ErrorKeys.VALUE_OUT_OF_BOUNDS_$3, name + '=' + value, 0, MASK)); } /** * Converts to given rectangle from absolute to relative coordinates. * Coordinates are rounded to the smallest box enclosing fully the given region. * * @param region The rectangle to converts. Values are replaced in-place. * @throws ArithmeticException if {@link #setSubsampling} has not be invoked. */ final void absoluteToRelative(final Rectangle region) throws ArithmeticException { final int sx = xSubsampling & MASK; final int sy = ySubsampling & MASK; int xmin = region.x; int xmax = region.width + xmin; int ymin = region.y; int ymax = region.height + ymin; if (xmin < 0) xmin -= (sx - 1); if (xmax > 0) xmax += (sx - 1); if (ymin < 0) ymin -= (sy - 1); if (ymax > 0) ymax += (sy - 1); xmin /= sx; xmax /= sx; ymin /= sy; ymax /= sy; region.x = xmin; region.y = ymin; region.width = xmax - xmin; region.height = ymax - ymin; } /** * Translates this tile. For internal usage by {@link RegionCalculator} only. * This method is invoked slightly after {@link #setRegion} for final adjustment. * <p> * Reminder: {@link #setGridToCRS(AffineTransform)} should be invoked after this method. * * @param xSubsampling The translation to apply on <var>x</var> values (often 0). * @param ySubsampling The translation to apply on <var>y</var> values (often 0). */ final synchronized void translate(final int dx, final int dy) { x += dx; y += dy; gridToCRS = null; } /** * Returns the amount of pixels in this tile that would be useless if reading the given region * at the given subsampling. This method is invoked by {@link TileManager} when two or more * tile overlaps, in order to choose the tiles that would minimize the amount of pixels to * read. The default implementation computes the sum of: * <ul> * <li>the amount of tile pixels skipped because of the given subsampling</li> * <li>the amount of pixels in this {@linkplain #getRegion tile region} that are outside * the given region, including the pixels below the bottom.</li> * </ul> * The later is conservative since many file formats will stop reading as soon as they reach * the region bottom. We may consider allowing overriding in order to alter this calculation * if a subclass is sure that pixels below the region have no disk seed cost. * * @param toRead The region to read, in the same units than {@link #getAbsoluteRegion}. * @param subsampling The number of columns and rows to advance between pixels * in the given region. Must be strictly positive (not zero). * @return The amount of pixels which would be unused if the reading was performed on this * tile. Smaller number is better. * @throws IOException if it was necessary to fetch the image dimension from the * {@linkplain #getImageReader reader} and this operation failed. */ final int countUnwantedPixelsFromAbsolute(final Rectangle toRead, final Dimension subsampling) throws IOException { final int sx = xSubsampling & MASK; final int sy = ySubsampling & MASK; assert subsampling.width >= sx && subsampling.height >= sy : subsampling; final Rectangle region = getRegion(); /* * Converts the tile region to absolute coordinates and clips it to the region to read. */ final long xmin, ymin, xmax, ymax; xmin = max((long) toRead.x, sx * ((long) region.x)); ymin = max((long) toRead.y, sy * ((long) region.y)); xmax = min((long) toRead.x + toRead.width, sx * ((long) region.x + region.width)); ymax = min((long) toRead.y + toRead.height, sy * ((long) region.y + region.height)); /* * Computes the amount of pixels to keep for the given region and subsampling. */ long count = max(xmax - xmin, 0) * max(ymax - ymin, 0); count /= (subsampling.width * subsampling.height); /* * Computes the amount of pixels from the current tile that would be unused. Note that * we are substracting a quantity derived from the absolute space from a quantity in the * relative space. The result should be positive anyway because we divided the former by * (s.width * s.height), which should be greater than (xSubsampling * ySubsampling). */ count = (region.width * region.height) - count; assert count >= 0 && count <= Integer.MAX_VALUE : count; return (int) count; } /** * Converts {@link URL} to {@link URI} and {@link CharSequence} to {@link String} for * comparaison purpose. {@link File}, {@link URI} and {@link String} are not converted * because they are already {@linkplain Comparable comparable}. */ private static Object toComparable(Object input) { if (input instanceof URL) try { input = ((URL) input).toURI(); } catch (URISyntaxException exception) { // Ignores - we will keep it as a URL. Logs with "compare" as source method // name, since it is the public API that invoked this private method. Logging.recoverableException(Tile.class, "compare", exception); } else if (input instanceof CharSequence) { input = input.toString(); } return input; } /** * Tries to converts the given input into something that can be compared to the given base. * Returns the input unchanged if this method doesn't know how to convert it. */ private static Object toCompatible(Object input, final Object target) { if (target instanceof URI) { if (input instanceof File) { input = ((File) input).toURI(); } } else if (target instanceof String) { if (input instanceof File || input instanceof URI) { input = input.toString(); } } return input; } /** * Compares two inputs for order. {@link String}, {@link File} and {@link URI} are comparable. * {@link URL} are not but can be converted to {@link URI} for comparaison purpose. */ @SuppressWarnings("unchecked") private static int compareInputs(Object input1, Object input2) { if (Utilities.equals(input1, input2)) { return 0; } input1 = toComparable(input1); input2 = toComparable(input2); // Must be before 'toCompatible'. input1 = toCompatible(input1, input2); input2 = toCompatible(input2, input1); if (input1 instanceof Comparable && input1.getClass().isInstance(input2)) { return ((Comparable) input1).compareTo(input2); } if (input2 instanceof Comparable && input2.getClass().isInstance(input1)) { return -((Comparable) input2).compareTo(input1); } int c = input1.getClass().getName().compareTo(input2.getClass().getName()); if (c != 0) { return c; } /* * Following is an unconvenient comparaison criterion, but this fallback should never * occurs in typical use cases. We use it on a "better than nothing" basis. It should * be consistent in a given running JVM, but it not likely to be consistent when comparing * the same tiles in two different JVM executions. In addition there is also a slight risk * that this code returns 0 while we would like to return a non-zero value. */ return System.identityHashCode(input2) - System.identityHashCode(input1); } /** * Compares two tiles for optimal order in sequential reads. Default implementation sorts by * {@linkplain #getInput input} first, then increasing {@linkplain #getImageIndex image index}. * This ordering allows efficient access for tiles that use the same * {@linkplain #getImageReader image reader}. * <p> * For tiles having the same input and index, additional criterions are used like increasing * subsampling, increasing <var>y</var> then increasing <var>x</var> coordinates. But the * actual set of additional criterions may change. * <p> * This method is consistent with {@link #equals} in the most common case where every * tiles to be compared (typically every tiles given to a {@link TileManager} instance) * have inputs of the same kind (preferrably {@link File}, {@link URL}, {@link URI} or * {@link String}), and there is no duplicated ({@linkplain #getInput input}, * {@linkplain #getImageIndex image index}) pair. * * @param other The tile to compare with. * @return -1 if this tile should be read before {@code other}, +1 if it should be read * after or 0 if equals. */ public final int compareTo(final Tile other) { int c = compareInputs(input, other.input); if (c == 0) { c = (imageIndex & MASK) - (other.imageIndex & MASK); if (c == 0) { /* * From this point it doesn't matter much for disk access. But we continue to * define criterions for consistency with 'equals(Object)' method. We compare * subsampling first because it may be undefined while it is needed for (x,y) * ordering. Undefined subsampling will be ordered first (this is arbitrary). */ final int sy = this.ySubsampling & MASK; final int oy = other.ySubsampling & MASK; c = sy - oy; if (c == 0) { final int sx = this.xSubsampling & MASK; final int ox = other.xSubsampling & MASK; c = sx - ox; if (c == 0) { c = (y * sy) - (other.y * oy); if (c == 0) { c = (x * sx) - (other.x * ox); } } } } } return c; } /** * Compares this tile with the specified one for equality. Two tiles are considered equal * if they have the same {@linkplain #getImageReaderSpi provider}, {@linkplain #getInput * input}, {@linkplain #getImageIndex image index}, {@linkplain #getRegion region} and * {@linkplain #getSubsampling subsampling}. * * @param object The object to compare with. * @return {@code true} if both objects are equal. */ @Override public boolean equals(final Object object) { if (object == this) { return true; } if (object != null && object.getClass().equals(getClass())) { final Tile that = (Tile) object; if (this.x == that.x && this.y == that.y && this.xSubsampling == that.xSubsampling && this.ySubsampling == that.ySubsampling && this.imageIndex == that.imageIndex && Utilities.equals(provider, that.provider) && Utilities.deepEquals(input, that.input)) { /* * Compares width and height only if they are defined in both tiles. We do not * invoke 'getRegion()' because it may be expensive and useless anyway: If both * tiles have the same image reader, image index and input, then logically they * must have the same size - invoking 'getRegion()' would read exactly the same * image twice. */ return (width == 0 || that.width == 0 || width == that.width) && (height == 0 || that.height == 0 || height == that.height); } } return false; } /** * Returns a hash code value for this tile. The default implementation uses the * {@linkplain #getImageReader reader}, {@linkplain #getInput input} and {@linkplain * #getImageIndex image index}, which should be suffisient for uniquely distinguish * every tiles. */ @Override public int hashCode() { return provider.hashCode() + Utilities.deepHashCode(input) + 37*imageIndex; } /** * Returns the name of the given provider, for {@link #toString} purpose only. * May returns {@code null} if the name is unknown. */ static String toString(final ImageReaderSpi provider) { String name = null; if (provider != null) { final String[] formats = provider.getFormatNames(); if (formats != null) { int length = 0; for (int i=0; i<formats.length; i++) { final String candidate = formats[i]; if (candidate != null) { final int lg = candidate.length(); if (lg > length) { length = lg; name = candidate; } } } } } return name; } /** * Returns a string representation of this tile. The default implementation uses only the * public getter methods, so if a subclass override them the effect should be visible in * the returned string. */ @Override public String toString() { final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this)).append('['); buffer.append("format=\"").append(getFormatName()) .append("\", input=\"").append(getInputName()) .append("\", index=").append(getImageIndex()); if (xSubsampling != 0 || ySubsampling != 0) { buffer.append(", location=("); if (width == 0 && height == 0) { final Point location = getLocation(); buffer.append(location.x).append(',').append(location.y); } else try { final Rectangle region = getRegion(); buffer.append(region.x).append(',').append(region.y) .append("), size=(").append(region.width).append(',').append(region.height); } catch (IOException e) { // Should not happen since we checked that 'getRegion' should be easy. // If it happen anyway, put the exception message at the place where // coordinates were supposed to appear, so we can debug. buffer.append(e); } final Dimension subsampling = getSubsampling(); buffer.append("), subsampling=(").append(subsampling.width) .append(',').append(subsampling.height).append(')'); } else { /* * Location and subsampling not yet computed, so don't display it. We can not * invoke 'getRegion()' neither since it would throw an IllegalStateException. * Since we have to read the fields directly, make sure that this instance is * not a subclass like LargeTile, otherwise those values may be wrong. */ if ((width != 0 || height != 0) && getClass().equals(Tile.class)) { buffer.append(", size=(").append(width & MASK) .append(',').append(height & MASK).append(')'); } } return buffer.append(']').toString(); } /** * Returns a string representation of a collection of tiles. The tiles are formatted in a * table in iteration order. Tip: consider sorting the tiles before to invoke this method; * tiles are {@linkplain Comparable comparable} for this purpose. * <p> * This method is not public because it can consume a large amount of memory (the underlying * {@link StringBuffer} can be quite large). Users are encouraged to use the method expecting * a {@link Writer}, which may be expensive too but less than this method. * * @param tiles * The tiles to format in a table. * @param maximum * The maximum number of tiles to format. If there is more tiles, a message will be * formatted below the table. A reasonable value like 5000 is recommanded since * attempt to format millions of tiles leads to {@link OutOfMemoryError}. * @return A string representation of the given tiles as a table. * * @see java.util.Collections#sort(List) */ static String toString(final Collection<Tile> tiles, final int maximum) { final StringWriter writer = new StringWriter(); try { writeTable(tiles, writer, maximum); } catch (IOException e) { // Should never happen since we are writing to a StringWriter. throw new AssertionError(e); } return writer.toString(); } /** * Formats a collection of tiles in a table. The tiles are appended in iteration * order. Tip: consider sorting the tiles before to invoke this method; tiles are * {@linkplain Comparable comparable} for this purpose. * * @param tiles * The tiles to format in a table. * @param out * Where to write the table. * @param maximum * The maximum number of tiles to format. If there is more tiles, a message will be * formatted below the table. A reasonable value like 5000 is recommanded since * attempt to format millions of tiles leads to {@link OutOfMemoryError}. * @throws IOException * If an error occured while writing to the given writer. * * @see java.util.Collections#sort(List) */ public static void writeTable(final Collection<Tile> tiles, final Writer out, final int maximum) throws IOException { int remaining = maximum; final TableWriter table = new TableWriter(out); table.nextLine(TableWriter.DOUBLE_HORIZONTAL_LINE); table.write("Format\tInput\tindex\tx\ty\twidth\theight\tdx\tdy\n"); table.nextLine(TableWriter.SINGLE_HORIZONTAL_LINE); table.setMultiLinesCells(true); for (final Tile tile : tiles) { if (--remaining < 0) { break; } table.setAlignment(TableWriter.ALIGN_LEFT); final String format = tile.getFormatName(); if (format != null) { table.write(format); } table.nextColumn(); table.write(tile.getInputName()); table.nextColumn(); table.setAlignment(TableWriter.ALIGN_RIGHT); table.write(String.valueOf(tile.getImageIndex())); table.nextColumn(); /* * Extracts now the tile information that we are going to format, but those * informations may be overriden later if the current tile is some subclass * of Tile. We format Tile instances in a special way since it allows us to * left a blank for subsampling and tile size if they are not yet computed, * rather than throwing an exception. */ int x = tile.x; int y = tile.y; int width = tile.width & MASK; int height = tile.height & MASK; int xSubsampling = tile.xSubsampling & MASK; int ySubsampling = tile.ySubsampling & MASK; if (!tile.getClass().equals(Tile.class)) { final Dimension subsampling = tile.getSubsampling(); xSubsampling = subsampling.width; ySubsampling = subsampling.height; try { final Rectangle region = tile.getRegion(); x = region.x; y = region.y; width = region.width; height = region.height; } catch (IOException e) { // The (x,y) are likely to be correct since only (width,height) are read // from the image file. So set only (width,height) to "unknown" and keep // the remaining, with (x,y) obtained from direct access to Tile fields. width = 0; height = 0; } } table.write(String.valueOf(x)); table.nextColumn(); table.write(String.valueOf(y)); if (width != 0 || height != 0) { table.nextColumn(); table.write(String.valueOf(width)); table.nextColumn(); table.write(String.valueOf(height)); } else { table.nextColumn(); table.nextColumn(); } if (xSubsampling != 0 || ySubsampling != 0) { table.nextColumn(); table.write(String.valueOf(xSubsampling)); table.nextColumn(); table.write(String.valueOf(ySubsampling)); } table.nextLine(); } table.nextLine(TableWriter.DOUBLE_HORIZONTAL_LINE); /* * Table completed. Flushs to the writer and appends additional text if we have * not formatted every tiles. IOException may be trown starting from this point * (the above code is not expected to thrown any IOException). */ table.flush(); if (remaining < 0) { out.write(Vocabulary.format(VocabularyKeys.MORE_$1, tiles.size() - maximum)); out.write(System.getProperty("line.separator", "\n")); } } /** * Invoked on serialization. Serialization of {@linkplain #provider} is replaced by * serialization of its class name only. The actual provider instance will be fetch * from ImageIO registry on deserialization. */ private void writeObject(final ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeObject(Classes.getClass(provider)); } /** * Invoked on deserialization. The provider is fetch from currently registered providers * in the {@link IIORegistry}. The search is performed by classname. */ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); final Object candidate = in.readObject(); final IIORegistry registry = IIORegistry.getDefaultInstance(); Class<?> type = candidate.getClass(); // Initialized in case of failure on next line. try { type = (Class<?>) candidate; provider = (ImageReaderSpi) registry.getServiceProviderByClass(type); } catch (ClassCastException cause) { InvalidClassException e = new InvalidClassException(type.getName(), Errors.format(ErrorKeys.ILLEGAL_CLASS_$2, type, ImageReaderSpi.class)); e.initCause(cause); throw e; } if (provider == null) { throw new ClassNotFoundException(type.getName()); } } }