/* * 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.io.OutputStream; import java.util.Locale; import java.util.Set; import java.awt.geom.AffineTransform; import javax.imageio.ImageWriter; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.spi.ServiceRegistry; import javax.imageio.metadata.IIOMetadata; import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageWriteParam; import javax.imageio.IIOException; import org.opengis.coverage.grid.RectifiedGrid; import org.opengis.referencing.crs.ImageCRS; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.geotoolkit.io.wkt.PrjFiles; import org.geotoolkit.image.io.InformationType; import org.geotoolkit.image.io.ImageWriterAdapter; import org.geotoolkit.image.io.metadata.MetadataHelper; import org.geotoolkit.image.io.metadata.SpatialMetadata; import org.geotoolkit.internal.image.io.SupportFiles; import org.geotoolkit.internal.image.io.Formats; import org.geotoolkit.nio.IOUtilities; import org.geotoolkit.lang.Configuration; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.resources.Errors; import org.apache.sis.util.logging.Logging; import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME; /** * Writer for the <cite>World File</cite> format. This writer wraps an other image writer * for an "ordinary" image format, like TIFF, PNG or JPEG. This {@code WorldFileImageWriter} * delegates the writing of pixel values to the wrapped writer, and additionally creates two * small text files in the same directory than the image file, with the same filename * but a different extension: * <p> * <ul> * <li>A text file containing the coefficients of the affine transform mapping pixel * coordinates to geodesic coordinates.</li> * <li>A text file containing the <cite>Coordinate Reference System</cite> (CRS) * definition in <cite>Well Known Text</cite> (WKT) syntax.</li> * </ul> * <p> * See {@link WorldFileImageReader} for more information about the name, content and encoding * of those files. * * @author Martin Desruisseaux (Geomatys) * @version 3.08 * * @see <a href="http://en.wikipedia.org/wiki/World_file">World File Format Description</a> * @see WorldFileImageReader * * @since 3.08 (derived from 3.07) * @module */ public class WorldFileImageWriter extends ImageWriterAdapter { /** * Constructs a new image writer. The provider argument is mandatory for this constructor. * If the provider is unknown, use the next constructor below instead. * * @param provider The {@link ImageWriterSpi} that is constructing this object. * @throws IOException If an error occurred while creating the {@linkplain #main main} writer. */ public WorldFileImageWriter(final Spi provider) throws IOException { super(provider); } /** * Constructs a new image writer wrapping the given writer. * * @param provider The {@link ImageWriterSpi} that is constructing this object, or {@code null}. * @param main The writer to use for writing the pixel values. */ public WorldFileImageWriter(final Spi provider, final ImageWriter main) { super(provider, main); } /** * Creates the output to be given to the writer identified by the given argument. If the * {@code writerID} argument is {@code "main"} (ignoring case), then this method delegates * to the {@linkplain ImageWriterAdapter#createOutput(String) super-class method}. Otherwise * this method returns an output which is typically a {@link File} or {@link java.net.URL} * having the same name than the {@linkplain #output output} of this writer, but a different * extension. The new extension is determined from the {@code writerID} argument, which can * be: * <p> * <ul> * <li>{@code "tfw"} for the <cite>World File</cite>.</li> * <li>{@code "prj"} for <cite>Map Projection</cite> file.</li> * </ul> * <p> * 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"}) * output. 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 * output. However the default {@code WorldFileImageWriter} implementation uses only * {@code "main"}, {@code "tfw"} and {@code "prj"}. * * @param writerID {@code "main"} for the {@linkplain #main main} output, * {@code "tfw"} for the <cite>World File</cite> output, or * {@code "prj"} for the <cite>Map Projection</cite> output. Other * identifiers are allowed but subclass-specific. * @return The given kind of output typically as a {@link File} or {@link java.net.URL} * object, or {@code null} if there is no output for the given identifier. * @throws IOException If an error occurred while creating the output. * * @see WorldFileImageReader#createInput(String) */ @Override protected Object createOutput(String writerID) throws IOException { if ("main".equalsIgnoreCase(writerID)) { return super.createOutput(writerID); } final ImageWriterSpi spi = originatingProvider; if ((spi instanceof Spi) && ((Spi) spi).exclude(writerID)) { return null; } final Object output = this.output; if ("tfw".equalsIgnoreCase(writerID)) { writerID = SupportFiles.toSuffixTFW(output); } return IOUtilities.changeExtension(output, writerID); } /** * Invoked by the {@code write} methods when image metadata needs to be written. * The default implementation writes the <cite>World File</cite> if an affine * transform can be build from the {@linkplain RectifiedGrid rectified grid domain}. */ @Override protected void writeImageMetadata(final IIOMetadata metadata, final int imageIndex, final ImageWriteParam param) throws IOException { if (imageIndex != 0) { throw new IIOException(Errors.getResources(locale).getString( Errors.Keys.IndexOutOfBounds_1, imageIndex)); } if (metadata instanceof SpatialMetadata) { final SpatialMetadata md = (SpatialMetadata) metadata; final RectifiedGrid rf = md.getInstanceForType(RectifiedGrid.class); if (rf != null) { final MetadataHelper mh = new MetadataHelper(md); final AffineTransform tr = mh.getAffineTransform(rf, param); final Object path = createOutput("tfw"); if (path != null) { try (OutputStream out = IOUtilities.openWrite(path)) { SupportFiles.writeTFW(out, tr); } } } /* * Write the CRS if non-null and not an instance of ImageCRS. The ImageCRS case is * excluded because it is the default CRS assigned by WorldFileImageReader when no * ".prj" file were found. */ final CoordinateReferenceSystem crs = md.getInstanceForType(CoordinateReferenceSystem.class); if (crs != null && !(crs instanceof ImageCRS)) { final Object path = createOutput("prj"); if (path != null) { try (OutputStream out = IOUtilities.openWrite(path)) { PrjFiles.write(crs, out); } } } } } /** * Service provider interface (SPI) for {@code WorldFileImageWriter}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 #outputTypes output types} are {@link String}, * {@link File}, {@link java.net.URI} and {@link java.net.URL} in order to allow the image * writer to infer the <cite>World File</cite> ({@code ".tfw"}) and <cite>Map Projection</cite> * ({@code ".prj"}) files from the image output file. * * {@section Plugins registration} * At the difference of other {@code ImageWriter} plugins, the {@code WorldFileImageWriter} * 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 ImageWriter} 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 WorldFileImageReader.Spi * * @since 3.08 (derived from 3.07) * @module */ public static class Spi extends ImageWriterAdapter.Spi { /** * Creates a provider which will use the given format for writing pixel values. * * @param main The provider of the writers to use for writing the pixel values. */ public Spi(final ImageWriterSpi main) { super(main); pluginClassName = "org.geotoolkit.image.io.plugin.WorldFileImageWriter"; addFormatNameSuffix(WorldFileImageReader.Spi.NAME_SUFFIX); addExtraMetadataFormat(GEOTK_FORMAT_NAME, false, true); } /** * Creates a provider which will use the given format for writing 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 writing the pixel values. * @throws IllegalArgumentException If no provider is found for the given format. */ public Spi(final String format) throws IllegalArgumentException { this(Formats.getWriterByFormatName(format, Spi.class)); } /** * Creates a provider which will use the given format for writing pixel values. * * @param format The name of the provider to use for writing the pixel values. * @param readerSpiName The fully qualified class name of a provider for a reader that * can read the files created by this writer 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 readerSpiName) throws IllegalArgumentException { this(format); readerSpiNames = new String[] {readerSpiName}; } /** * Returns a brief, human-writable 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, 1, Formats.getDisplayName(main)); } /** * Returns {@code true} if the given type of file should be excluded. The {@code writerID} * argument is either {@code "tfw"} or {@code "prj"}. If this method returns {@code true} * for the given ID, no attempt to write the corresponding file will be made. * <p> * This is useful for excluding attempts to write the TFW file when the information * is already provided in the main writer, as for example in the ASCII-Grid format. * * @param writerID Identifier of the writer for which an output is needed. * @return {@code true} if no attempt to write the corresponding file should be made. */ boolean exclude(final String writerID) { return false; } /** * Returns the kind of information that this wrapper will add or modify compared to the * {@linkplain #main} writer. * * @param type The layout of the image to be written. * @return The set of information to be written or modified by this adapter. * * @since 3.20 */ @Override public Set<InformationType> getModifiedInformation(final ImageTypeSpecifier type) { return WorldFileImageReader.Spi.INFO; } /** * Creates a new <cite>World File</cite> writer. 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 writer. * @throws IOException If the writer can not be created. */ @Override public ImageWriter createWriterInstance(final Object extension) throws IOException { return new WorldFileImageWriter(this, main.createWriterInstance(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 encode * <cite>World File</cite> images. This method can also be invoked more time if the PNG, TIFF * or other standard writers changed, and this change needs to be taken in account by the * <cite>World File</cite> writers. 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; 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, ImageWriterSpi.class); registry.setOrdering(ImageWriterSpi.class, provider.main, provider); /* * We give precedence to the standard writer. Note that this is the opposite * of what WorldFileImageReader.Spi does. We do that way because the writer * is required to accept ImageOutputStream - at the opposite of ImageReaders, * we have no way to filter writers according their output type. The standard * writer should be preferred when the output is a stream, because the adapter * would not be able to write the .prj and .tfw files anyway. */ } } /** * 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; default: return; } final Spi provider = registry.getServiceProviderByClass(type); if (provider != null) { registry.deregisterServiceProvider(provider, ImageWriterSpi.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.WorldFileImageReader$JPEG");}} private static final class PNG extends Spi { PNG() {super("PNG", "org.geotoolkit.image.io.plugin.WorldFileImageReader$PNG");}} private static final class GIF extends Spi { GIF() {super("GIF", "org.geotoolkit.image.io.plugin.WorldFileImageReader$GIF");}} private static final class BMP extends Spi { BMP() {super("BMP", "org.geotoolkit.image.io.plugin.WorldFileImageReader$BMP");}} private static final class TXT extends Spi { TXT() {super("matrix", "org.geotoolkit.image.io.plugin.WorldFileImageReader$TXT");}} private static final class ASC extends Spi { ASC() {super("ASCII-Grid", "org.geotoolkit.image.io.plugin.WorldFileImageReader$ASC");} @Override boolean exclude(final String readerID) { return "tfw".equalsIgnoreCase(readerID); } } }