/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2005-2015, 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.gce.geotiff; import it.geosolutions.imageio.plugins.tiff.TIFFImageWriteParam; import it.geosolutions.imageioimpl.plugins.tiff.TIFFImageMetadata; import it.geosolutions.imageioimpl.plugins.tiff.TIFFImageWriter; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.awt.image.RenderedImage; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStream; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.IIOException; import javax.imageio.IIOImage; import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.metadata.IIOInvalidTreeException; import javax.imageio.metadata.IIOMetadata; import javax.imageio.stream.ImageOutputStream; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.io.AbstractGridCoverageWriter; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.coverage.grid.io.imageio.GeoToolsWriteParams; import org.geotools.coverage.grid.io.imageio.geotiff.CRS2GeoTiffMetadataAdapter; import org.geotools.coverage.grid.io.imageio.geotiff.GeoTiffConstants; import org.geotools.coverage.grid.io.imageio.geotiff.GeoTiffException; import org.geotools.coverage.grid.io.imageio.geotiff.GeoTiffIIOMetadataEncoder; import org.geotools.data.DataUtilities; import org.geotools.data.WorldFileWriter; import org.geotools.factory.Hints; import org.geotools.image.io.GridCoverageWriterProgressAdapter; import org.geotools.image.io.ImageIOExt; import org.geotools.parameter.Parameter; import org.geotools.referencing.CRS; import org.geotools.referencing.CRS.AxisOrder; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.util.logging.Logging; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Parent; import org.jdom.input.DOMBuilder; import org.jdom.output.DOMOutputter; import org.opengis.coverage.grid.Format; import org.opengis.coverage.grid.GridCoverage; import org.opengis.coverage.grid.GridCoverageWriter; import org.opengis.parameter.GeneralParameterValue; import org.opengis.referencing.ReferenceIdentifier; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.crs.ProjectedCRS; import org.opengis.util.ProgressListener; /** * {@link AbstractGridCoverageWriter} implementation for the geotiff format. * * @author Simone Giannecchini, GeoSolutions SAS * */ public class GeoTiffWriter extends AbstractGridCoverageWriter implements GridCoverageWriter { private final static Logger LOGGER= Logging.getLogger(GeoTiffWriter.class); private final Map<String, String> metadataKeyValue = new HashMap<String, String>(); /** * Constructor for a {@link GeoTiffWriter}. * * @param destination * @throws IOException */ public GeoTiffWriter(Object destination) throws IOException { this(destination, null); } /** * Allows to setup metadata by leveraging on Ascii TIFF Tags. * @param name is the Ascii TIFF Tag identifier. * It can be a String representing: * 1) a simple Integer (referring to a tag ID) (in that case it will refer to the BaselineTIFFTagSet * 2) OR an identifier in the form: * TIFFTagSet:TIFFTagID. As an instance: "BaselineTIFFTagSet:305" in order to add the Copyright info. * @param value is the value to be assigned to that tag. * @see GeoTiffIIOMetadataEncoder.TagSet */ @Override public void setMetadataValue(String name, String value) throws IOException { if (name != null && name.length() > 0){ metadataKeyValue.put(name, value); } } /** * Constructor for a {@link GeoTiffWriter}. * * @param destination * @param hints * @throws IOException */ public GeoTiffWriter(Object destination, Hints hints) throws IOException { this.destination = destination; if (destination instanceof File) this.outStream = ImageIOExt.createImageOutputStream(null, destination); else if (destination instanceof URL) { final URL dest = (URL) destination; if (dest.getProtocol().equalsIgnoreCase("file")) { final File destFile = DataUtilities.urlToFile(dest); this.outStream = ImageIOExt.createImageOutputStream(null, destFile); } } else if (destination instanceof OutputStream) { this.outStream = ImageIOExt .createImageOutputStream(null, (OutputStream) destination); } else if (destination instanceof ImageOutputStream) this.outStream = (ImageOutputStream) destination; else throw new IllegalArgumentException( "The provided destination canno be used!"); // // // // managing hints // // // if (this.hints == null) this.hints= new Hints(); if (hints != null) { // prevent the use from reordering axes hints.remove(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER); this.hints.add(hints); this.hints.add(new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE)); } } /* * (non-Javadoc) * * @see org.opengis.coverage.grid.GridCoverageWriter#getFormat() */ public Format getFormat() { return new GeoTiffFormat(); } /* * (non-Javadoc) * * @see org.opengis.coverage.grid.GridCoverageWriter#write(org.opengis.coverage.grid.GridCoverage, * org.opengis.parameter.GeneralParameterValue[]) */ @SuppressWarnings("rawtypes") public void write(final GridCoverage gc, final GeneralParameterValue[] params) throws IllegalArgumentException, IOException, IndexOutOfBoundsException { GeoToolsWriteParams gtParams = null; boolean writeTfw=GeoTiffFormat.WRITE_TFW.getDefaultValue(); ProgressListener listener=null; boolean retainAxesOrder = false; if (params != null) { // ///////////////////////////////////////////////////////////////////// // // Checking params // // ///////////////////////////////////////////////////////////////////// if (params != null) { Parameter<?> param; final int length = params.length; for (int i = 0; i < length; i++) { param = (Parameter) params[i]; final ReferenceIdentifier name=param.getDescriptor().getName(); if (name.equals(AbstractGridFormat.GEOTOOLS_WRITE_PARAMS.getName())) { gtParams = (GeoToolsWriteParams) param.getValue(); continue; } if (name.equals(GeoTiffFormat.WRITE_TFW.getName())){ writeTfw = (Boolean) param.getValue(); continue; } if (name.equals(GeoTiffFormat.PROGRESS_LISTENER.getName())) { listener = (ProgressListener) param.getValue(); continue; } if (name.equals(GeoTiffFormat.RETAIN_AXES_ORDER.getName())) { retainAxesOrder = (Boolean) param.getValue(); continue; } } } } if (gtParams == null) gtParams = new GeoTiffWriteParams(); // // getting the coordinate reference system // final GridGeometry2D gg = (GridGeometry2D) gc.getGridGeometry(); GridEnvelope2D range = gg.getGridRange2D(); final Rectangle sourceRegion = gtParams.getSourceRegion(); if (sourceRegion != null){ range = new GridEnvelope2D(sourceRegion); } final AffineTransform tr = (AffineTransform) gg.getGridToCRS2D(); final CoordinateReferenceSystem crs = gg.getCoordinateReferenceSystem2D(); // // we handle just projected and geographic crs // if (!(crs instanceof ProjectedCRS || crs instanceof GeographicCRS)){ throw new GeoTiffException( null, "The supplied grid coverage uses an unsupported crs! You are allowed to use only projected and geographic coordinate reference systems", null); } // creating geotiff metadata final CRS2GeoTiffMetadataAdapter adapter = new CRS2GeoTiffMetadataAdapter(crs); final GeoTiffIIOMetadataEncoder metadata = adapter.parseCoordinateReferenceSystem(); // setting georeferencing setGeoReference(crs, metadata, tr, range,retainAxesOrder); // handling noData final double inNoData = CoverageUtilities.getBackgroundValues((GridCoverage2D) gc)[0]; if (!Double.isNaN(inNoData)) metadata.setNoData(inNoData); if (metadataKeyValue != null && !metadataKeyValue.isEmpty()) { metadata.setTiffTagsMetadata(metadataKeyValue); } // // write image // writeImage(((GridCoverage2D) gc).getRenderedImage(), this.outStream, metadata, gtParams, listener); // // write tfw // if (writeTfw && (destination instanceof File)) { handleTFW(gc, tr); } } /** * Takes care of writing the world file for this geotiff * * @param gc the {@link GridCoverage} to take the georefeerincing from. * @param tr * * @throws IOException in case something bad occurs while writing. */ private void handleTFW(final GridCoverage gc, AffineTransform tr) throws IOException { final File destFile = (File) this.destination; final File parentFile = destFile.getParentFile(); final String name = destFile.getName(); final File tfw = new File(parentFile, name.replace("tif", "tfw")); final File prj = new File(parentFile, name.replace("tif", "prj")); final WorldFileWriter writer = new WorldFileWriter(tfw, tr); final BufferedWriter outW = new BufferedWriter(new FileWriter(prj)); try { outW.write(gc.getCoordinateReferenceSystem().toWKT()); } finally { try { outW.close(); } catch (Exception e) { // ssshhh :) } } } /** * This method is used to set the tie point and the scale parameters for the * GeoTiff file we are writing or the ModelTransformation in case a more * general {@link AffineTransform} is needed to represent the raster space * to model space transform. * * <p> * This method works regardles of the nature fo the crs without making any * assumptions on the order or the direction of the axes, but checking them * from the supplied CRS. * * @see {@link http://lists.maptools.org/pipermail/geotiff/2006-January/000213.html} * @see {@http://lists.maptools.org/pipermail/geotiff/2006-January/000212.html} * @param crs * The {@link CoordinateReferenceSystem} of the * {@link GridCoverage2D} to encode. * @param metadata * where to set the georeferencing information. * @param range * that describes the raster space for this geotiff. * @param rasterToModel * describes the {@link AffineTransform} between raster space and * model space. * @param retainAxesOrder <code>true</code> in case we want to retain the axes order, <code>false</code> otherwise for lon-lat enforcing. * * @throws IOException in case something bad happens during the write operation. */ private static void setGeoReference(final CoordinateReferenceSystem crs, final GeoTiffIIOMetadataEncoder metadata, final AffineTransform rasterToModel, GridEnvelope2D range, boolean retainAxesOrder) throws IOException { // // We have to set an affine transformation which is going to be 2D // since we support baseline GeoTiff. // final AffineTransform modifiedRasterToModel = new AffineTransform(rasterToModel); // move the internal grid to world to corner from center modifiedRasterToModel.concatenate(CoverageUtilities.CENTER_TO_CORNER);; int minx = range.getLow(0), miny = range.getLow(1); if (minx != 0 || miny != 0) { // // // // Preconcatenate a transform to have raster space beginning at // (0,0) as this is not captured by the TIFF spec // // // modifiedRasterToModel.concatenate(AffineTransform.getTranslateInstance(minx, miny)); } // // Setting raster type to pixel corner since that is the default for geotiff // and makes most software happy // metadata.addGeoShortParam(GeoTiffConstants.GTRasterTypeGeoKey,GeoTiffConstants.RasterPixelIsArea); // // AXES Swap Management // // we need to understand how the axes of this gridcoverage are // specified, trying to understand the direction of the first axis in // order to correctly use transformations. // boolean swapAxes = XAffineTransform.getSwapXY(modifiedRasterToModel) == -1 || CRS.getAxisOrder(crs).equals(AxisOrder.NORTH_EAST); swapAxes&=!retainAxesOrder; // // Deciding how to save the georef with respect to the CRS. // // Notice that if we were asked to retain the axes order we don't swap axes! // if(swapAxes){ modifiedRasterToModel.preConcatenate(CoverageUtilities.AXES_SWAP); } metadata.setModelTransformation(modifiedRasterToModel); } /** * Writes the provided rendered image to the provided image output stream * using the supplied geotiff metadata. * * @param gtParams * @param listener */ private boolean writeImage(final RenderedImage image, final ImageOutputStream outputStream, final GeoTiffIIOMetadataEncoder geoTIFFMetadata, GeoToolsWriteParams gtParams, ProgressListener listener) throws IOException { if (image == null || outputStream == null) { throw new NullPointerException("Some input parameters are null"); } final ImageWriteParam params = gtParams.getAdaptee(); if (params instanceof TIFFImageWriteParam && gtParams instanceof GeoTiffWriteParams){ TIFFImageWriteParam param = (TIFFImageWriteParam) params; param.setForceToBigTIFF(((GeoTiffWriteParams)gtParams).isForceToBigTIFF()); } // // GETTING READER AND METADATA // final TIFFImageWriter writer = (TIFFImageWriter) GeoTiffFormat.IMAGEIO_WRITER_FACTORY.createWriterInstance(); final IIOMetadata metadata = createGeoTiffIIOMetadata(writer,ImageTypeSpecifier.createFromRenderedImage(image),geoTIFFMetadata, params); try{ // // IMAGEWRITE // writer.setOutput(outputStream); // listeners if(listener!=null){ final GridCoverageWriterProgressAdapter progressAdapter = new GridCoverageWriterProgressAdapter(listener); writer.addIIOWriteProgressListener(progressAdapter); writer.addIIOWriteWarningListener(progressAdapter); } writer.write(writer.getDefaultStreamMetadata(params), new IIOImage(image, null, metadata), params); outputStream.flush(); }finally{ try{ if (!(destination instanceof ImageOutputStream)&&outputStream!=null) outputStream.close(); }catch (Throwable e) { // eat me if(LOGGER.isLoggable(Level.WARNING)){ LOGGER.log(Level.WARNING,e.getLocalizedMessage(),e); } } try{ if (writer!=null) writer.dispose(); }catch (Throwable e) { // eat me if(LOGGER.isLoggable(Level.WARNING)){ LOGGER.log(Level.WARNING,e.getLocalizedMessage(),e); } } } return true; } /** * Creates image metadata which complies to the GeoTIFFWritingUtilities * specification for the given image writer, image type and * GeoTIFFWritingUtilities metadata. * * @param writer * the image writer, must not be null * @param type * the image type, must not be null * @param geoTIFFMetadata * the GeoTIFFWritingUtilities metadata, must not be null * @param params * @return the image metadata, never null * @throws IIOException * if the metadata cannot be created */ public final static IIOMetadata createGeoTiffIIOMetadata( ImageWriter writer, ImageTypeSpecifier type, GeoTiffIIOMetadataEncoder geoTIFFMetadata, ImageWriteParam params) throws IIOException { IIOMetadata imageMetadata = writer .getDefaultImageMetadata(type, params); imageMetadata = writer .convertImageMetadata(imageMetadata, type, params); org.w3c.dom.Element w3cElement = (org.w3c.dom.Element) imageMetadata .getAsTree(GeoTiffConstants.GEOTIFF_IIO_METADATA_FORMAT_NAME); final Element element = new DOMBuilder().build(w3cElement); geoTIFFMetadata.assignTo(element); final Parent parent = element.getParent(); parent.removeContent(element); final Document document = new Document(element); try { final org.w3c.dom.Document w3cDoc = new DOMOutputter() .output(document); final IIOMetadata iioMetadata = new TIFFImageMetadata( TIFFImageMetadata.parseIFD(w3cDoc.getDocumentElement() .getFirstChild())); imageMetadata = iioMetadata; } catch (JDOMException e) { throw new IIOException( "Failed to set GeoTIFFWritingUtilities specific tags.", e); } catch (IIOInvalidTreeException e) { throw new IIOException( "Failed to set GeoTIFFWritingUtilities specific tags.", e); } return imageMetadata; } @Override public void dispose() { // release any metadata metadataKeyValue.clear(); // release father super.dispose(); } }