/* (c) 2017 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.wcs.responses; import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.io.OutputStream; import javax.media.jai.OpImage; import javax.media.jai.RenderedOp; import org.apache.commons.io.FileUtils; import org.geoserver.platform.ServiceException; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader; 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.gce.geotiff.GeoTiffFormat; import org.geotools.gce.geotiff.GeoTiffReader; import org.geotools.gce.geotiff.GeoTiffWriteParams; import org.geotools.geometry.GeneralEnvelope; import org.geotools.image.ImageWorker; import org.geotools.resources.image.ImageUtilities; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.geometry.Envelope; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.EngineeringCRS; /** * Support class setting up reasonable defaults on the write parameters and centralizing the write code and associated optimizations * * @author Andrea Aime - GeoSolutions */ public class GeoTiffWriterHelper { private final static GeoTiffFormat TIFF_FORMAT = new GeoTiffFormat(); private GridCoverage2D coverage; private File sourceFile; private GeoTiffWriteParams imageIoWriteParams; private ParameterValueGroup geotoolsWriteParams; public GeoTiffWriterHelper(GridCoverage2D coverage) throws IOException { this.coverage = coverage; // did we get lucky and all we need to do is to copy the original file file over? if (isUnprocessed(coverage)) { this.sourceFile = getSourceFile(coverage); } // setup default writing params, respect by default the original tiling structure // for optimal extraction performance this.imageIoWriteParams = buildWriteParams(coverage); this.geotoolsWriteParams = buildGeoToolsWriteParams(imageIoWriteParams); } /** * Returns the original source file, is present in the metadata, and if the coverage */ private File getSourceFile(GridCoverage2D coverage) { final Object fileSource = coverage .getProperty(AbstractGridCoverage2DReader.FILE_SOURCE_PROPERTY); if (fileSource != null && fileSource instanceof String) { File file = new File((String) fileSource); if (file.exists()) { GeoTiffReader reader = null; try { reader = new GeoTiffReader(file); GeneralEnvelope originalEnvelope = reader.getOriginalEnvelope(); Envelope envelope = coverage.getEnvelope(); if (originalEnvelope.equals(envelope, 1e-9, false)) { GridCoverage2D test = reader.read(null); ImageUtilities.disposeImage(test.getRenderedImage()); return file; } } catch (Exception e) { // ok, not a geotiff! } finally { if (reader != null) { reader.dispose(); } } } } return null; } private GeoTiffWriteParams buildWriteParams(GridCoverage2D coverage) { final RenderedImage renderedImage = coverage.getRenderedImage(); int tileWidth = renderedImage.getTileWidth(); int tileHeight = renderedImage.getTileHeight(); // avoid tiles bigger than the image final GridEnvelope gr = coverage.getGridGeometry().getGridRange(); if(gr.getSpan(0) < tileWidth) { tileWidth = gr.getSpan(0); } if(gr.getSpan(1) < tileHeight) { tileHeight = gr.getSpan(1); } GeoTiffWriteParams writeParams = new GeoTiffWriteParams(); writeParams.setTilingMode(GeoToolsWriteParams.MODE_EXPLICIT); writeParams.setTiling(tileWidth, tileHeight); return writeParams; } private ParameterValueGroup buildGeoToolsWriteParams(GeoTiffWriteParams writeParams) { final ParameterValueGroup wparams = TIFF_FORMAT.getWriteParameters(); wparams.parameter(AbstractGridFormat.GEOTOOLS_WRITE_PARAMS.getName().toString()) .setValue(writeParams); return wparams; } /** * Returns the write parameters, allowing their customization * * @return */ public GeoTiffWriteParams getImageIoWriteParams() { return imageIoWriteParams; } /** * Returns the GeoTools grid writer params, allowing their customization * @return */ public ParameterValueGroup getGeotoolsWriteParams() { return geotoolsWriteParams; } /** * The code can figure out if it's really just copying over a GeoTiff source file and run a straight file copy, call this method if the source * copy should be turned off */ public void disableSourceCopyOptimization() { this.sourceFile = null; } public void write(OutputStream stream) throws IOException { if(sourceFile != null) { FileUtils.copyFile(sourceFile, stream); } else { CoordinateReferenceSystem crs = coverage.getCoordinateReferenceSystem(); boolean unreferenced = crs == null || crs instanceof EngineeringCRS; if(unreferenced) { RenderedImage ri = coverage.getRenderedImage(); int tileWidth, tileHeight; if(imageIoWriteParams.getTilingMode() == GeoToolsWriteParams.MODE_EXPLICIT) { tileWidth = imageIoWriteParams.getTileWidth(); tileHeight = imageIoWriteParams.getTileHeight(); } else { tileWidth = ri.getTileWidth(); tileHeight = ri.getTileHeight(); } new ImageWorker(ri).writeTIFF(stream, null, 0.75f, tileWidth, tileHeight); } else { final GeneralParameterValue[] wps = (GeneralParameterValue[]) geotoolsWriteParams.values() .toArray(new GeneralParameterValue[geotoolsWriteParams.values().size()]); // write out the coverage AbstractGridCoverageWriter writer = (AbstractGridCoverageWriter) TIFF_FORMAT .getWriter(stream); if (writer == null) throw new ServiceException( "Could not find the GeoTIFF writer, please check it's in the classpath"); try { writer.write(coverage, wps); } finally { try { writer.dispose(); } catch (Exception e) { // swallow, silent close } } } } } /** * Returns true if the coverage has not been processed in any way since it has been read * * @param coverage * */ private boolean isUnprocessed(GridCoverage2D coverage) { RenderedImage ri = coverage.getRenderedImage(); if (ri instanceof RenderedOp) { RenderedOp op = (RenderedOp) ri; return op.getOperationName().startsWith("ImageRead"); } else if (ri instanceof OpImage) { return ri.getClass().getSimpleName().startsWith("ImageRead"); } else { return true; } } }