/* * 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.image.io.plugin; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Locale; import java.util.Set; import java.util.Collections; import javax.imageio.ImageReader; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ServiceRegistry; import java.awt.geom.AffineTransform; import java.awt.Rectangle; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.geotoolkit.image.io.InformationType; import org.geotoolkit.image.io.ImageReaderAdapter; import org.geotoolkit.image.io.metadata.SpatialMetadata; import org.geotoolkit.image.io.metadata.ReferencingBuilder; import org.geotoolkit.internal.image.io.GridDomainAccessor; import org.geotoolkit.internal.image.io.SupportFiles; import org.geotoolkit.internal.image.io.Formats; import org.geotoolkit.nio.IOUtilities; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.lang.Configuration; import org.geotoolkit.io.wkt.PrjFiles; import org.apache.sis.util.logging.Logging; import org.geotoolkit.image.io.SpatialImageReader; import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME; /** * Reader for the <cite>World File</cite> format. This reader wraps an other image reader * for an "ordinary" image format, like TIFF, PNG or JPEG. This {@code WorldFileImageReader} * delegates the reading of pixel values to the wrapped reader, and additionally looks for * two small text files in the same directory than the image file, with the same filename * but a different extension: * * <ul> * <li><p>A text file containing the coefficients of the affine transform mapping pixel * coordinates to geodesic coordinates. The reader expects one coefficient per line, * in the same order than the one expected by the * {@link AffineTransform#AffineTransform(double[]) AffineTransform(double[])} * constructor, which is <var>scaleX</var>, <var>shearY</var>, <var>shearX</var>, * <var>scaleY</var>, <var>translateX</var>, <var>translateY</var>. * This reader looks for a file having the following extensions, in preference order:</p> * <ol> * <li>The first letter of the image file extension, followed by the last letter of * the image file extension, followed by {@code 'w'}. Example: {@code "tfw"} for * {@code "tiff"} images, and {@code "jgw"} for {@code "jpeg"} images.</li> * <li>The extension of the image file with a {@code 'w'} appended.</li> * <li>The {@code "tfw} extension.</li> * </ol> * </li> * <li><p>A text file containing the <cite>Coordinate Reference System</cite> (CRS) * definition in <cite>Well Known Text</cite> (WKT) syntax. This reader looks * for a file having the {@code ".prj"} extension.</p></li> * </ul> * * Every text file are expected to be encoded in ISO-8859-1 (a.k.a. ISO-LATIN-1) and every * numbers are expected to be formatted in US locale. * * @author Martin Desruisseaux (Geomatys) * @version 3.08 * * @see <a href="http://en.wikipedia.org/wiki/World_file">World File Format Description</a> * @see WorldFileImageWriter * * @since 3.08 (derived from 3.07) * @module */ public class WorldFileImageReader extends ImageReaderAdapter { /** * {@code true} if the attempt to replace the {@linkplain #input} by a {@link File} * has been done. We try to use a {@link File} because it allows us to check if the * file exists. Only one attempt will be performed for a new input. */ private boolean inputReplaced; /** * Constructs a new image reader. The provider argument is mandatory for this constructor. * If the provider is unknown, use the next constructor below instead. * * @param provider The {@link ImageReaderSpi} that is constructing this object. * @throws IOException If an error occurred while creating the {@linkplain #main main} reader. */ public WorldFileImageReader(final Spi provider) throws IOException { super(provider); } /** * Constructs a new image reader wrapping the given reader. * * @param provider The {@link ImageReaderSpi} that is constructing this object, or {@code null}. * @param main The reader to use for reading the pixel values. */ public WorldFileImageReader(final Spi provider, final ImageReader main) { super(provider, main); } /** * Creates the input to be given to the reader identified by the given argument. If the * {@code readerID} argument is {@code "main"} (ignoring case), then this method delegates * to the {@linkplain ImageReaderAdapter#createInput(String) super-class method}. Otherwise * this method returns an input which is typically a {@link File} or {@link java.net.URL} * having the same name than the {@linkplain #input input} of this reader, but a different * extension. The new extension is determined from the {@code readerID} argument, which can * be: * * <ul> * <li><p>{@code "tfw"} for the <cite>World File</cite>. The extension of the returned * input may be {@code "tfw"} (most common), {@code "jgw"}, {@code "pgw"} or other * suffix depending on the extension of this reader {@linkplain #input input}, and * depending which file has been determined to exist. See the * <a href="#skip-navbar_top">class javadoc</a> for more details.</p></li> * * <li><p>{@code "prj"} for <cite>Map Projection</cite> file. The extension * of the returned input is {@code "prj"}.</p></li> * </ul> * * Subclasses can override this method for specifying a different main ({@code "main"}), * <cite>World File</cite> ({@code "tfw"}) or <cite>Map Projection</cite> ({@code "prj"}) * input. They can also invoke this method with other identifiers than the three above-cited * ones, in which case this method uses the given identifier as the extension of the returned * input. However the default {@code WorldFileImageReader} implementation uses only * {@code "main"}, {@code "tfw"} and {@code "prj"}. * * @param readerID {@code "main"} for the {@linkplain #main main} input, * {@code "tfw"} for the <cite>World File</cite> input, or * {@code "prj"} for the <cite>Map Projection</cite> input. Other * identifiers are allowed but subclass-specific. * @return The given kind of input typically as a {@link File} or {@link java.net.URL} * object, or {@code null} if there is no input for the given identifier. * @throws IOException If an error occurred while creating the input. * * @see WorldFileImageWriter#createOutput(String) */ @Override protected Object createInput(final String readerID) throws IOException { if ("main".equalsIgnoreCase(readerID)) { return super.createInput(readerID); } final ImageReaderSpi spi = originatingProvider; if ((spi instanceof Spi) && ((Spi) spi).exclude(readerID)) { return null; } return SupportFiles.changeExtension(input, readerID); } /** * Invokes {@link #createInput(String)} and verifies if the returned file exists. * If it does not exist, then returns {@code null}. * * @todo Current implementation checks only {@link File} object. * We should check URL as well. */ private Object getVerifiedInput(final String part) throws IOException { /* * Replaces the input by a File object if possible, * for allowing us to check if the file exists. */ if (!inputReplaced) { input = IOUtilities.tryToFile(input); inputReplaced = true; } Object in = createInput(part); if (in instanceof File) { if (!((File) in).isFile()) { in = null; } } return in; } /** * Creates a new stream or image metadata. This method first delegates to the main reader as * documented in the {@linkplain ImageReaderAdapter#createMetadata(int) super-class method}, * then completes the metadata with information read from the <cite>World File</cite> and * <cite>Map Projection</cite> files. * <p> * The <cite>World File</cite> and <cite>Map Projection</cite> files are determined by calls * to the {@link #createInput(String)} method with {@code "tfw"} and {@code "prj"} argument * values. Subclasses can override the later method if they want to specify different files * to be read. */ @Override protected SpatialMetadata createMetadata(final int imageIndex) throws IOException { SpatialMetadata metadata = super.createMetadata(imageIndex); if (imageIndex >= 0) { AffineTransform gridToCRS = null; CoordinateReferenceSystem crs = null; Object in = getVerifiedInput("tfw"); if (in != null) { gridToCRS = SupportFiles.parseTFW(IOUtilities.open(in), in); } in = getVerifiedInput("prj"); if (in != null) { crs = PrjFiles.read(IOUtilities.open(in), true); } /* * If we have found information in TFW or PRJ files, complete metadata. */ if (gridToCRS != null || crs != null) { //-- if exist some metadata from sub reader complete them, else create new spatial metadata if (main instanceof SpatialImageReader) { metadata = ((SpatialImageReader) main).getImageMetadata(imageIndex); } else { metadata = new SpatialMetadata(false, this, null); } if (gridToCRS != null) { final int width = getWidth (imageIndex); final int height = getHeight(imageIndex); new GridDomainAccessor(metadata).setAll(gridToCRS, new Rectangle(width, height), null, PixelOrientation.UPPER_LEFT); } if (crs != null) { new ReferencingBuilder(metadata).setCoordinateReferenceSystem(crs); } } } return metadata; } /** * Closes the input streams created by this reader. This method is automatically * invoked when a new input is set, or when the reader is reset or disposed. */ @Override protected void close() throws IOException { inputReplaced = false; super.close(); } /** * Service provider interface (SPI) for {@code WorldFileImageReader}s. This provider wraps * an other provider (typically for the TIFF, JPEG or PNG formats), which shall be specified * at construction time. The legal {@linkplain #inputTypes input types} are {@link String}, * {@link File}, {@link java.net.URI} and {@link java.net.URL} in order to allow the image * reader to infer the <cite>World File</cite> ({@code ".tfw"}) and <cite>Map Projection</cite> * ({@code ".prj"}) files from the image input file. * * {@section Plugins registration} * At the difference of other {@code ImageReader} plugins, the {@code WorldFileImageReader} * plugin is not automatically registered in the JVM. This is because there is many plugins * to register (one instance of this {@code Spi} class for each format to wrap), and because * attempts to get an {@code ImageReader} to wrap while {@link IIORegistry} is scanning the * classpath for services cause an infinite loop. To enable the <cite>World File</cite> plugins, * users must invoke {@link #registerDefaults(ServiceRegistry)} explicitly. * * @author Martin Desruisseaux (Geomatys) * @version 3.20 * * @see WorldFileImageWriter.Spi * * @since 3.08 (derived from 3.07) * @module */ public static class Spi extends ImageReaderAdapter.Spi { /** * The suffix added to format names and MIME types. * * @since 3.20 */ static final String NAME_SUFFIX = "-wf"; /** * The value to be returned by {@link #getModifiedInformation(Object)}. */ static final Set<InformationType> INFO = Collections.singleton(InformationType.IMAGE_METADATA); /** * Creates a provider which will use the given format for reading pixel values. * * @param main The provider of the readers to use for reading the pixel values. */ public Spi(final ImageReaderSpi main) { super(main); pluginClassName = "org.geotoolkit.image.io.plugin.WorldFileImageReader"; addFormatNameSuffix(NAME_SUFFIX); addExtraMetadataFormat(GEOTK_FORMAT_NAME, false, true); } /** * Creates a provider which will use the given format for reading pixel values. * This is a convenience constructor for the above constructor with a provider * fetched from the given format name. * * @param format The name of the provider to use for reading the pixel values. * @throws IllegalArgumentException If no provider is found for the given format. */ public Spi(final String format) throws IllegalArgumentException { this(Formats.getReaderByFormatName(format, Spi.class)); } /** * Creates a provider which will use the given format for reading pixel values. * * @param format The name of the provider to use for reading the pixel values. * @param writerSpiName The fully qualified class name of a provider for a writer that * can write the files expected by this reader format, or {@code null} if none. * @throws IllegalArgumentException If no provider is found for the given format. * * @since 3.21 */ protected Spi(final String format, final String writerSpiName) throws IllegalArgumentException { this(format); writerSpiNames = new String[] {writerSpiName}; } /** * Returns a brief, human-readable description of this service provider. * * @param locale The locale for which the return value should be localized. * @return A description of this service provider. */ @Override public String getDescription(final Locale locale) { return Vocabulary.getResources(locale).getString( Vocabulary.Keys.ImageCodecWithWorldFile_2, 0, Formats.getDisplayName(main)); } /** * Returns {@code true} if the given type of file should be excluded. The {@code readerID} * argument is either {@code "tfw"} or {@code "prj"}. If this method returns {@code true} * for the given ID, no attempt to read the corresponding file will be made. * <p> * This is useful for excluding attempts to read the TFW file when the information * is already provided in the main reader, as for example in the ASCII-Grid format. * * @param readerID Identifier of the reader for which an input is needed. * @return {@code true} if no attempt to read the corresponding file should be made. */ boolean exclude(final String readerID) { return false; } /** * Checks if the TFW or PRJ file exists for the given input. If we can not determine * the file existence, conservatively returns {@code false}. This happen typically if * the input is a URL. By returning {@code false} in the later case, we prevent the * world file reader to be selected, but it also avoid the risk of getting an * {@link IOException} when the reader will attempt to open a connection for the * PRJ or TFW URLs. */ private boolean exists(final Object input, final String readerID) throws IOException { if (!exclude(readerID)) { final Object derived = SupportFiles.changeExtension(input, readerID); if (derived instanceof File) { return ((File) derived).isFile(); } if (derived instanceof Path) { return Files.isRegularFile((Path) derived); } } return false; } /** * Returns {@code true} if the supplied source object appears to be of the format supported * by this reader. The default implementation checks if at least one of the {@code ".tfw} * (actual extension may vary) or {@code ".prj"} file is presents, then delegates to the * super-class method. * * @param source The input (typically a {@link File}) to be decoded. * @return {@code true} if it is likely that the file can be decoded. * @throws IOException If an error occurred while reading the file. */ @Override public boolean canDecodeInput(Object source) throws IOException { if (IOUtilities.canProcessAsPath(source)) { source = IOUtilities.tryToPath(source); if (exists(source, "tfw") || exists(source, "prj")) { return super.canDecodeInput(source); } } return false; } /** * Returns the kind of information that this wrapper will add or modify compared to the * {@linkplain #main} reader. * * @param source The input (typically a {@link File}) to be decoded. * @return The set of information to be read or modified by this adapter. * @throws IOException If an error occurred while reading the file. * * @since 3.20 */ @Override public Set<InformationType> getModifiedInformation(final Object source) throws IOException { return INFO; } /** * Creates a new <cite>World File</cite> reader. The {@code extension} argument * is forwarded to the {@linkplain #main main} provider with no change. * * @param extension A plug-in specific extension object, or {@code null}. * @return A new reader. * @throws IOException If the reader can not be created. */ @Override public ImageReader createReaderInstance(final Object extension) throws IOException { return new WorldFileImageReader(this, main.createReaderInstance(extension)); } /** * Registers a default set of <cite>World File</cite> formats. This method shall be invoked * at least once by client application before to use Image I/O library if they wish to decode * <cite>World File</cite> images. This method can also be invoked more time if the PNG, TIFF * or other standard readers changed, and this change needs to be taken in account by the * <cite>World File</cite> readers. See the <cite>System initialization</cite> section in * the <a href="../package-summary.html#package_description">package description</a> * for more information. * <p> * The current implementation registers plugins for the TIFF, JPEG, PNG, GIF, BMP, * matrix and ASCII-Grid ({@code ".prj"} file only) formats, but this list can be * augmented in any future Geotk version. * * @param registry The registry where to register the formats, or {@code null} for * the {@linkplain IIORegistry#getDefaultInstance() default registry}. * * @see org.geotoolkit.image.jai.Registry#setDefaultCodecPreferences() * @see org.geotoolkit.lang.Setup */ @Configuration public static void registerDefaults(ServiceRegistry registry) { if (registry == null) { registry = IIORegistry.getDefaultInstance(); } for (int index=0; ;index++) { final Spi provider; try { switch (index) { case 0: provider = new JPEG (); break; case 1: provider = new PNG (); break; case 2: provider = new GIF (); break; case 3: provider = new BMP (); break; case 4: provider = new TXT (); break; case 5: provider = new ASC (); break; case 6: provider = new Records(); break; default: return; } } catch (RuntimeException e) { /* * If we failed to register a plugin, this is not really a big deal. * This format will not be available, but it will not prevent the * rest of the application to work. */ Logging.recoverableException(Logging.getLogger("org.geotoolkit.image.io"), Spi.class, "registerDefaults", e); continue; } registry.registerServiceProvider(provider, ImageReaderSpi.class); registry.setOrdering(ImageReaderSpi.class, provider, provider.main); } } /** * Unregisters the providers registered by {@link #registerDefaults(ServiceRegistry)}. * * @param registry The registry from which to unregister the formats, or {@code null} * for the {@linkplain IIORegistry#getDefaultInstance() default registry}. * * @see org.geotoolkit.lang.Setup */ @Configuration public static void unregisterDefaults(ServiceRegistry registry) { if (registry == null) { registry = IIORegistry.getDefaultInstance(); } for (int index=0; ;index++) { final Class<? extends Spi> type; switch (index) { case 0: type = JPEG .class; break; case 1: type = PNG .class; break; case 2: type = GIF .class; break; case 3: type = BMP .class; break; case 4: type = TXT .class; break; case 5: type = ASC .class; break; case 6: type = Records.class; break; default: return; } final Spi provider = registry.getServiceProviderByClass(type); if (provider != null) { registry.deregisterServiceProvider(provider, ImageReaderSpi.class); } } } } /** * Providers for common formats. Each provider needs to be a different class because * {@link ServiceRegistry} allows the registration of only one instance of each class. */ private static final class JPEG extends Spi {JPEG() {super("JPEG", "org.geotoolkit.image.io.plugin.WorldFileImageWriter$JPEG");}} private static final class PNG extends Spi { PNG() {super("PNG", "org.geotoolkit.image.io.plugin.WorldFileImageWriter$PNG");}} private static final class GIF extends Spi { GIF() {super("GIF", "org.geotoolkit.image.io.plugin.WorldFileImageWriter$GIF");}} private static final class BMP extends Spi { BMP() {super("BMP", "org.geotoolkit.image.io.plugin.WorldFileImageWriter$BMP");}} private static final class TXT extends Spi { TXT() {super("matrix", "org.geotoolkit.image.io.plugin.WorldFileImageWriter$TXT");}} private static final class ASC extends Spi { ASC() {super("ASCII-Grid", "org.geotoolkit.image.io.plugin.WorldFileImageWriter$ASC");} @Override boolean exclude(final String readerID) { return "tfw".equalsIgnoreCase(readerID); } } private static final class Records extends Spi { Records() { super("records"); } @Override boolean exclude(final String readerID) { return "tfw".equalsIgnoreCase(readerID); } } }