| /* |
| * Copyright (c) 2005, 2016, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code 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 General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| package com.sun.imageio.plugins.tiff; |
| |
| import java.awt.Point; |
| import java.awt.Rectangle; |
| import java.awt.color.ColorSpace; |
| import java.awt.color.ICC_ColorSpace; |
| import java.awt.image.BufferedImage; |
| import java.awt.image.ColorModel; |
| import java.awt.image.ComponentSampleModel; |
| import java.awt.image.DataBuffer; |
| import java.awt.image.DataBufferByte; |
| import java.awt.image.IndexColorModel; |
| import java.awt.image.RenderedImage; |
| import java.awt.image.Raster; |
| import java.awt.image.SampleModel; |
| import java.awt.image.WritableRaster; |
| import java.io.EOFException; |
| import java.io.IOException; |
| import java.nio.ByteOrder; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import javax.imageio.IIOException; |
| import javax.imageio.IIOImage; |
| import javax.imageio.ImageWriteParam; |
| import javax.imageio.ImageWriter; |
| import javax.imageio.ImageTypeSpecifier; |
| import javax.imageio.metadata.IIOInvalidTreeException; |
| import javax.imageio.metadata.IIOMetadata; |
| import javax.imageio.metadata.IIOMetadataFormatImpl; |
| import javax.imageio.spi.ImageWriterSpi; |
| import javax.imageio.stream.ImageOutputStream; |
| import org.w3c.dom.Node; |
| import com.sun.imageio.plugins.common.ImageUtil; |
| import javax.imageio.plugins.tiff.BaselineTIFFTagSet; |
| import javax.imageio.plugins.tiff.ExifParentTIFFTagSet; |
| import javax.imageio.plugins.tiff.ExifTIFFTagSet; |
| import javax.imageio.plugins.tiff.TIFFField; |
| import javax.imageio.plugins.tiff.TIFFTag; |
| import javax.imageio.plugins.tiff.TIFFTagSet; |
| import com.sun.imageio.plugins.common.SimpleRenderedImage; |
| import com.sun.imageio.plugins.common.SingleTileRenderedImage; |
| import java.nio.charset.StandardCharsets; |
| |
| public class TIFFImageWriter extends ImageWriter { |
| |
| static final String EXIF_JPEG_COMPRESSION_TYPE = "Exif JPEG"; |
| |
| private static final int DEFAULT_BYTES_PER_STRIP = 8192; |
| |
| /** |
| * Supported TIFF compression types. |
| */ |
| static final String[] TIFFCompressionTypes = { |
| "CCITT RLE", |
| "CCITT T.4", |
| "CCITT T.6", |
| "LZW", |
| // "Old JPEG", |
| "JPEG", |
| "ZLib", |
| "PackBits", |
| "Deflate", |
| EXIF_JPEG_COMPRESSION_TYPE |
| }; |
| |
| // |
| // The lengths of the arrays 'compressionTypes', |
| // 'isCompressionLossless', and 'compressionNumbers' |
| // must be equal. |
| // |
| |
| /** |
| * Known TIFF compression types. |
| */ |
| static final String[] compressionTypes = { |
| "CCITT RLE", |
| "CCITT T.4", |
| "CCITT T.6", |
| "LZW", |
| "Old JPEG", |
| "JPEG", |
| "ZLib", |
| "PackBits", |
| "Deflate", |
| EXIF_JPEG_COMPRESSION_TYPE |
| }; |
| |
| /** |
| * Lossless flag for known compression types. |
| */ |
| static final boolean[] isCompressionLossless = { |
| true, // RLE |
| true, // T.4 |
| true, // T.6 |
| true, // LZW |
| false, // Old JPEG |
| false, // JPEG |
| true, // ZLib |
| true, // PackBits |
| true, // DEFLATE |
| false // Exif JPEG |
| }; |
| |
| /** |
| * Compression tag values for known compression types. |
| */ |
| static final int[] compressionNumbers = { |
| BaselineTIFFTagSet.COMPRESSION_CCITT_RLE, |
| BaselineTIFFTagSet.COMPRESSION_CCITT_T_4, |
| BaselineTIFFTagSet.COMPRESSION_CCITT_T_6, |
| BaselineTIFFTagSet.COMPRESSION_LZW, |
| BaselineTIFFTagSet.COMPRESSION_OLD_JPEG, |
| BaselineTIFFTagSet.COMPRESSION_JPEG, |
| BaselineTIFFTagSet.COMPRESSION_ZLIB, |
| BaselineTIFFTagSet.COMPRESSION_PACKBITS, |
| BaselineTIFFTagSet.COMPRESSION_DEFLATE, |
| BaselineTIFFTagSet.COMPRESSION_OLD_JPEG, // Exif JPEG |
| }; |
| |
| private ImageOutputStream stream; |
| private long headerPosition; |
| private RenderedImage image; |
| private ImageTypeSpecifier imageType; |
| private ByteOrder byteOrder; |
| private ImageWriteParam param; |
| private TIFFCompressor compressor; |
| private TIFFColorConverter colorConverter; |
| |
| private TIFFStreamMetadata streamMetadata; |
| private TIFFImageMetadata imageMetadata; |
| |
| private int sourceXOffset; |
| private int sourceYOffset; |
| private int sourceWidth; |
| private int sourceHeight; |
| private int[] sourceBands; |
| private int periodX; |
| private int periodY; |
| |
| private int bitDepth; // bits per channel |
| private int numBands; |
| private int tileWidth; |
| private int tileLength; |
| private int tilesAcross; |
| private int tilesDown; |
| |
| private int[] sampleSize = null; // Input sample size per band, in bits |
| private int scalingBitDepth = -1; // Output bit depth of the scaling tables |
| private boolean isRescaling = false; // Whether rescaling is needed. |
| |
| private boolean isBilevel; // Whether image is bilevel |
| private boolean isImageSimple; // Whether image can be copied into directly |
| private boolean isInverted; // Whether photometric inversion is required |
| |
| private boolean isTiled; // Whether the image is tiled (true) or stipped (false). |
| |
| private int nativePhotometricInterpretation; |
| private int photometricInterpretation; |
| |
| private char[] bitsPerSample; // Output sample size per band |
| private int sampleFormat = |
| BaselineTIFFTagSet.SAMPLE_FORMAT_UNDEFINED; // Output sample format |
| |
| // Tables for 1, 2, 4, or 8 bit output |
| private byte[][] scale = null; // 8 bit table |
| private byte[] scale0 = null; // equivalent to scale[0] |
| |
| // Tables for 16 bit output |
| private byte[][] scaleh = null; // High bytes of output |
| private byte[][] scalel = null; // Low bytes of output |
| |
| private int compression; |
| private int predictor; |
| |
| private int totalPixels; |
| private int pixelsDone; |
| |
| private long nextIFDPointerPos; |
| |
| // Next available space. |
| private long nextSpace = 0L; |
| |
| private long prevStreamPosition; |
| private long prevHeaderPosition; |
| private long prevNextSpace; |
| |
| // Whether a sequence is being written. |
| private boolean isWritingSequence = false; |
| private boolean isInsertingEmpty = false; |
| private boolean isWritingEmpty = false; |
| |
| private int currentImage = 0; |
| |
| /** |
| * Converts a pixel's X coordinate into a horizontal tile index |
| * relative to a given tile grid layout specified by its X offset |
| * and tile width. |
| * |
| * <p> If {@code tileWidth < 0}, the results of this method |
| * are undefined. If {@code tileWidth == 0}, an |
| * {@code ArithmeticException} will be thrown. |
| * |
| * @throws ArithmeticException If {@code tileWidth == 0}. |
| */ |
| public static int XToTileX(int x, int tileGridXOffset, int tileWidth) { |
| x -= tileGridXOffset; |
| if (x < 0) { |
| x += 1 - tileWidth; // force round to -infinity (ceiling) |
| } |
| return x/tileWidth; |
| } |
| |
| /** |
| * Converts a pixel's Y coordinate into a vertical tile index |
| * relative to a given tile grid layout specified by its Y offset |
| * and tile height. |
| * |
| * <p> If {@code tileHeight < 0}, the results of this method |
| * are undefined. If {@code tileHeight == 0}, an |
| * {@code ArithmeticException} will be thrown. |
| * |
| * @throws ArithmeticException If {@code tileHeight == 0}. |
| */ |
| public static int YToTileY(int y, int tileGridYOffset, int tileHeight) { |
| y -= tileGridYOffset; |
| if (y < 0) { |
| y += 1 - tileHeight; // force round to -infinity (ceiling) |
| } |
| return y/tileHeight; |
| } |
| |
| public TIFFImageWriter(ImageWriterSpi originatingProvider) { |
| super(originatingProvider); |
| } |
| |
| public ImageWriteParam getDefaultWriteParam() { |
| return new TIFFImageWriteParam(getLocale()); |
| } |
| |
| public void setOutput(Object output) { |
| if (output != null) { |
| if (!(output instanceof ImageOutputStream)) { |
| throw new IllegalArgumentException |
| ("output not an ImageOutputStream!"); |
| } |
| |
| // reset() must precede setOutput() as it sets output to null |
| reset(); |
| |
| this.stream = (ImageOutputStream)output; |
| |
| // |
| // The output is expected to be positioned at a TIFF header |
| // or at some arbitrary location which may or may not be |
| // the EOF. In the former case the writer should be able |
| // either to overwrite the existing sequence or append to it. |
| // |
| |
| // Set the position of the header and the next available space. |
| try { |
| headerPosition = this.stream.getStreamPosition(); |
| try { |
| // Read byte order and magic number. |
| byte[] b = new byte[4]; |
| stream.readFully(b); |
| |
| // Check bytes for TIFF header. |
| if((b[0] == (byte)0x49 && b[1] == (byte)0x49 && |
| b[2] == (byte)0x2a && b[3] == (byte)0x00) || |
| (b[0] == (byte)0x4d && b[1] == (byte)0x4d && |
| b[2] == (byte)0x00 && b[3] == (byte)0x2a)) { |
| // TIFF header. |
| this.nextSpace = stream.length(); |
| } else { |
| // Neither TIFF header nor EOF: overwrite. |
| this.nextSpace = headerPosition; |
| } |
| } catch(IOException io) { // thrown by readFully() |
| // At EOF or not at a TIFF header. |
| this.nextSpace = headerPosition; |
| } |
| stream.seek(headerPosition); |
| } catch(IOException ioe) { // thrown by getStreamPosition() |
| // Assume it's at zero. |
| this.nextSpace = headerPosition = 0L; |
| } |
| } else { |
| this.stream = null; |
| } |
| |
| super.setOutput(output); |
| } |
| |
| public IIOMetadata |
| getDefaultStreamMetadata(ImageWriteParam param) { |
| return new TIFFStreamMetadata(); |
| } |
| |
| public IIOMetadata |
| getDefaultImageMetadata(ImageTypeSpecifier imageType, |
| ImageWriteParam param) { |
| |
| List<TIFFTagSet> tagSets = new ArrayList<TIFFTagSet>(1); |
| tagSets.add(BaselineTIFFTagSet.getInstance()); |
| TIFFImageMetadata imageMetadata = new TIFFImageMetadata(tagSets); |
| |
| if(imageType != null) { |
| TIFFImageMetadata im = |
| (TIFFImageMetadata)convertImageMetadata(imageMetadata, |
| imageType, |
| param); |
| if(im != null) { |
| imageMetadata = im; |
| } |
| } |
| |
| return imageMetadata; |
| } |
| |
| public IIOMetadata convertStreamMetadata(IIOMetadata inData, |
| ImageWriteParam param) { |
| // Check arguments. |
| if(inData == null) { |
| throw new NullPointerException("inData == null!"); |
| } |
| |
| // Note: param is irrelevant as it does not contain byte order. |
| |
| TIFFStreamMetadata outData = null; |
| if(inData instanceof TIFFStreamMetadata) { |
| outData = new TIFFStreamMetadata(); |
| outData.byteOrder = ((TIFFStreamMetadata)inData).byteOrder; |
| return outData; |
| } else if(Arrays.asList(inData.getMetadataFormatNames()).contains( |
| TIFFStreamMetadata.NATIVE_METADATA_FORMAT_NAME)) { |
| outData = new TIFFStreamMetadata(); |
| String format = TIFFStreamMetadata.NATIVE_METADATA_FORMAT_NAME; |
| try { |
| outData.mergeTree(format, inData.getAsTree(format)); |
| } catch(IIOInvalidTreeException e) { |
| return null; |
| } |
| } |
| |
| return outData; |
| } |
| |
| public IIOMetadata |
| convertImageMetadata(IIOMetadata inData, |
| ImageTypeSpecifier imageType, |
| ImageWriteParam param) { |
| // Check arguments. |
| if(inData == null) { |
| throw new NullPointerException("inData == null!"); |
| } |
| if(imageType == null) { |
| throw new NullPointerException("imageType == null!"); |
| } |
| |
| TIFFImageMetadata outData = null; |
| |
| // Obtain a TIFFImageMetadata object. |
| if(inData instanceof TIFFImageMetadata) { |
| // Create a new metadata object from a clone of the input IFD. |
| TIFFIFD inIFD = ((TIFFImageMetadata)inData).getRootIFD(); |
| outData = new TIFFImageMetadata(inIFD.getShallowClone()); |
| } else if(Arrays.asList(inData.getMetadataFormatNames()).contains( |
| TIFFImageMetadata.NATIVE_METADATA_FORMAT_NAME)) { |
| // Initialize from the native metadata form of the input tree. |
| try { |
| outData = convertNativeImageMetadata(inData); |
| } catch(IIOInvalidTreeException e) { |
| return null; |
| } |
| } else if(inData.isStandardMetadataFormatSupported()) { |
| // Initialize from the standard metadata form of the input tree. |
| try { |
| outData = convertStandardImageMetadata(inData); |
| } catch(IIOInvalidTreeException e) { |
| return null; |
| } |
| } |
| |
| // Update the metadata per the image type and param. |
| if(outData != null) { |
| TIFFImageWriter bogusWriter = |
| new TIFFImageWriter(this.originatingProvider); |
| bogusWriter.imageMetadata = outData; |
| bogusWriter.param = param; |
| SampleModel sm = imageType.getSampleModel(); |
| try { |
| bogusWriter.setupMetadata(imageType.getColorModel(), sm, |
| sm.getWidth(), sm.getHeight()); |
| return bogusWriter.imageMetadata; |
| } catch(IIOException e) { |
| return null; |
| } |
| } |
| |
| return outData; |
| } |
| |
| /** |
| * Converts a standard {@code javax_imageio_1.0} tree to a |
| * {@code TIFFImageMetadata} object. |
| * |
| * @param inData The metadata object. |
| * @return a {@code TIFFImageMetadata} or {@code null} if |
| * the standard tree derived from the input object is {@code null}. |
| * @throws IllegalArgumentException if {@code inData} is |
| * {@code null}. |
| * @throws IllegalArgumentException if {@code inData} does not support |
| * the standard metadata format. |
| * @throws IIOInvalidTreeException if {@code inData} generates an |
| * invalid standard metadata tree. |
| */ |
| private TIFFImageMetadata convertStandardImageMetadata(IIOMetadata inData) |
| throws IIOInvalidTreeException { |
| |
| if(inData == null) { |
| throw new NullPointerException("inData == null!"); |
| } else if(!inData.isStandardMetadataFormatSupported()) { |
| throw new IllegalArgumentException |
| ("inData does not support standard metadata format!"); |
| } |
| |
| TIFFImageMetadata outData = null; |
| |
| String formatName = IIOMetadataFormatImpl.standardMetadataFormatName; |
| Node tree = inData.getAsTree(formatName); |
| if (tree != null) { |
| List<TIFFTagSet> tagSets = new ArrayList<TIFFTagSet>(1); |
| tagSets.add(BaselineTIFFTagSet.getInstance()); |
| outData = new TIFFImageMetadata(tagSets); |
| outData.setFromTree(formatName, tree); |
| } |
| |
| return outData; |
| } |
| |
| /** |
| * Converts a native |
| * {@code javax_imageio_tiff_image_1.0} tree to a |
| * {@code TIFFImageMetadata} object. |
| * |
| * @param inData The metadata object. |
| * @return a {@code TIFFImageMetadata} or {@code null} if |
| * the native tree derived from the input object is {@code null}. |
| * @throws IllegalArgumentException if {@code inData} is |
| * {@code null} or does not support the native metadata format. |
| * @throws IIOInvalidTreeException if {@code inData} generates an |
| * invalid native metadata tree. |
| */ |
| private TIFFImageMetadata convertNativeImageMetadata(IIOMetadata inData) |
| throws IIOInvalidTreeException { |
| |
| if(inData == null) { |
| throw new NullPointerException("inData == null!"); |
| } else if(!Arrays.asList(inData.getMetadataFormatNames()).contains( |
| TIFFImageMetadata.NATIVE_METADATA_FORMAT_NAME)) { |
| throw new IllegalArgumentException |
| ("inData does not support native metadata format!"); |
| } |
| |
| TIFFImageMetadata outData = null; |
| |
| String formatName = TIFFImageMetadata.NATIVE_METADATA_FORMAT_NAME; |
| Node tree = inData.getAsTree(formatName); |
| if (tree != null) { |
| List<TIFFTagSet> tagSets = new ArrayList<TIFFTagSet>(1); |
| tagSets.add(BaselineTIFFTagSet.getInstance()); |
| outData = new TIFFImageMetadata(tagSets); |
| outData.setFromTree(formatName, tree); |
| } |
| |
| return outData; |
| } |
| |
| /** |
| * Sets up the output metadata adding, removing, and overriding fields |
| * as needed. The destination image dimensions are provided as parameters |
| * because these might differ from those of the source due to subsampling. |
| * |
| * @param cm The {@code ColorModel} of the image being written. |
| * @param sm The {@code SampleModel} of the image being written. |
| * @param destWidth The width of the written image after subsampling. |
| * @param destHeight The height of the written image after subsampling. |
| */ |
| void setupMetadata(ColorModel cm, SampleModel sm, |
| int destWidth, int destHeight) |
| throws IIOException { |
| // Get initial IFD from metadata |
| |
| // Always emit these fields: |
| // |
| // Override values from metadata: |
| // |
| // planarConfiguration -> chunky (planar not supported on output) |
| // |
| // Override values from metadata with image-derived values: |
| // |
| // bitsPerSample (if not bilivel) |
| // colorMap (if palette color) |
| // photometricInterpretation (derive from image) |
| // imageLength |
| // imageWidth |
| // |
| // rowsPerStrip \ / tileLength |
| // stripOffsets | OR | tileOffsets |
| // stripByteCounts / | tileByteCounts |
| // \ tileWidth |
| // |
| // |
| // Override values from metadata with write param values: |
| // |
| // compression |
| |
| // Use values from metadata if present for these fields, |
| // otherwise use defaults: |
| // |
| // resolutionUnit |
| // XResolution (take from metadata if present) |
| // YResolution |
| // rowsPerStrip |
| // sampleFormat |
| |
| TIFFIFD rootIFD = imageMetadata.getRootIFD(); |
| |
| BaselineTIFFTagSet base = BaselineTIFFTagSet.getInstance(); |
| |
| // If PlanarConfiguration field present, set value to chunky. |
| |
| TIFFField f = |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_PLANAR_CONFIGURATION); |
| if(f != null && |
| f.getAsInt(0) != BaselineTIFFTagSet.PLANAR_CONFIGURATION_CHUNKY) { |
| TIFFField planarConfigurationField = |
| new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_PLANAR_CONFIGURATION), |
| BaselineTIFFTagSet.PLANAR_CONFIGURATION_CHUNKY); |
| rootIFD.addTIFFField(planarConfigurationField); |
| } |
| |
| char[] extraSamples = null; |
| |
| this.photometricInterpretation = -1; |
| boolean forcePhotometricInterpretation = false; |
| |
| f = |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_PHOTOMETRIC_INTERPRETATION); |
| if (f != null) { |
| photometricInterpretation = f.getAsInt(0); |
| if(photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR && |
| !(cm instanceof IndexColorModel)) { |
| photometricInterpretation = -1; |
| } else { |
| forcePhotometricInterpretation = true; |
| } |
| } |
| |
| int[] sampleSize = sm.getSampleSize(); |
| |
| int numBands = sm.getNumBands(); |
| int numExtraSamples = 0; |
| |
| // Check that numBands > 1 here because TIFF requires that |
| // SamplesPerPixel = numBands + numExtraSamples and numBands |
| // cannot be zero. |
| if (numBands > 1 && cm != null && cm.hasAlpha()) { |
| --numBands; |
| numExtraSamples = 1; |
| extraSamples = new char[1]; |
| if (cm.isAlphaPremultiplied()) { |
| extraSamples[0] = |
| BaselineTIFFTagSet.EXTRA_SAMPLES_ASSOCIATED_ALPHA; |
| } else { |
| extraSamples[0] = |
| BaselineTIFFTagSet.EXTRA_SAMPLES_UNASSOCIATED_ALPHA; |
| } |
| } |
| |
| if (numBands == 3) { |
| this.nativePhotometricInterpretation = |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB; |
| if (photometricInterpretation == -1) { |
| photometricInterpretation = |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB; |
| } |
| } else if (sm.getNumBands() == 1 && cm instanceof IndexColorModel) { |
| IndexColorModel icm = (IndexColorModel)cm; |
| int r0 = icm.getRed(0); |
| int r1 = icm.getRed(1); |
| if (icm.getMapSize() == 2 && |
| (r0 == icm.getGreen(0)) && (r0 == icm.getBlue(0)) && |
| (r1 == icm.getGreen(1)) && (r1 == icm.getBlue(1)) && |
| (r0 == 0 || r0 == 255) && |
| (r1 == 0 || r1 == 255) && |
| (r0 != r1)) { |
| // Black/white image |
| |
| if (r0 == 0) { |
| nativePhotometricInterpretation = |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO; |
| } else { |
| nativePhotometricInterpretation = |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO; |
| } |
| |
| |
| // If photometricInterpretation is already set to |
| // WhiteIsZero or BlackIsZero, leave it alone |
| if (photometricInterpretation != |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO && |
| photometricInterpretation != |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO) { |
| photometricInterpretation = |
| r0 == 0 ? |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO : |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO; |
| } |
| } else { |
| nativePhotometricInterpretation = |
| photometricInterpretation = |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR; |
| } |
| } else { |
| if(cm != null) { |
| switch(cm.getColorSpace().getType()) { |
| case ColorSpace.TYPE_Lab: |
| nativePhotometricInterpretation = |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_CIELAB; |
| break; |
| case ColorSpace.TYPE_YCbCr: |
| nativePhotometricInterpretation = |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR; |
| break; |
| case ColorSpace.TYPE_CMYK: |
| nativePhotometricInterpretation = |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_CMYK; |
| break; |
| default: |
| nativePhotometricInterpretation = |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO; |
| } |
| } else { |
| nativePhotometricInterpretation = |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO; |
| } |
| if (photometricInterpretation == -1) { |
| photometricInterpretation = nativePhotometricInterpretation; |
| } |
| } |
| |
| // Emit compression tag |
| |
| int compressionMode = param.getCompressionMode(); |
| switch(compressionMode) { |
| case ImageWriteParam.MODE_EXPLICIT: |
| { |
| String compressionType = param.getCompressionType(); |
| if (compressionType == null) { |
| this.compression = BaselineTIFFTagSet.COMPRESSION_NONE; |
| } else { |
| // Determine corresponding compression tag value. |
| int len = compressionTypes.length; |
| for (int i = 0; i < len; i++) { |
| if (compressionType.equals(compressionTypes[i])) { |
| this.compression = compressionNumbers[i]; |
| } |
| } |
| } |
| } |
| break; |
| case ImageWriteParam.MODE_COPY_FROM_METADATA: |
| { |
| TIFFField compField = |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_COMPRESSION); |
| if(compField != null) { |
| this.compression = compField.getAsInt(0); |
| } else { |
| this.compression = BaselineTIFFTagSet.COMPRESSION_NONE; |
| } |
| } |
| break; |
| default: |
| this.compression = BaselineTIFFTagSet.COMPRESSION_NONE; |
| } |
| |
| TIFFField predictorField = |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_PREDICTOR); |
| if (predictorField != null) { |
| this.predictor = predictorField.getAsInt(0); |
| |
| // We only support Horizontal Predictor for a bitDepth of 8 |
| if (sampleSize[0] != 8 || |
| // Check the value of the tag for validity |
| (predictor != BaselineTIFFTagSet.PREDICTOR_NONE && |
| predictor != |
| BaselineTIFFTagSet.PREDICTOR_HORIZONTAL_DIFFERENCING)) { |
| // Set to default |
| predictor = BaselineTIFFTagSet.PREDICTOR_NONE; |
| |
| // Emit this changed predictor value to metadata |
| TIFFField newPredictorField = |
| new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_PREDICTOR), |
| predictor); |
| rootIFD.addTIFFField(newPredictorField); |
| } |
| } |
| |
| TIFFField compressionField = |
| new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_COMPRESSION), |
| compression); |
| rootIFD.addTIFFField(compressionField); |
| |
| // Set Exif flag. Note that there is no way to determine definitively |
| // when an uncompressed thumbnail is being written as the Exif IFD |
| // pointer field is optional for thumbnails. |
| boolean isExif = false; |
| if(numBands == 3 && |
| sampleSize[0] == 8 && sampleSize[1] == 8 && sampleSize[2] == 8) { |
| // Three bands with 8 bits per sample. |
| if(rootIFD.getTIFFField(ExifParentTIFFTagSet.TAG_EXIF_IFD_POINTER) |
| != null) { |
| // Exif IFD pointer present. |
| if(compression == BaselineTIFFTagSet.COMPRESSION_NONE && |
| (photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB || |
| photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR)) { |
| // Uncompressed RGB or YCbCr. |
| isExif = true; |
| } else if(compression == |
| BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) { |
| // Compressed. |
| isExif = true; |
| } |
| } else if(compressionMode == ImageWriteParam.MODE_EXPLICIT && |
| EXIF_JPEG_COMPRESSION_TYPE.equals |
| (param.getCompressionType())) { |
| // Exif IFD pointer absent but Exif JPEG compression set. |
| isExif = true; |
| } |
| } |
| |
| // Initialize JPEG interchange format flag which is used to |
| // indicate that the image is stored as a single JPEG stream. |
| // This flag is separated from the 'isExif' flag in case JPEG |
| // interchange format is eventually supported for non-Exif images. |
| boolean isJPEGInterchange = |
| isExif && compression == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG; |
| |
| this.compressor = null; |
| if (compression == BaselineTIFFTagSet.COMPRESSION_CCITT_RLE) { |
| compressor = new TIFFRLECompressor(); |
| |
| if (!forcePhotometricInterpretation) { |
| photometricInterpretation |
| = BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO; |
| } |
| } else if (compression |
| == BaselineTIFFTagSet.COMPRESSION_CCITT_T_4) { |
| compressor = new TIFFT4Compressor(); |
| |
| if (!forcePhotometricInterpretation) { |
| photometricInterpretation |
| = BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO; |
| } |
| } else if (compression |
| == BaselineTIFFTagSet.COMPRESSION_CCITT_T_6) { |
| compressor = new TIFFT6Compressor(); |
| |
| if (!forcePhotometricInterpretation) { |
| photometricInterpretation |
| = BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO; |
| } |
| } else if (compression |
| == BaselineTIFFTagSet.COMPRESSION_LZW) { |
| compressor = new TIFFLZWCompressor(predictor); |
| } else if (compression |
| == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) { |
| if (isExif) { |
| compressor = new TIFFExifJPEGCompressor(param); |
| } else { |
| throw new IIOException("Old JPEG compression not supported!"); |
| } |
| } else if (compression |
| == BaselineTIFFTagSet.COMPRESSION_JPEG) { |
| if (numBands == 3 && sampleSize[0] == 8 |
| && sampleSize[1] == 8 && sampleSize[2] == 8) { |
| photometricInterpretation |
| = BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR; |
| } else if (numBands == 1 && sampleSize[0] == 8) { |
| photometricInterpretation |
| = BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO; |
| } else { |
| throw new IIOException("JPEG compression supported for 1- and 3-band byte images only!"); |
| } |
| compressor = new TIFFJPEGCompressor(param); |
| } else if (compression |
| == BaselineTIFFTagSet.COMPRESSION_ZLIB) { |
| compressor = new TIFFZLibCompressor(param, predictor); |
| } else if (compression |
| == BaselineTIFFTagSet.COMPRESSION_PACKBITS) { |
| compressor = new TIFFPackBitsCompressor(); |
| } else if (compression |
| == BaselineTIFFTagSet.COMPRESSION_DEFLATE) { |
| compressor = new TIFFDeflateCompressor(param, predictor); |
| } else { |
| // Determine inverse fill setting. |
| f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_FILL_ORDER); |
| boolean inverseFill = (f != null && f.getAsInt(0) == 2); |
| |
| if (inverseFill) { |
| compressor = new TIFFLSBCompressor(); |
| } else { |
| compressor = new TIFFNullCompressor(); |
| } |
| } |
| |
| |
| this.colorConverter = null; |
| if (cm != null |
| && cm.getColorSpace().getType() == ColorSpace.TYPE_RGB) { |
| // |
| // Perform color conversion only if image has RGB color space. |
| // |
| if (photometricInterpretation |
| == BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR |
| && compression |
| != BaselineTIFFTagSet.COMPRESSION_JPEG) { |
| // |
| // Convert RGB to YCbCr only if compression type is not |
| // JPEG in which case this is handled implicitly by the |
| // compressor. |
| // |
| colorConverter = new TIFFYCbCrColorConverter(imageMetadata); |
| } else if (photometricInterpretation |
| == BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_CIELAB) { |
| colorConverter = new TIFFCIELabColorConverter(); |
| } |
| } |
| |
| // |
| // Cannot at this time do YCbCr subsampling so set the |
| // YCbCrSubsampling field value to [1, 1] and the YCbCrPositioning |
| // field value to "cosited". |
| // |
| if(photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR && |
| compression != |
| BaselineTIFFTagSet.COMPRESSION_JPEG) { |
| // Remove old subsampling and positioning fields. |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING); |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING); |
| |
| // Add unity chrominance subsampling factors. |
| rootIFD.addTIFFField |
| (new TIFFField |
| (base.getTag(BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING), |
| TIFFTag.TIFF_SHORT, |
| 2, |
| new char[] {(char)1, (char)1})); |
| |
| // Add cosited positioning. |
| rootIFD.addTIFFField |
| (new TIFFField |
| (base.getTag(BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING), |
| TIFFTag.TIFF_SHORT, |
| 1, |
| new char[] { |
| (char)BaselineTIFFTagSet.Y_CB_CR_POSITIONING_COSITED |
| })); |
| } |
| |
| TIFFField photometricInterpretationField = |
| new TIFFField( |
| base.getTag(BaselineTIFFTagSet.TAG_PHOTOMETRIC_INTERPRETATION), |
| photometricInterpretation); |
| rootIFD.addTIFFField(photometricInterpretationField); |
| |
| this.bitsPerSample = new char[numBands + numExtraSamples]; |
| this.bitDepth = 0; |
| for (int i = 0; i < numBands; i++) { |
| this.bitDepth = Math.max(bitDepth, sampleSize[i]); |
| } |
| if (bitDepth == 3) { |
| bitDepth = 4; |
| } else if (bitDepth > 4 && bitDepth < 8) { |
| bitDepth = 8; |
| } else if (bitDepth > 8 && bitDepth < 16) { |
| bitDepth = 16; |
| } else if (bitDepth > 16 && bitDepth < 32) { |
| bitDepth = 32; |
| } else if (bitDepth > 32) { |
| bitDepth = 64; |
| } |
| |
| for (int i = 0; i < bitsPerSample.length; i++) { |
| bitsPerSample[i] = (char)bitDepth; |
| } |
| |
| // Emit BitsPerSample. If the image is bilevel, emit if and only |
| // if already in the metadata and correct (count and value == 1). |
| if (bitsPerSample.length != 1 || bitsPerSample[0] != 1) { |
| TIFFField bitsPerSampleField = |
| new TIFFField( |
| base.getTag(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE), |
| TIFFTag.TIFF_SHORT, |
| bitsPerSample.length, |
| bitsPerSample); |
| rootIFD.addTIFFField(bitsPerSampleField); |
| } else { // bitsPerSample.length == 1 && bitsPerSample[0] == 1 |
| TIFFField bitsPerSampleField = |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE); |
| if(bitsPerSampleField != null) { |
| int[] bps = bitsPerSampleField.getAsInts(); |
| if(bps == null || bps.length != 1 || bps[0] != 1) { |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE); |
| } |
| } |
| } |
| |
| // Prepare SampleFormat field. |
| f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_SAMPLE_FORMAT); |
| if(f == null && (bitDepth == 16 || bitDepth == 32 || bitDepth == 64)) { |
| // Set up default content for 16-, 32-, and 64-bit cases. |
| char sampleFormatValue; |
| int dataType = sm.getDataType(); |
| if(bitDepth == 16 && dataType == DataBuffer.TYPE_USHORT) { |
| sampleFormatValue = |
| (char)BaselineTIFFTagSet.SAMPLE_FORMAT_UNSIGNED_INTEGER; |
| } else if((bitDepth == 32 && dataType == DataBuffer.TYPE_FLOAT) || |
| (bitDepth == 64 && dataType == DataBuffer.TYPE_DOUBLE)) { |
| sampleFormatValue = |
| (char)BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT; |
| } else { |
| sampleFormatValue = |
| BaselineTIFFTagSet.SAMPLE_FORMAT_SIGNED_INTEGER; |
| } |
| this.sampleFormat = (int)sampleFormatValue; |
| char[] sampleFormatArray = new char[bitsPerSample.length]; |
| Arrays.fill(sampleFormatArray, sampleFormatValue); |
| |
| // Update the metadata. |
| TIFFTag sampleFormatTag = |
| base.getTag(BaselineTIFFTagSet.TAG_SAMPLE_FORMAT); |
| |
| TIFFField sampleFormatField = |
| new TIFFField(sampleFormatTag, TIFFTag.TIFF_SHORT, |
| sampleFormatArray.length, sampleFormatArray); |
| |
| rootIFD.addTIFFField(sampleFormatField); |
| } else if(f != null) { |
| // Get whatever was provided. |
| sampleFormat = f.getAsInt(0); |
| } else { |
| // Set default value for internal use only. |
| sampleFormat = BaselineTIFFTagSet.SAMPLE_FORMAT_UNDEFINED; |
| } |
| |
| if (extraSamples != null) { |
| TIFFField extraSamplesField = |
| new TIFFField( |
| base.getTag(BaselineTIFFTagSet.TAG_EXTRA_SAMPLES), |
| TIFFTag.TIFF_SHORT, |
| extraSamples.length, |
| extraSamples); |
| rootIFD.addTIFFField(extraSamplesField); |
| } else { |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_EXTRA_SAMPLES); |
| } |
| |
| TIFFField samplesPerPixelField = |
| new TIFFField( |
| base.getTag(BaselineTIFFTagSet.TAG_SAMPLES_PER_PIXEL), |
| bitsPerSample.length); |
| rootIFD.addTIFFField(samplesPerPixelField); |
| |
| // Emit ColorMap if image is of palette color type |
| if (photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR && |
| cm instanceof IndexColorModel) { |
| char[] colorMap = new char[3*(1 << bitsPerSample[0])]; |
| |
| IndexColorModel icm = (IndexColorModel)cm; |
| |
| // mapSize is determined by BitsPerSample, not by incoming ICM. |
| int mapSize = 1 << bitsPerSample[0]; |
| int indexBound = Math.min(mapSize, icm.getMapSize()); |
| for (int i = 0; i < indexBound; i++) { |
| colorMap[i] = (char)((icm.getRed(i)*65535)/255); |
| colorMap[mapSize + i] = (char)((icm.getGreen(i)*65535)/255); |
| colorMap[2*mapSize + i] = (char)((icm.getBlue(i)*65535)/255); |
| } |
| |
| TIFFField colorMapField = |
| new TIFFField( |
| base.getTag(BaselineTIFFTagSet.TAG_COLOR_MAP), |
| TIFFTag.TIFF_SHORT, |
| colorMap.length, |
| colorMap); |
| rootIFD.addTIFFField(colorMapField); |
| } else { |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_COLOR_MAP); |
| } |
| |
| // Emit ICCProfile if there is no ICCProfile field already in the |
| // metadata and the ColorSpace is non-standard ICC. |
| if(cm != null && |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_ICC_PROFILE) == null && |
| ImageUtil.isNonStandardICCColorSpace(cm.getColorSpace())) { |
| ICC_ColorSpace iccColorSpace = (ICC_ColorSpace)cm.getColorSpace(); |
| byte[] iccProfileData = iccColorSpace.getProfile().getData(); |
| TIFFField iccProfileField = |
| new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_ICC_PROFILE), |
| TIFFTag.TIFF_UNDEFINED, |
| iccProfileData.length, |
| iccProfileData); |
| rootIFD.addTIFFField(iccProfileField); |
| } |
| |
| // Always emit XResolution and YResolution. |
| |
| TIFFField XResolutionField = |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_X_RESOLUTION); |
| TIFFField YResolutionField = |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_Y_RESOLUTION); |
| |
| if(XResolutionField == null && YResolutionField == null) { |
| long[][] resRational = new long[1][2]; |
| resRational[0] = new long[2]; |
| |
| TIFFField ResolutionUnitField = |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_RESOLUTION_UNIT); |
| |
| // Don't force dimensionless if one of the other dimensional |
| // quantities is present. |
| if(ResolutionUnitField == null && |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_X_POSITION) == null && |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_Y_POSITION) == null) { |
| // Set resolution to unit and units to dimensionless. |
| resRational[0][0] = 1; |
| resRational[0][1] = 1; |
| |
| ResolutionUnitField = |
| new TIFFField(rootIFD.getTag |
| (BaselineTIFFTagSet.TAG_RESOLUTION_UNIT), |
| BaselineTIFFTagSet.RESOLUTION_UNIT_NONE); |
| rootIFD.addTIFFField(ResolutionUnitField); |
| } else { |
| // Set resolution to a value which would make the maximum |
| // image dimension equal to 4 inches as arbitrarily stated |
| // in the description of ResolutionUnit in the TIFF 6.0 |
| // specification. If the ResolutionUnit field specifies |
| // "none" then set the resolution to unity (1/1). |
| int resolutionUnit = ResolutionUnitField != null ? |
| ResolutionUnitField.getAsInt(0) : |
| BaselineTIFFTagSet.RESOLUTION_UNIT_INCH; |
| int maxDimension = Math.max(destWidth, destHeight); |
| switch(resolutionUnit) { |
| case BaselineTIFFTagSet.RESOLUTION_UNIT_INCH: |
| resRational[0][0] = maxDimension; |
| resRational[0][1] = 4; |
| break; |
| case BaselineTIFFTagSet.RESOLUTION_UNIT_CENTIMETER: |
| resRational[0][0] = 100L*maxDimension; // divide out 100 |
| resRational[0][1] = 4*254; // 2.54 cm/inch * 100 |
| break; |
| default: |
| resRational[0][0] = 1; |
| resRational[0][1] = 1; |
| } |
| } |
| |
| XResolutionField = |
| new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_X_RESOLUTION), |
| TIFFTag.TIFF_RATIONAL, |
| 1, |
| resRational); |
| rootIFD.addTIFFField(XResolutionField); |
| |
| YResolutionField = |
| new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_Y_RESOLUTION), |
| TIFFTag.TIFF_RATIONAL, |
| 1, |
| resRational); |
| rootIFD.addTIFFField(YResolutionField); |
| } else if(XResolutionField == null && YResolutionField != null) { |
| // Set XResolution to YResolution. |
| long[] yResolution = |
| YResolutionField.getAsRational(0).clone(); |
| XResolutionField = |
| new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_X_RESOLUTION), |
| TIFFTag.TIFF_RATIONAL, |
| 1, |
| yResolution); |
| rootIFD.addTIFFField(XResolutionField); |
| } else if(XResolutionField != null && YResolutionField == null) { |
| // Set YResolution to XResolution. |
| long[] xResolution = |
| XResolutionField.getAsRational(0).clone(); |
| YResolutionField = |
| new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_Y_RESOLUTION), |
| TIFFTag.TIFF_RATIONAL, |
| 1, |
| xResolution); |
| rootIFD.addTIFFField(YResolutionField); |
| } |
| |
| // Set mandatory fields, overriding metadata passed in |
| |
| int width = destWidth; |
| TIFFField imageWidthField = |
| new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_IMAGE_WIDTH), |
| width); |
| rootIFD.addTIFFField(imageWidthField); |
| |
| int height = destHeight; |
| TIFFField imageLengthField = |
| new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_IMAGE_LENGTH), |
| height); |
| rootIFD.addTIFFField(imageLengthField); |
| |
| // Determine rowsPerStrip |
| |
| int rowsPerStrip; |
| |
| TIFFField rowsPerStripField = |
| rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP); |
| if (rowsPerStripField != null) { |
| rowsPerStrip = rowsPerStripField.getAsInt(0); |
| if(rowsPerStrip < 0) { |
| rowsPerStrip = height; |
| } |
| } else { |
| int bitsPerPixel = bitDepth*(numBands + numExtraSamples); |
| int bytesPerRow = (bitsPerPixel*width + 7)/8; |
| rowsPerStrip = |
| Math.max(Math.max(DEFAULT_BYTES_PER_STRIP/bytesPerRow, 1), 8); |
| } |
| rowsPerStrip = Math.min(rowsPerStrip, height); |
| |
| // Tiling flag. |
| boolean useTiling = false; |
| |
| // Analyze tiling parameters |
| int tilingMode = param.getTilingMode(); |
| if (tilingMode == ImageWriteParam.MODE_DISABLED || |
| tilingMode == ImageWriteParam.MODE_DEFAULT) { |
| this.tileWidth = width; |
| this.tileLength = rowsPerStrip; |
| useTiling = false; |
| } else if (tilingMode == ImageWriteParam.MODE_EXPLICIT) { |
| tileWidth = param.getTileWidth(); |
| tileLength = param.getTileHeight(); |
| useTiling = true; |
| } else if (tilingMode == ImageWriteParam.MODE_COPY_FROM_METADATA) { |
| f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_TILE_WIDTH); |
| if (f == null) { |
| tileWidth = width; |
| useTiling = false; |
| } else { |
| tileWidth = f.getAsInt(0); |
| useTiling = true; |
| } |
| |
| f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_TILE_LENGTH); |
| if (f == null) { |
| tileLength = rowsPerStrip; |
| } else { |
| tileLength = f.getAsInt(0); |
| useTiling = true; |
| } |
| } else { |
| throw new IIOException("Illegal value of tilingMode!"); |
| } |
| |
| if(compression == BaselineTIFFTagSet.COMPRESSION_JPEG) { |
| // Reset tile size per TTN2 spec for JPEG compression. |
| int subX; |
| int subY; |
| if(numBands == 1) { |
| subX = subY = 1; |
| } else { |
| subX = subY = TIFFJPEGCompressor.CHROMA_SUBSAMPLING; |
| } |
| if(useTiling) { |
| int MCUMultipleX = 8*subX; |
| int MCUMultipleY = 8*subY; |
| tileWidth = |
| Math.max(MCUMultipleX*((tileWidth + |
| MCUMultipleX/2)/MCUMultipleX), |
| MCUMultipleX); |
| tileLength = |
| Math.max(MCUMultipleY*((tileLength + |
| MCUMultipleY/2)/MCUMultipleY), |
| MCUMultipleY); |
| } else if(rowsPerStrip < height) { |
| int MCUMultiple = 8*Math.max(subX, subY); |
| rowsPerStrip = tileLength = |
| Math.max(MCUMultiple*((tileLength + |
| MCUMultiple/2)/MCUMultiple), |
| MCUMultiple); |
| } |
| |
| // The written image may be unreadable if these fields are present. |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); |
| |
| // Also remove fields related to the old JPEG encoding scheme |
| // which may be misleading when the compression type is JPEG. |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_PROC); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_RESTART_INTERVAL); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_LOSSLESS_PREDICTORS); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_POINT_TRANSFORMS); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_Q_TABLES); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_DC_TABLES); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_AC_TABLES); |
| } else if(isJPEGInterchange) { |
| // Force tile size to equal image size. |
| tileWidth = width; |
| tileLength = height; |
| } else if(useTiling) { |
| // Round tile size to multiple of 16 per TIFF 6.0 specification |
| // (see pages 67-68 of version 6.0.1 from Adobe). |
| int tileWidthRemainder = tileWidth % 16; |
| if(tileWidthRemainder != 0) { |
| // Round to nearest multiple of 16 not less than 16. |
| tileWidth = Math.max(16*((tileWidth + 8)/16), 16); |
| processWarningOccurred(currentImage, |
| "Tile width rounded to multiple of 16."); |
| } |
| |
| int tileLengthRemainder = tileLength % 16; |
| if(tileLengthRemainder != 0) { |
| // Round to nearest multiple of 16 not less than 16. |
| tileLength = Math.max(16*((tileLength + 8)/16), 16); |
| processWarningOccurred(currentImage, |
| "Tile height rounded to multiple of 16."); |
| } |
| } |
| |
| this.tilesAcross = (width + tileWidth - 1)/tileWidth; |
| this.tilesDown = (height + tileLength - 1)/tileLength; |
| |
| if (!useTiling) { |
| this.isTiled = false; |
| |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_WIDTH); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_LENGTH); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_OFFSETS); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_BYTE_COUNTS); |
| |
| rowsPerStripField = |
| new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP), |
| rowsPerStrip); |
| rootIFD.addTIFFField(rowsPerStripField); |
| |
| TIFFField stripOffsetsField = |
| new TIFFField( |
| base.getTag(BaselineTIFFTagSet.TAG_STRIP_OFFSETS), |
| TIFFTag.TIFF_LONG, |
| tilesDown); |
| rootIFD.addTIFFField(stripOffsetsField); |
| |
| TIFFField stripByteCountsField = |
| new TIFFField( |
| base.getTag(BaselineTIFFTagSet.TAG_STRIP_BYTE_COUNTS), |
| TIFFTag.TIFF_LONG, |
| tilesDown); |
| rootIFD.addTIFFField(stripByteCountsField); |
| } else { |
| this.isTiled = true; |
| |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_OFFSETS); |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_BYTE_COUNTS); |
| |
| TIFFField tileWidthField = |
| new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_TILE_WIDTH), |
| tileWidth); |
| rootIFD.addTIFFField(tileWidthField); |
| |
| TIFFField tileLengthField = |
| new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_TILE_LENGTH), |
| tileLength); |
| rootIFD.addTIFFField(tileLengthField); |
| |
| TIFFField tileOffsetsField = |
| new TIFFField( |
| base.getTag(BaselineTIFFTagSet.TAG_TILE_OFFSETS), |
| TIFFTag.TIFF_LONG, |
| tilesDown*tilesAcross); |
| rootIFD.addTIFFField(tileOffsetsField); |
| |
| TIFFField tileByteCountsField = |
| new TIFFField( |
| base.getTag(BaselineTIFFTagSet.TAG_TILE_BYTE_COUNTS), |
| TIFFTag.TIFF_LONG, |
| tilesDown*tilesAcross); |
| rootIFD.addTIFFField(tileByteCountsField); |
| } |
| |
| if(isExif) { |
| // |
| // Ensure presence of mandatory fields and absence of prohibited |
| // fields and those that duplicate information in JPEG marker |
| // segments per tables 14-18 of the Exif 2.2 specification. |
| // |
| |
| // If an empty image is being written or inserted then infer |
| // that the primary IFD is being set up. |
| boolean isPrimaryIFD = isEncodingEmpty(); |
| |
| // Handle TIFF fields in order of increasing tag number. |
| if(compression == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) { |
| // ImageWidth |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_IMAGE_WIDTH); |
| |
| // ImageLength |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_IMAGE_LENGTH); |
| |
| // BitsPerSample |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE); |
| // Compression |
| if(isPrimaryIFD) { |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_COMPRESSION); |
| } |
| |
| // PhotometricInterpretation |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_PHOTOMETRIC_INTERPRETATION); |
| |
| // StripOffsets |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_OFFSETS); |
| |
| // SamplesPerPixel |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_SAMPLES_PER_PIXEL); |
| |
| // RowsPerStrip |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP); |
| |
| // StripByteCounts |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_BYTE_COUNTS); |
| // XResolution and YResolution are handled above for all TIFFs. |
| |
| // PlanarConfiguration |
| rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_PLANAR_CONFIGURATION); |
| |
| // ResolutionUnit |
| if(rootIFD.getTIFFField |
| (BaselineTIFFTagSet.TAG_RESOLUTION_UNIT) == null) { |
| f = new TIFFField(base.getTag |
| (BaselineTIFFTagSet.TAG_RESOLUTION_UNIT), |
| BaselineTIFFTagSet.RESOLUTION_UNIT_INCH); |
| rootIFD.addTIFFField(f); |
| } |
| |
| if(isPrimaryIFD) { |
| // JPEGInterchangeFormat |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT); |
| |
| // JPEGInterchangeFormatLength |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); |
| |
| // YCbCrSubsampling |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING); |
| |
| // YCbCrPositioning |
| if(rootIFD.getTIFFField |
| (BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING) == null) { |
| f = new TIFFField |
| (base.getTag |
| (BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING), |
| TIFFTag.TIFF_SHORT, |
| 1, |
| new char[] { |
| (char)BaselineTIFFTagSet.Y_CB_CR_POSITIONING_CENTERED |
| }); |
| rootIFD.addTIFFField(f); |
| } |
| } else { // Thumbnail IFD |
| // JPEGInterchangeFormat |
| f = new TIFFField |
| (base.getTag |
| (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT), |
| TIFFTag.TIFF_LONG, |
| 1); |
| rootIFD.addTIFFField(f); |
| |
| // JPEGInterchangeFormatLength |
| f = new TIFFField |
| (base.getTag |
| (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH), |
| TIFFTag.TIFF_LONG, |
| 1); |
| rootIFD.addTIFFField(f); |
| |
| // YCbCrSubsampling |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING); |
| } |
| } else { // Uncompressed |
| // ImageWidth through PlanarConfiguration are set above. |
| // XResolution and YResolution are handled above for all TIFFs. |
| |
| // ResolutionUnit |
| if(rootIFD.getTIFFField |
| (BaselineTIFFTagSet.TAG_RESOLUTION_UNIT) == null) { |
| f = new TIFFField(base.getTag |
| (BaselineTIFFTagSet.TAG_RESOLUTION_UNIT), |
| BaselineTIFFTagSet.RESOLUTION_UNIT_INCH); |
| rootIFD.addTIFFField(f); |
| } |
| |
| |
| // JPEGInterchangeFormat |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT); |
| |
| // JPEGInterchangeFormatLength |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); |
| |
| if(photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB) { |
| // YCbCrCoefficients |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_Y_CB_CR_COEFFICIENTS); |
| |
| // YCbCrSubsampling |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING); |
| |
| // YCbCrPositioning |
| rootIFD.removeTIFFField |
| (BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING); |
| } |
| } |
| |
| // Get Exif tags. |
| TIFFTagSet exifTags = ExifTIFFTagSet.getInstance(); |
| |
| // Retrieve or create the Exif IFD. |
| TIFFIFD exifIFD = null; |
| f = rootIFD.getTIFFField |
| (ExifParentTIFFTagSet.TAG_EXIF_IFD_POINTER); |
| if(f != null && f.hasDirectory()) { |
| // Retrieve the Exif IFD. |
| exifIFD = TIFFIFD.getDirectoryAsIFD(f.getDirectory()); |
| } else if(isPrimaryIFD) { |
| // Create the Exif IFD. |
| List<TIFFTagSet> exifTagSets = new ArrayList<TIFFTagSet>(1); |
| exifTagSets.add(exifTags); |
| exifIFD = new TIFFIFD(exifTagSets); |
| |
| // Add it to the root IFD. |
| TIFFTagSet tagSet = ExifParentTIFFTagSet.getInstance(); |
| TIFFTag exifIFDTag = |
| tagSet.getTag(ExifParentTIFFTagSet.TAG_EXIF_IFD_POINTER); |
| rootIFD.addTIFFField(new TIFFField(exifIFDTag, |
| TIFFTag.TIFF_LONG, |
| 1L, |
| exifIFD)); |
| } |
| |
| if(exifIFD != null) { |
| // Handle Exif private fields in order of increasing |
| // tag number. |
| |
| // ExifVersion |
| if(exifIFD.getTIFFField |
| (ExifTIFFTagSet.TAG_EXIF_VERSION) == null) { |
| f = new TIFFField |
| (exifTags.getTag(ExifTIFFTagSet.TAG_EXIF_VERSION), |
| TIFFTag.TIFF_UNDEFINED, |
| 4, |
| ExifTIFFTagSet.EXIF_VERSION_2_2.getBytes(StandardCharsets.US_ASCII)); |
| exifIFD.addTIFFField(f); |
| } |
| |
| if(compression == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) { |
| // ComponentsConfiguration |
| if(exifIFD.getTIFFField |
| (ExifTIFFTagSet.TAG_COMPONENTS_CONFIGURATION) == null) { |
| f = new TIFFField |
| (exifTags.getTag |
| (ExifTIFFTagSet.TAG_COMPONENTS_CONFIGURATION), |
| TIFFTag.TIFF_UNDEFINED, |
| 4, |
| new byte[] { |
| (byte)ExifTIFFTagSet.COMPONENTS_CONFIGURATION_Y, |
| (byte)ExifTIFFTagSet.COMPONENTS_CONFIGURATION_CB, |
| (byte)ExifTIFFTagSet.COMPONENTS_CONFIGURATION_CR, |
| (byte)0 |
| }); |
| exifIFD.addTIFFField(f); |
| } |
| } else { |
| // ComponentsConfiguration |
| exifIFD.removeTIFFField |
| (ExifTIFFTagSet.TAG_COMPONENTS_CONFIGURATION); |
| |
| // CompressedBitsPerPixel |
| exifIFD.removeTIFFField |
| (ExifTIFFTagSet.TAG_COMPRESSED_BITS_PER_PIXEL); |
| } |
| |
| // FlashpixVersion |
| if(exifIFD.getTIFFField |
| (ExifTIFFTagSet.TAG_FLASHPIX_VERSION) == null) { |
| f = new TIFFField |
| (exifTags.getTag(ExifTIFFTagSet.TAG_FLASHPIX_VERSION), |
| TIFFTag.TIFF_UNDEFINED, |
| 4, |
| new byte[] {(byte)'0', (byte)'1', (byte)'0', (byte)'0'}); |
| exifIFD.addTIFFField(f); |
| } |
| |
| // ColorSpace |
| if(exifIFD.getTIFFField |
| (ExifTIFFTagSet.TAG_COLOR_SPACE) == null) { |
| f = new TIFFField |
| (exifTags.getTag(ExifTIFFTagSet.TAG_COLOR_SPACE), |
| TIFFTag.TIFF_SHORT, |
| 1, |
| new char[] { |
| (char)ExifTIFFTagSet.COLOR_SPACE_SRGB |
| }); |
| exifIFD.addTIFFField(f); |
| } |
| |
| if(compression == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) { |
| // PixelXDimension |
| if(exifIFD.getTIFFField |
| (ExifTIFFTagSet.TAG_PIXEL_X_DIMENSION) == null) { |
| f = new TIFFField |
| (exifTags.getTag(ExifTIFFTagSet.TAG_PIXEL_X_DIMENSION), |
| width); |
| exifIFD.addTIFFField(f); |
| } |
| |
| // PixelYDimension |
| if(exifIFD.getTIFFField |
| (ExifTIFFTagSet.TAG_PIXEL_Y_DIMENSION) == null) { |
| f = new TIFFField |
| (exifTags.getTag(ExifTIFFTagSet.TAG_PIXEL_Y_DIMENSION), |
| height); |
| exifIFD.addTIFFField(f); |
| } |
| } else { |
| exifIFD.removeTIFFField |
| (ExifTIFFTagSet.TAG_INTEROPERABILITY_IFD_POINTER); |
| } |
| } |
| |
| } // if(isExif) |
| } |
| |
| ImageTypeSpecifier getImageType() { |
| return imageType; |
| } |
| |
| /** |
| @param tileRect The area to be written which might be outside the image. |
| */ |
| private int writeTile(Rectangle tileRect, TIFFCompressor compressor) |
| throws IOException { |
| // Determine the rectangle which will actually be written |
| // and set the padding flag. Padding will occur only when the |
| // image is written as a tiled TIFF and the tile bounds are not |
| // contained within the image bounds. |
| Rectangle activeRect; |
| boolean isPadded; |
| Rectangle imageBounds = |
| new Rectangle(image.getMinX(), image.getMinY(), |
| image.getWidth(), image.getHeight()); |
| if(!isTiled) { |
| // Stripped |
| activeRect = tileRect.intersection(imageBounds); |
| tileRect = activeRect; |
| isPadded = false; |
| } else if(imageBounds.contains(tileRect)) { |
| // Tiled, tile within image bounds |
| activeRect = tileRect; |
| isPadded = false; |
| } else { |
| // Tiled, tile not within image bounds |
| activeRect = imageBounds.intersection(tileRect); |
| isPadded = true; |
| } |
| |
| // Return early if empty intersection. |
| if(activeRect.isEmpty()) { |
| return 0; |
| } |
| |
| int minX = tileRect.x; |
| int minY = tileRect.y; |
| int width = tileRect.width; |
| int height = tileRect.height; |
| |
| if(isImageSimple) { |
| |
| SampleModel sm = image.getSampleModel(); |
| |
| // Read only data from the active rectangle. |
| Raster raster = image.getData(activeRect); |
| |
| // If padding is required, create a larger Raster and fill |
| // it from the active rectangle. |
| if(isPadded) { |
| WritableRaster wr = |
| raster.createCompatibleWritableRaster(minX, minY, |
| width, height); |
| wr.setRect(raster); |
| raster = wr; |
| } |
| |
| if(isBilevel) { |
| byte[] buf = ImageUtil.getPackedBinaryData(raster, |
| tileRect); |
| |
| if(isInverted) { |
| DataBuffer dbb = raster.getDataBuffer(); |
| if(dbb instanceof DataBufferByte && |
| buf == ((DataBufferByte)dbb).getData()) { |
| byte[] bbuf = new byte[buf.length]; |
| int len = buf.length; |
| for(int i = 0; i < len; i++) { |
| bbuf[i] = (byte)(buf[i] ^ 0xff); |
| } |
| buf = bbuf; |
| } else { |
| int len = buf.length; |
| for(int i = 0; i < len; i++) { |
| buf[i] ^= 0xff; |
| } |
| } |
| } |
| |
| return compressor.encode(buf, 0, |
| width, height, sampleSize, |
| (tileRect.width + 7)/8); |
| } else if(bitDepth == 8 && |
| sm.getDataType() == DataBuffer.TYPE_BYTE) { |
| ComponentSampleModel csm = |
| (ComponentSampleModel)raster.getSampleModel(); |
| |
| byte[] buf = |
| ((DataBufferByte)raster.getDataBuffer()).getData(); |
| |
| int off = |
| csm.getOffset(minX - |
| raster.getSampleModelTranslateX(), |
| minY - |
| raster.getSampleModelTranslateY()); |
| |
| return compressor.encode(buf, off, |
| width, height, sampleSize, |
| csm.getScanlineStride()); |
| } |
| } |
| |
| // Set offsets and skips based on source subsampling factors |
| int xOffset = minX; |
| int xSkip = periodX; |
| int yOffset = minY; |
| int ySkip = periodY; |
| |
| // Early exit if no data for this pass |
| int hpixels = (width + xSkip - 1)/xSkip; |
| int vpixels = (height + ySkip - 1)/ySkip; |
| if (hpixels == 0 || vpixels == 0) { |
| return 0; |
| } |
| |
| // Convert X offset and skip from pixels to samples |
| xOffset *= numBands; |
| xSkip *= numBands; |
| |
| // Initialize sizes |
| int samplesPerByte = 8/bitDepth; |
| int numSamples = width*numBands; |
| int bytesPerRow = hpixels*numBands; |
| |
| // Update number of bytes per row. |
| if (bitDepth < 8) { |
| bytesPerRow = (bytesPerRow + samplesPerByte - 1)/samplesPerByte; |
| } else if (bitDepth == 16) { |
| bytesPerRow *= 2; |
| } else if (bitDepth == 32) { |
| bytesPerRow *= 4; |
| } else if (bitDepth == 64) { |
| bytesPerRow *= 8; |
| } |
| |
| // Create row buffers |
| int[] samples = null; |
| float[] fsamples = null; |
| double[] dsamples = null; |
| if(sampleFormat == BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT) { |
| if (bitDepth == 32) { |
| fsamples = new float[numSamples]; |
| } else { |
| dsamples = new double[numSamples]; |
| } |
| } else { |
| samples = new int[numSamples]; |
| } |
| |
| // Create tile buffer |
| byte[] currTile = new byte[bytesPerRow*vpixels]; |
| |
| // Sub-optimal case: shy of "isImageSimple" only by virtue of |
| // not being contiguous. |
| if(!isInverted && // no inversion |
| !isRescaling && // no value rescaling |
| sourceBands == null && // no subbanding |
| periodX == 1 && periodY == 1 && // no subsampling |
| colorConverter == null) { |
| |
| SampleModel sm = image.getSampleModel(); |
| |
| if(sm instanceof ComponentSampleModel && // component |
| bitDepth == 8 && // 8 bits/sample |
| sm.getDataType() == DataBuffer.TYPE_BYTE) { // byte type |
| |
| // Read only data from the active rectangle. |
| Raster raster = image.getData(activeRect); |
| |
| // If padding is required, create a larger Raster and fill |
| // it from the active rectangle. |
| if(isPadded) { |
| WritableRaster wr = |
| raster.createCompatibleWritableRaster(minX, minY, |
| width, height); |
| wr.setRect(raster); |
| raster = wr; |
| } |
| |
| // Get SampleModel info. |
| ComponentSampleModel csm = |
| (ComponentSampleModel)raster.getSampleModel(); |
| int[] bankIndices = csm.getBankIndices(); |
| byte[][] bankData = |
| ((DataBufferByte)raster.getDataBuffer()).getBankData(); |
| int lineStride = csm.getScanlineStride(); |
| int pixelStride = csm.getPixelStride(); |
| |
| // Copy the data into a contiguous pixel interleaved buffer. |
| for(int k = 0; k < numBands; k++) { |
| byte[] bandData = bankData[bankIndices[k]]; |
| int lineOffset = |
| csm.getOffset(raster.getMinX() - |
| raster.getSampleModelTranslateX(), |
| raster.getMinY() - |
| raster.getSampleModelTranslateY(), k); |
| int idx = k; |
| for(int j = 0; j < vpixels; j++) { |
| int offset = lineOffset; |
| for(int i = 0; i < hpixels; i++) { |
| currTile[idx] = bandData[offset]; |
| idx += numBands; |
| offset += pixelStride; |
| } |
| lineOffset += lineStride; |
| } |
| } |
| |
| // Compressor and return. |
| return compressor.encode(currTile, 0, |
| width, height, sampleSize, |
| width*numBands); |
| } |
| } |
| |
| int tcount = 0; |
| |
| // Save active rectangle variables. |
| int activeMinX = activeRect.x; |
| int activeMinY = activeRect.y; |
| int activeMaxY = activeMinY + activeRect.height - 1; |
| int activeWidth = activeRect.width; |
| |
| // Set a SampleModel for use in padding. |
| SampleModel rowSampleModel = null; |
| if(isPadded) { |
| rowSampleModel = |
| image.getSampleModel().createCompatibleSampleModel(width, 1); |
| } |
| |
| for (int row = yOffset; row < yOffset + height; row += ySkip) { |
| Raster ras = null; |
| if(isPadded) { |
| // Create a raster for the entire row. |
| WritableRaster wr = |
| Raster.createWritableRaster(rowSampleModel, |
| new Point(minX, row)); |
| |
| // Populate the raster from the active sub-row, if any. |
| if(row >= activeMinY && row <= activeMaxY) { |
| Rectangle rect = |
| new Rectangle(activeMinX, row, activeWidth, 1); |
| ras = image.getData(rect); |
| wr.setRect(ras); |
| } |
| |
| // Update the raster variable. |
| ras = wr; |
| } else { |
| Rectangle rect = new Rectangle(minX, row, width, 1); |
| ras = image.getData(rect); |
| } |
| if (sourceBands != null) { |
| ras = ras.createChild(minX, row, width, 1, minX, row, |
| sourceBands); |
| } |
| |
| if(sampleFormat == |
| BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT) { |
| if (fsamples != null) { |
| ras.getPixels(minX, row, width, 1, fsamples); |
| } else { |
| ras.getPixels(minX, row, width, 1, dsamples); |
| } |
| } else { |
| ras.getPixels(minX, row, width, 1, samples); |
| |
| if ((nativePhotometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO && |
| photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO) || |
| (nativePhotometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO && |
| photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO)) { |
| int bitMask = (1 << bitDepth) - 1; |
| for (int s = 0; s < numSamples; s++) { |
| samples[s] ^= bitMask; |
| } |
| } |
| } |
| |
| if (colorConverter != null) { |
| int idx = 0; |
| float[] result = new float[3]; |
| |
| if(sampleFormat == |
| BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT) { |
| if (bitDepth == 32) { |
| for (int i = 0; i < width; i++) { |
| float r = fsamples[idx]; |
| float g = fsamples[idx + 1]; |
| float b = fsamples[idx + 2]; |
| |
| colorConverter.fromRGB(r, g, b, result); |
| |
| fsamples[idx] = result[0]; |
| fsamples[idx + 1] = result[1]; |
| fsamples[idx + 2] = result[2]; |
| |
| idx += 3; |
| } |
| } else { |
| for (int i = 0; i < width; i++) { |
| // Note: Possible loss of precision. |
| float r = (float)dsamples[idx]; |
| float g = (float)dsamples[idx + 1]; |
| float b = (float)dsamples[idx + 2]; |
| |
| colorConverter.fromRGB(r, g, b, result); |
| |
| dsamples[idx] = result[0]; |
| dsamples[idx + 1] = result[1]; |
| dsamples[idx + 2] = result[2]; |
| |
| idx += 3; |
| } |
| } |
| } else { |
| for (int i = 0; i < width; i++) { |
| float r = (float)samples[idx]; |
| float g = (float)samples[idx + 1]; |
| float b = (float)samples[idx + 2]; |
| |
| colorConverter.fromRGB(r, g, b, result); |
| |
| samples[idx] = (int)(result[0]); |
| samples[idx + 1] = (int)(result[1]); |
| samples[idx + 2] = (int)(result[2]); |
| |
| idx += 3; |
| } |
| } |
| } |
| |
| int tmp = 0; |
| int pos = 0; |
| |
| switch (bitDepth) { |
| case 1: case 2: case 4: |
| // Image can only have a single band |
| |
| if(isRescaling) { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| byte val = scale0[samples[s]]; |
| tmp = (tmp << bitDepth) | val; |
| |
| if (++pos == samplesPerByte) { |
| currTile[tcount++] = (byte)tmp; |
| tmp = 0; |
| pos = 0; |
| } |
| } |
| } else { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| byte val = (byte)samples[s]; |
| tmp = (tmp << bitDepth) | val; |
| |
| if (++pos == samplesPerByte) { |
| currTile[tcount++] = (byte)tmp; |
| tmp = 0; |
| pos = 0; |
| } |
| } |
| } |
| |
| // Left shift the last byte |
| if (pos != 0) { |
| tmp <<= ((8/bitDepth) - pos)*bitDepth; |
| currTile[tcount++] = (byte)tmp; |
| } |
| break; |
| |
| case 8: |
| if (numBands == 1) { |
| if(isRescaling) { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| currTile[tcount++] = scale0[samples[s]]; |
| } |
| } else { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| currTile[tcount++] = (byte)samples[s]; |
| } |
| } |
| } else { |
| if(isRescaling) { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| currTile[tcount++] = scale[b][samples[s + b]]; |
| } |
| } |
| } else { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| currTile[tcount++] = (byte)samples[s + b]; |
| } |
| } |
| } |
| } |
| break; |
| |
| case 16: |
| if(isRescaling) { |
| if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| int sample = samples[s + b]; |
| currTile[tcount++] = scaleh[b][sample]; |
| currTile[tcount++] = scalel[b][sample]; |
| } |
| } |
| } else { // ByteOrder.LITLE_ENDIAN |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| int sample = samples[s + b]; |
| currTile[tcount++] = scalel[b][sample]; |
| currTile[tcount++] = scaleh[b][sample]; |
| } |
| } |
| } |
| } else { |
| if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| int sample = samples[s + b]; |
| currTile[tcount++] = |
| (byte)((sample >>> 8) & 0xff); |
| currTile[tcount++] = |
| (byte)(sample & 0xff); |
| } |
| } |
| } else { // ByteOrder.LITLE_ENDIAN |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| int sample = samples[s + b]; |
| currTile[tcount++] = |
| (byte)(sample & 0xff); |
| currTile[tcount++] = |
| (byte)((sample >>> 8) & 0xff); |
| } |
| } |
| } |
| } |
| break; |
| |
| case 32: |
| if(sampleFormat == |
| BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT) { |
| if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| float fsample = fsamples[s + b]; |
| int isample = Float.floatToIntBits(fsample); |
| currTile[tcount++] = |
| (byte)((isample & 0xff000000) >> 24); |
| currTile[tcount++] = |
| (byte)((isample & 0x00ff0000) >> 16); |
| currTile[tcount++] = |
| (byte)((isample & 0x0000ff00) >> 8); |
| currTile[tcount++] = |
| (byte)(isample & 0x000000ff); |
| } |
| } |
| } else { // ByteOrder.LITLE_ENDIAN |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| float fsample = fsamples[s + b]; |
| int isample = Float.floatToIntBits(fsample); |
| currTile[tcount++] = |
| (byte)(isample & 0x000000ff); |
| currTile[tcount++] = |
| (byte)((isample & 0x0000ff00) >> 8); |
| currTile[tcount++] = |
| (byte)((isample & 0x00ff0000) >> 16); |
| currTile[tcount++] = |
| (byte)((isample & 0xff000000) >> 24); |
| } |
| } |
| } |
| } else { |
| if(isRescaling) { |
| long[] maxIn = new long[numBands]; |
| long[] halfIn = new long[numBands]; |
| long maxOut = (1L << (long)bitDepth) - 1L; |
| |
| for (int b = 0; b < numBands; b++) { |
| maxIn[b] = ((1L << (long)sampleSize[b]) - 1L); |
| halfIn[b] = maxIn[b]/2; |
| } |
| |
| if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| long sampleOut = |
| (samples[s + b]*maxOut + halfIn[b])/ |
| maxIn[b]; |
| currTile[tcount++] = |
| (byte)((sampleOut & 0xff000000) >> 24); |
| currTile[tcount++] = |
| (byte)((sampleOut & 0x00ff0000) >> 16); |
| currTile[tcount++] = |
| (byte)((sampleOut & 0x0000ff00) >> 8); |
| currTile[tcount++] = |
| (byte)(sampleOut & 0x000000ff); |
| } |
| } |
| } else { // ByteOrder.LITLE_ENDIAN |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| long sampleOut = |
| (samples[s + b]*maxOut + halfIn[b])/ |
| maxIn[b]; |
| currTile[tcount++] = |
| (byte)(sampleOut & 0x000000ff); |
| currTile[tcount++] = |
| (byte)((sampleOut & 0x0000ff00) >> 8); |
| currTile[tcount++] = |
| (byte)((sampleOut & 0x00ff0000) >> 16); |
| currTile[tcount++] = |
| (byte)((sampleOut & 0xff000000) >> 24); |
| } |
| } |
| } |
| } else { |
| if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| int isample = samples[s + b]; |
| currTile[tcount++] = |
| (byte)((isample & 0xff000000) >> 24); |
| currTile[tcount++] = |
| (byte)((isample & 0x00ff0000) >> 16); |
| currTile[tcount++] = |
| (byte)((isample & 0x0000ff00) >> 8); |
| currTile[tcount++] = |
| (byte)(isample & 0x000000ff); |
| } |
| } |
| } else { // ByteOrder.LITLE_ENDIAN |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| int isample = samples[s + b]; |
| currTile[tcount++] = |
| (byte)(isample & 0x000000ff); |
| currTile[tcount++] = |
| (byte)((isample & 0x0000ff00) >> 8); |
| currTile[tcount++] = |
| (byte)((isample & 0x00ff0000) >> 16); |
| currTile[tcount++] = |
| (byte)((isample & 0xff000000) >> 24); |
| } |
| } |
| } |
| } |
| } |
| break; |
| |
| case 64: |
| if(sampleFormat == |
| BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT) { |
| if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| double dsample = dsamples[s + b]; |
| long lsample = Double.doubleToLongBits(dsample); |
| currTile[tcount++] = |
| (byte)((lsample & 0xff00000000000000L) >> 56); |
| currTile[tcount++] = |
| (byte)((lsample & 0x00ff000000000000L) >> 48); |
| currTile[tcount++] = |
| (byte)((lsample & 0x0000ff0000000000L) >> 40); |
| currTile[tcount++] = |
| (byte)((lsample & 0x000000ff00000000L) >> 32); |
| currTile[tcount++] = |
| (byte)((lsample & 0x00000000ff000000L) >> 24); |
| currTile[tcount++] = |
| (byte)((lsample & 0x0000000000ff0000L) >> 16); |
| currTile[tcount++] = |
| (byte)((lsample & 0x000000000000ff00L) >> 8); |
| currTile[tcount++] = |
| (byte)(lsample & 0x00000000000000ffL); |
| } |
| } |
| } else { // ByteOrder.LITLE_ENDIAN |
| for (int s = 0; s < numSamples; s += xSkip) { |
| for (int b = 0; b < numBands; b++) { |
| double dsample = dsamples[s + b]; |
| long lsample = Double.doubleToLongBits(dsample); |
| currTile[tcount++] = |
| (byte)(lsample & 0x00000000000000ffL); |
| currTile[tcount++] = |
| (byte)((lsample & 0x000000000000ff00L) >> 8); |
| currTile[tcount++] = |
| (byte)((lsample & 0x0000000000ff0000L) >> 16); |
| currTile[tcount++] = |
| (byte)((lsample & 0x00000000ff000000L) >> 24); |
| currTile[tcount++] = |
| (byte)((lsample & 0x000000ff00000000L) >> 32); |
| currTile[tcount++] = |
| (byte)((lsample & 0x0000ff0000000000L) >> 40); |
| currTile[tcount++] = |
| (byte)((lsample & 0x00ff000000000000L) >> 48); |
| currTile[tcount++] = |
| (byte)((lsample & 0xff00000000000000L) >> 56); |
| } |
| } |
| } |
| } |
| break; |
| } |
| } |
| |
| int[] bitsPerSample = new int[numBands]; |
| for (int i = 0; i < bitsPerSample.length; i++) { |
| bitsPerSample[i] = bitDepth; |
| } |
| |
| int byteCount = compressor.encode(currTile, 0, |
| hpixels, vpixels, |
| bitsPerSample, |
| bytesPerRow); |
| return byteCount; |
| } |
| |
| // Check two int arrays for value equality, always returns false |
| // if either array is null |
| private boolean equals(int[] s0, int[] s1) { |
| if (s0 == null || s1 == null) { |
| return false; |
| } |
| if (s0.length != s1.length) { |
| return false; |
| } |
| for (int i = 0; i < s0.length; i++) { |
| if (s0[i] != s1[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Initialize the scale/scale0 or scaleh/scalel arrays to |
| // hold the results of scaling an input value to the desired |
| // output bit depth |
| private void initializeScaleTables(int[] sampleSize) { |
| // Save the sample size in the instance variable. |
| |
| // If the existing tables are still valid, just return. |
| if (bitDepth == scalingBitDepth && |
| equals(sampleSize, this.sampleSize)) { |
| return; |
| } |
| |
| // Reset scaling variables. |
| isRescaling = false; |
| scalingBitDepth = -1; |
| scale = scalel = scaleh = null; |
| scale0 = null; |
| |
| // Set global sample size to parameter. |
| this.sampleSize = sampleSize; |
| |
| // Check whether rescaling is called for. |
| if(bitDepth <= 16) { |
| for(int b = 0; b < numBands; b++) { |
| if(sampleSize[b] != bitDepth) { |
| isRescaling = true; |
| break; |
| } |
| } |
| } |
| |
| // If not rescaling then return after saving the sample size. |
| if(!isRescaling) { |
| return; |
| } |
| |
| // Compute new tables |
| this.scalingBitDepth = bitDepth; |
| int maxOutSample = (1 << bitDepth) - 1; |
| if (bitDepth <= 8) { |
| scale = new byte[numBands][]; |
| for (int b = 0; b < numBands; b++) { |
| int maxInSample = (1 << sampleSize[b]) - 1; |
| int halfMaxInSample = maxInSample/2; |
| scale[b] = new byte[maxInSample + 1]; |
| for (int s = 0; s <= maxInSample; s++) { |
| scale[b][s] = |
| (byte)((s*maxOutSample + halfMaxInSample)/maxInSample); |
| } |
| } |
| scale0 = scale[0]; |
| scaleh = scalel = null; |
| } else if(bitDepth <= 16) { |
| // Divide scaling table into high and low bytes |
| scaleh = new byte[numBands][]; |
| scalel = new byte[numBands][]; |
| |
| for (int b = 0; b < numBands; b++) { |
| int maxInSample = (1 << sampleSize[b]) - 1; |
| int halfMaxInSample = maxInSample/2; |
| scaleh[b] = new byte[maxInSample + 1]; |
| scalel[b] = new byte[maxInSample + 1]; |
| for (int s = 0; s <= maxInSample; s++) { |
| int val = (s*maxOutSample + halfMaxInSample)/maxInSample; |
| scaleh[b][s] = (byte)(val >> 8); |
| scalel[b][s] = (byte)(val & 0xff); |
| } |
| } |
| scale = null; |
| scale0 = null; |
| } |
| } |
| |
| public void write(IIOMetadata sm, |
| IIOImage iioimage, |
| ImageWriteParam p) throws IOException { |
| if (stream == null) { |
| throw new IllegalStateException("output == null!"); |
| } |
| markPositions(); |
| write(sm, iioimage, p, true, true); |
| if (abortRequested()) { |
| resetPositions(); |
| } |
| } |
| |
| private void writeHeader() throws IOException { |
| if (streamMetadata != null) { |
| this.byteOrder = streamMetadata.byteOrder; |
| } else { |
| this.byteOrder = ByteOrder.BIG_ENDIAN; |
| } |
| |
| stream.setByteOrder(byteOrder); |
| if (byteOrder == ByteOrder.BIG_ENDIAN) { |
| stream.writeShort(0x4d4d); |
| } else { |
| stream.writeShort(0x4949); |
| } |
| |
| stream.writeShort(42); // Magic number |
| stream.writeInt(0); // Offset of first IFD (0 == none) |
| |
| nextSpace = stream.getStreamPosition(); |
| headerPosition = nextSpace - 8; |
| } |
| |
| private void write(IIOMetadata sm, |
| IIOImage iioimage, |
| ImageWriteParam p, |
| boolean writeHeader, |
| boolean writeData) throws IOException { |
| if (stream == null) { |
| throw new IllegalStateException("output == null!"); |
| } |
| if (iioimage == null) { |
| throw new IllegalArgumentException("image == null!"); |
| } |
| if(iioimage.hasRaster() && !canWriteRasters()) { |
| throw new UnsupportedOperationException |
| ("TIFF ImageWriter cannot write Rasters!"); |
| } |
| |
| this.image = iioimage.getRenderedImage(); |
| SampleModel sampleModel = image.getSampleModel(); |
| |
| this.sourceXOffset = image.getMinX(); |
| this.sourceYOffset = image.getMinY(); |
| this.sourceWidth = image.getWidth(); |
| this.sourceHeight = image.getHeight(); |
| |
| Rectangle imageBounds = new Rectangle(sourceXOffset, |
| sourceYOffset, |
| sourceWidth, |
| sourceHeight); |
| |
| ColorModel colorModel = null; |
| if (p == null) { |
| this.param = getDefaultWriteParam(); |
| this.sourceBands = null; |
| this.periodX = 1; |
| this.periodY = 1; |
| this.numBands = sampleModel.getNumBands(); |
| colorModel = image.getColorModel(); |
| } else { |
| this.param = p; |
| |
| // Get source region and subsampling factors |
| Rectangle sourceRegion = param.getSourceRegion(); |
| if (sourceRegion != null) { |
| // Clip to actual image bounds |
| sourceRegion = sourceRegion.intersection(imageBounds); |
| |
| sourceXOffset = sourceRegion.x; |
| sourceYOffset = sourceRegion.y; |
| sourceWidth = sourceRegion.width; |
| sourceHeight = sourceRegion.height; |
| } |
| |
| // Adjust for subsampling offsets |
| int gridX = param.getSubsamplingXOffset(); |
| int gridY = param.getSubsamplingYOffset(); |
| this.sourceXOffset += gridX; |
| this.sourceYOffset += gridY; |
| this.sourceWidth -= gridX; |
| this.sourceHeight -= gridY; |
| |
| // Get subsampling factors |
| this.periodX = param.getSourceXSubsampling(); |
| this.periodY = param.getSourceYSubsampling(); |
| |
| int[] sBands = param.getSourceBands(); |
| if (sBands != null) { |
| sourceBands = sBands; |
| this.numBands = sourceBands.length; |
| } else { |
| this.numBands = sampleModel.getNumBands(); |
| } |
| |
| ImageTypeSpecifier destType = p.getDestinationType(); |
| if(destType != null) { |
| ColorModel cm = destType.getColorModel(); |
| if(cm.getNumComponents() == numBands) { |
| colorModel = cm; |
| } |
| } |
| |
| if(colorModel == null) { |
| colorModel = image.getColorModel(); |
| } |
| } |
| |
| this.imageType = new ImageTypeSpecifier(colorModel, sampleModel); |
| |
| ImageUtil.canEncodeImage(this, this.imageType); |
| |
| // Compute output dimensions |
| int destWidth = (sourceWidth + periodX - 1)/periodX; |
| int destHeight = (sourceHeight + periodY - 1)/periodY; |
| if (destWidth <= 0 || destHeight <= 0) { |
| throw new IllegalArgumentException("Empty source region!"); |
| } |
| |
| clearAbortRequest(); |
| processImageStarted(0); |
| if (abortRequested()) { |
| processWriteAborted(); |
| return; |
| } |
| |
| // Optionally write the header. |
| if (writeHeader) { |
| // Clear previous stream metadata. |
| this.streamMetadata = null; |
| |
| // Try to convert non-null input stream metadata. |
| if (sm != null) { |
| this.streamMetadata = |
| (TIFFStreamMetadata)convertStreamMetadata(sm, param); |
| } |
| |
| // Set to default if not converted. |
| if(this.streamMetadata == null) { |
| this.streamMetadata = |
| (TIFFStreamMetadata)getDefaultStreamMetadata(param); |
| } |
| |
| // Write the header. |
| writeHeader(); |
| |
| // Seek to the position of the IFD pointer in the header. |
| stream.seek(headerPosition + 4); |
| |
| // Ensure IFD is written on a word boundary |
| nextSpace = (nextSpace + 3) & ~0x3; |
| |
| // Write the pointer to the first IFD after the header. |
| stream.writeInt((int)nextSpace); |
| } |
| |
| // Write out the IFD and any sub IFDs, followed by a zero |
| |
| // Clear previous image metadata. |
| this.imageMetadata = null; |
| |
| // Initialize the metadata object. |
| IIOMetadata im = iioimage.getMetadata(); |
| if(im != null) { |
| if (im instanceof TIFFImageMetadata) { |
| // Clone the one passed in. |
| this.imageMetadata = ((TIFFImageMetadata)im).getShallowClone(); |
| } else if(Arrays.asList(im.getMetadataFormatNames()).contains( |
| TIFFImageMetadata.NATIVE_METADATA_FORMAT_NAME)) { |
| this.imageMetadata = convertNativeImageMetadata(im); |
| } else if(im.isStandardMetadataFormatSupported()) { |
| // Convert standard metadata. |
| this.imageMetadata = convertStandardImageMetadata(im); |
| } |
| if (this.imageMetadata == null) { |
| processWarningOccurred(currentImage, |
| "Could not initialize image metadata"); |
| } |
| } |
| |
| // Use default metadata if still null. |
| if(this.imageMetadata == null) { |
| this.imageMetadata = |
| (TIFFImageMetadata)getDefaultImageMetadata(this.imageType, |
| this.param); |
| } |
| |
| // Set or overwrite mandatory fields in the root IFD |
| setupMetadata(colorModel, sampleModel, destWidth, destHeight); |
| |
| // Set compressor fields. |
| compressor.setWriter(this); |
| // Metadata needs to be set on the compressor before the IFD is |
| // written as the compressor could modify the metadata. |
| compressor.setMetadata(imageMetadata); |
| compressor.setStream(stream); |
| |
| // Initialize scaling tables for this image |
| sampleSize = sampleModel.getSampleSize(); |
| initializeScaleTables(sampleModel.getSampleSize()); |
| |
| // Determine whether bilevel. |
| this.isBilevel = ImageUtil.isBinary(this.image.getSampleModel()); |
| |
| // Check for photometric inversion. |
| this.isInverted = |
| (nativePhotometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO && |
| photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO) || |
| (nativePhotometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO && |
| photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO); |
| |
| // Analyze image data suitability for direct copy. |
| this.isImageSimple = |
| (isBilevel || |
| (!isInverted && ImageUtil.imageIsContiguous(this.image))) && |
| !isRescaling && // no value rescaling |
| sourceBands == null && // no subbanding |
| periodX == 1 && periodY == 1 && // no subsampling |
| colorConverter == null; |
| |
| TIFFIFD rootIFD = imageMetadata.getRootIFD(); |
| |
| rootIFD.writeToStream(stream); |
| |
| this.nextIFDPointerPos = stream.getStreamPosition(); |
| stream.writeInt(0); |
| |
| // Seek to end of IFD data |
| long lastIFDPosition = rootIFD.getLastPosition(); |
| stream.seek(lastIFDPosition); |
| if(lastIFDPosition > this.nextSpace) { |
| this.nextSpace = lastIFDPosition; |
| } |
| |
| // If not writing the image data, i.e., if writing or inserting an |
| // empty image, return. |
| if(!writeData) { |
| return; |
| } |
| |
| // Get positions of fields within the IFD to update as we write |
| // each strip or tile |
| long stripOrTileByteCountsPosition = |
| rootIFD.getStripOrTileByteCountsPosition(); |
| long stripOrTileOffsetsPosition = |
| rootIFD.getStripOrTileOffsetsPosition(); |
| |
| // Compute total number of pixels for progress notification |
| this.totalPixels = tileWidth*tileLength*tilesDown*tilesAcross; |
| this.pixelsDone = 0; |
| |
| // Write the image, a strip or tile at a time |
| for (int tj = 0; tj < tilesDown; tj++) { |
| for (int ti = 0; ti < tilesAcross; ti++) { |
| long pos = stream.getStreamPosition(); |
| |
| // Write the (possibly compressed) tile data |
| |
| Rectangle tileRect = |
| new Rectangle(sourceXOffset + ti*tileWidth*periodX, |
| sourceYOffset + tj*tileLength*periodY, |
| tileWidth*periodX, |
| tileLength*periodY); |
| |
| try { |
| int byteCount = writeTile(tileRect, compressor); |
| |
| if(pos + byteCount > nextSpace) { |
| nextSpace = pos + byteCount; |
| } |
| |
| // Fill in the offset and byte count for the file |
| stream.mark(); |
| stream.seek(stripOrTileOffsetsPosition); |
| stream.writeInt((int)pos); |
| stripOrTileOffsetsPosition += 4; |
| |
| stream.seek(stripOrTileByteCountsPosition); |
| stream.writeInt(byteCount); |
| stripOrTileByteCountsPosition += 4; |
| stream.reset(); |
| |
| pixelsDone += tileRect.width*tileRect.height; |
| processImageProgress(100.0F*pixelsDone/totalPixels); |
| if (abortRequested()) { |
| processWriteAborted(); |
| return; |
| } |
| } catch (IOException e) { |
| throw new IIOException("I/O error writing TIFF file!", e); |
| } |
| } |
| } |
| |
| processImageComplete(); |
| currentImage++; |
| } |
| |
| public boolean canWriteSequence() { |
| return true; |
| } |
| |
| public void prepareWriteSequence(IIOMetadata streamMetadata) |
| throws IOException { |
| if (getOutput() == null) { |
| throw new IllegalStateException("getOutput() == null!"); |
| } |
| |
| // Set up stream metadata. |
| if (streamMetadata != null) { |
| streamMetadata = convertStreamMetadata(streamMetadata, null); |
| } |
| if(streamMetadata == null) { |
| streamMetadata = getDefaultStreamMetadata(null); |
| } |
| this.streamMetadata = (TIFFStreamMetadata)streamMetadata; |
| |
| // Write the header. |
| writeHeader(); |
| |
| // Set the sequence flag. |
| this.isWritingSequence = true; |
| } |
| |
| public void writeToSequence(IIOImage image, ImageWriteParam param) |
| throws IOException { |
| // Check sequence flag. |
| if(!this.isWritingSequence) { |
| throw new IllegalStateException |
| ("prepareWriteSequence() has not been called!"); |
| } |
| |
| // Append image. |
| writeInsert(-1, image, param); |
| } |
| |
| public void endWriteSequence() throws IOException { |
| // Check output. |
| if (getOutput() == null) { |
| throw new IllegalStateException("getOutput() == null!"); |
| } |
| |
| // Check sequence flag. |
| if(!isWritingSequence) { |
| throw new IllegalStateException |
| ("prepareWriteSequence() has not been called!"); |
| } |
| |
| // Unset sequence flag. |
| this.isWritingSequence = false; |
| |
| // Position the stream at the end, not at the next IFD pointer position. |
| long streamLength = this.stream.length(); |
| if (streamLength != -1) { |
| stream.seek(streamLength); |
| } |
| } |
| |
| public boolean canInsertImage(int imageIndex) throws IOException { |
| if (getOutput() == null) { |
| throw new IllegalStateException("getOutput() == null!"); |
| } |
| |
| // Mark position as locateIFD() will seek to IFD at imageIndex. |
| stream.mark(); |
| |
| // locateIFD() will throw an IndexOutOfBoundsException if |
| // imageIndex is < -1 or is too big thereby satisfying the spec. |
| long[] ifdpos = new long[1]; |
| long[] ifd = new long[1]; |
| locateIFD(imageIndex, ifdpos, ifd); |
| |
| // Reset to position before locateIFD(). |
| stream.reset(); |
| |
| return true; |
| } |
| |
| // Locate start of IFD for image. |
| // Throws IIOException if not at a TIFF header and |
| // IndexOutOfBoundsException if imageIndex is < -1 or is too big. |
| private void locateIFD(int imageIndex, long[] ifdpos, long[] ifd) |
| throws IOException { |
| |
| if(imageIndex < -1) { |
| throw new IndexOutOfBoundsException("imageIndex < -1!"); |
| } |
| |
| long startPos = stream.getStreamPosition(); |
| |
| stream.seek(headerPosition); |
| int byteOrder = stream.readUnsignedShort(); |
| if (byteOrder == 0x4d4d) { |
| stream.setByteOrder(ByteOrder.BIG_ENDIAN); |
| } else if (byteOrder == 0x4949) { |
| stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); |
| } else { |
| stream.seek(startPos); |
| throw new IIOException("Illegal byte order"); |
| } |
| if (stream.readUnsignedShort() != 42) { |
| stream.seek(startPos); |
| throw new IIOException("Illegal magic number"); |
| } |
| |
| ifdpos[0] = stream.getStreamPosition(); |
| ifd[0] = stream.readUnsignedInt(); |
| if (ifd[0] == 0) { |
| // imageIndex has to be >= -1 due to check above. |
| if(imageIndex > 0) { |
| stream.seek(startPos); |
| throw new IndexOutOfBoundsException |
| ("imageIndex is greater than the largest available index!"); |
| } |
| return; |
| } |
| stream.seek(ifd[0]); |
| |
| for (int i = 0; imageIndex == -1 || i < imageIndex; i++) { |
| int numFields; |
| try { |
| numFields = stream.readShort(); |
| } catch (EOFException eof) { |
| stream.seek(startPos); |
| ifd[0] = 0; |
| return; |
| } |
| |
| stream.skipBytes(12*numFields); |
| |
| ifdpos[0] = stream.getStreamPosition(); |
| ifd[0] = stream.readUnsignedInt(); |
| if (ifd[0] == 0) { |
| if (imageIndex != -1 && i < imageIndex - 1) { |
| stream.seek(startPos); |
| throw new IndexOutOfBoundsException( |
| "imageIndex is greater than the largest available index!"); |
| } |
| break; |
| } |
| stream.seek(ifd[0]); |
| } |
| } |
| |
| public void writeInsert(int imageIndex, |
| IIOImage image, |
| ImageWriteParam param) throws IOException { |
| int currentImageCached = currentImage; |
| try { |
| insert(imageIndex, image, param, true); |
| } catch (Exception e) { |
| throw e; |
| } finally { |
| currentImage = currentImageCached; |
| } |
| } |
| |
| private void insert(int imageIndex, |
| IIOImage image, |
| ImageWriteParam param, |
| boolean writeData) throws IOException { |
| if (stream == null) { |
| throw new IllegalStateException("Output not set!"); |
| } |
| if (image == null) { |
| throw new IllegalArgumentException("image == null!"); |
| } |
| |
| // Locate the position of the old IFD (ifd) and the location |
| // of the pointer to that position (ifdpos). |
| long[] ifdpos = new long[1]; |
| long[] ifd = new long[1]; |
| |
| // locateIFD() will throw an IndexOutOfBoundsException if |
| // imageIndex is < -1 or is too big thereby satisfying the spec. |
| locateIFD(imageIndex, ifdpos, ifd); |
| |
| markPositions(); |
| |
| // Seek to the position containing the pointer to the old IFD. |
| stream.seek(ifdpos[0]); |
| |
| // Save the previous pointer value in case of abort. |
| stream.mark(); |
| long prevPointerValue = stream.readUnsignedInt(); |
| stream.reset(); |
| |
| // Update next space pointer in anticipation of next write. |
| if(ifdpos[0] + 4 > nextSpace) { |
| nextSpace = ifdpos[0] + 4; |
| } |
| |
| // Ensure IFD is written on a word boundary |
| nextSpace = (nextSpace + 3) & ~0x3; |
| |
| // Update the value to point to the next available space. |
| stream.writeInt((int)nextSpace); |
| |
| // Seek to the next available space. |
| stream.seek(nextSpace); |
| |
| // Write the image (IFD and data). |
| write(null, image, param, false, writeData); |
| |
| // Seek to the position containing the pointer in the new IFD. |
| stream.seek(nextIFDPointerPos); |
| |
| // Update the new IFD to point to the old IFD. |
| stream.writeInt((int)ifd[0]); |
| // Don't need to update nextSpace here as already done in write(). |
| |
| if (abortRequested()) { |
| stream.seek(ifdpos[0]); |
| stream.writeInt((int)prevPointerValue); |
| resetPositions(); |
| } |
| } |
| |
| // ----- BEGIN insert/writeEmpty methods ----- |
| |
| private boolean isEncodingEmpty() { |
| return isInsertingEmpty || isWritingEmpty; |
| } |
| |
| public boolean canInsertEmpty(int imageIndex) throws IOException { |
| return canInsertImage(imageIndex); |
| } |
| |
| public boolean canWriteEmpty() throws IOException { |
| if (getOutput() == null) { |
| throw new IllegalStateException("getOutput() == null!"); |
| } |
| return true; |
| } |
| |
| // Check state and parameters for writing or inserting empty images. |
| private void checkParamsEmpty(ImageTypeSpecifier imageType, |
| int width, |
| int height, |
| List<? extends BufferedImage> thumbnails) { |
| if (getOutput() == null) { |
| throw new IllegalStateException("getOutput() == null!"); |
| } |
| |
| if(imageType == null) { |
| throw new IllegalArgumentException("imageType == null!"); |
| } |
| |
| if(width < 1 || height < 1) { |
| throw new IllegalArgumentException("width < 1 || height < 1!"); |
| } |
| |
| if(thumbnails != null) { |
| int numThumbs = thumbnails.size(); |
| for(int i = 0; i < numThumbs; i++) { |
| Object thumb = thumbnails.get(i); |
| if(thumb == null || !(thumb instanceof BufferedImage)) { |
| throw new IllegalArgumentException |
| ("thumbnails contains null references or objects other than BufferedImages!"); |
| } |
| } |
| } |
| |
| if(this.isInsertingEmpty) { |
| throw new IllegalStateException |
| ("Previous call to prepareInsertEmpty() without corresponding call to endInsertEmpty()!"); |
| } |
| |
| if(this.isWritingEmpty) { |
| throw new IllegalStateException |
| ("Previous call to prepareWriteEmpty() without corresponding call to endWriteEmpty()!"); |
| } |
| } |
| |
| public void prepareInsertEmpty(int imageIndex, |
| ImageTypeSpecifier imageType, |
| int width, |
| int height, |
| IIOMetadata imageMetadata, |
| List<? extends BufferedImage> thumbnails, |
| ImageWriteParam param) throws IOException { |
| checkParamsEmpty(imageType, width, height, thumbnails); |
| |
| this.isInsertingEmpty = true; |
| |
| SampleModel emptySM = imageType.getSampleModel(); |
| RenderedImage emptyImage = |
| new EmptyImage(0, 0, width, height, |
| 0, 0, emptySM.getWidth(), emptySM.getHeight(), |
| emptySM, imageType.getColorModel()); |
| |
| insert(imageIndex, new IIOImage(emptyImage, null, imageMetadata), |
| param, false); |
| } |
| |
| public void prepareWriteEmpty(IIOMetadata streamMetadata, |
| ImageTypeSpecifier imageType, |
| int width, |
| int height, |
| IIOMetadata imageMetadata, |
| List<? extends BufferedImage> thumbnails, |
| ImageWriteParam param) throws IOException { |
| if (stream == null) { |
| throw new IllegalStateException("output == null!"); |
| } |
| |
| checkParamsEmpty(imageType, width, height, thumbnails); |
| |
| this.isWritingEmpty = true; |
| |
| SampleModel emptySM = imageType.getSampleModel(); |
| RenderedImage emptyImage = |
| new EmptyImage(0, 0, width, height, |
| 0, 0, emptySM.getWidth(), emptySM.getHeight(), |
| emptySM, imageType.getColorModel()); |
| |
| markPositions(); |
| write(streamMetadata, new IIOImage(emptyImage, null, imageMetadata), |
| param, true, false); |
| if (abortRequested()) { |
| resetPositions(); |
| } |
| } |
| |
| public void endInsertEmpty() throws IOException { |
| if (getOutput() == null) { |
| throw new IllegalStateException("getOutput() == null!"); |
| } |
| |
| if(!this.isInsertingEmpty) { |
| throw new IllegalStateException |
| ("No previous call to prepareInsertEmpty()!"); |
| } |
| |
| if(this.isWritingEmpty) { |
| throw new IllegalStateException |
| ("Previous call to prepareWriteEmpty() without corresponding call to endWriteEmpty()!"); |
| } |
| |
| if (inReplacePixelsNest) { |
| throw new IllegalStateException |
| ("In nested call to prepareReplacePixels!"); |
| } |
| |
| this.isInsertingEmpty = false; |
| } |
| |
| public void endWriteEmpty() throws IOException { |
| if (getOutput() == null) { |
| throw new IllegalStateException("getOutput() == null!"); |
| } |
| |
| if(!this.isWritingEmpty) { |
| throw new IllegalStateException |
| ("No previous call to prepareWriteEmpty()!"); |
| } |
| |
| if(this.isInsertingEmpty) { |
| throw new IllegalStateException |
| ("Previous call to prepareInsertEmpty() without corresponding call to endInsertEmpty()!"); |
| } |
| |
| if (inReplacePixelsNest) { |
| throw new IllegalStateException |
| ("In nested call to prepareReplacePixels!"); |
| } |
| |
| this.isWritingEmpty = false; |
| } |
| |
| // ----- END insert/writeEmpty methods ----- |
| |
| // ----- BEGIN replacePixels methods ----- |
| |
| private TIFFIFD readIFD(int imageIndex) throws IOException { |
| if (stream == null) { |
| throw new IllegalStateException("Output not set!"); |
| } |
| if (imageIndex < 0) { |
| throw new IndexOutOfBoundsException("imageIndex < 0!"); |
| } |
| |
| stream.mark(); |
| long[] ifdpos = new long[1]; |
| long[] ifd = new long[1]; |
| locateIFD(imageIndex, ifdpos, ifd); |
| if (ifd[0] == 0) { |
| stream.reset(); |
| throw new IndexOutOfBoundsException |
| ("imageIndex out of bounds!"); |
| } |
| |
| List<TIFFTagSet> tagSets = new ArrayList<TIFFTagSet>(1); |
| tagSets.add(BaselineTIFFTagSet.getInstance()); |
| TIFFIFD rootIFD = new TIFFIFD(tagSets); |
| rootIFD.initialize(stream, true, false, false); |
| stream.reset(); |
| |
| return rootIFD; |
| } |
| |
| public boolean canReplacePixels(int imageIndex) throws IOException { |
| if (getOutput() == null) { |
| throw new IllegalStateException("getOutput() == null!"); |
| } |
| |
| TIFFIFD rootIFD = readIFD(imageIndex); |
| TIFFField f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_COMPRESSION); |
| int compression = f.getAsInt(0); |
| |
| return compression == BaselineTIFFTagSet.COMPRESSION_NONE; |
| } |
| |
| private Object replacePixelsLock = new Object(); |
| |
| private int replacePixelsIndex = -1; |
| private TIFFImageMetadata replacePixelsMetadata = null; |
| private long[] replacePixelsTileOffsets = null; |
| private long[] replacePixelsByteCounts = null; |
| private long replacePixelsOffsetsPosition = 0L; |
| private long replacePixelsByteCountsPosition = 0L; |
| private Rectangle replacePixelsRegion = null; |
| private boolean inReplacePixelsNest = false; |
| |
| private TIFFImageReader reader = null; |
| |
| public void prepareReplacePixels(int imageIndex, |
| Rectangle region) throws IOException { |
| synchronized(replacePixelsLock) { |
| // Check state and parameters vis-a-vis ImageWriter specification. |
| if (stream == null) { |
| throw new IllegalStateException("Output not set!"); |
| } |
| if (region == null) { |
| throw new IllegalArgumentException("region == null!"); |
| } |
| if (region.getWidth() < 1) { |
| throw new IllegalArgumentException("region.getWidth() < 1!"); |
| } |
| if (region.getHeight() < 1) { |
| throw new IllegalArgumentException("region.getHeight() < 1!"); |
| } |
| if (inReplacePixelsNest) { |
| throw new IllegalStateException |
| ("In nested call to prepareReplacePixels!"); |
| } |
| |
| // Read the IFD for the pixel replacement index. |
| TIFFIFD replacePixelsIFD = readIFD(imageIndex); |
| |
| // Ensure that compression is "none". |
| TIFFField f = |
| replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_COMPRESSION); |
| int compression = f.getAsInt(0); |
| if (compression != BaselineTIFFTagSet.COMPRESSION_NONE) { |
| throw new UnsupportedOperationException |
| ("canReplacePixels(imageIndex) == false!"); |
| } |
| |
| // Get the image dimensions. |
| f = |
| replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_IMAGE_WIDTH); |
| if(f == null) { |
| throw new IIOException("Cannot read ImageWidth field."); |
| } |
| int w = f.getAsInt(0); |
| |
| f = |
| replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_IMAGE_LENGTH); |
| if(f == null) { |
| throw new IIOException("Cannot read ImageHeight field."); |
| } |
| int h = f.getAsInt(0); |
| |
| // Create image bounds. |
| Rectangle bounds = new Rectangle(0, 0, w, h); |
| |
| // Intersect region with bounds. |
| region = region.intersection(bounds); |
| |
| // Check for empty intersection. |
| if(region.isEmpty()) { |
| throw new IIOException("Region does not intersect image bounds"); |
| } |
| |
| // Save the region. |
| replacePixelsRegion = region; |
| |
| // Get the tile offsets. |
| f = replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_TILE_OFFSETS); |
| if(f == null) { |
| f = replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_STRIP_OFFSETS); |
| } |
| replacePixelsTileOffsets = f.getAsLongs(); |
| |
| // Get the byte counts. |
| f = replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_TILE_BYTE_COUNTS); |
| if(f == null) { |
| f = replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_STRIP_BYTE_COUNTS); |
| } |
| replacePixelsByteCounts = f.getAsLongs(); |
| |
| replacePixelsOffsetsPosition = |
| replacePixelsIFD.getStripOrTileOffsetsPosition(); |
| replacePixelsByteCountsPosition = |
| replacePixelsIFD.getStripOrTileByteCountsPosition(); |
| |
| // Get the image metadata. |
| replacePixelsMetadata = new TIFFImageMetadata(replacePixelsIFD); |
| |
| // Save the image index. |
| replacePixelsIndex = imageIndex; |
| |
| // Set the pixel replacement flag. |
| inReplacePixelsNest = true; |
| } |
| } |
| |
| private Raster subsample(Raster raster, int[] sourceBands, |
| int subOriginX, int subOriginY, |
| int subPeriodX, int subPeriodY, |
| int dstOffsetX, int dstOffsetY, |
| Rectangle target) { |
| |
| int x = raster.getMinX(); |
| int y = raster.getMinY(); |
| int w = raster.getWidth(); |
| int h = raster.getHeight(); |
| int b = raster.getSampleModel().getNumBands(); |
| int t = raster.getSampleModel().getDataType(); |
| |
| int outMinX = XToTileX(x, subOriginX, subPeriodX) + dstOffsetX; |
| int outMinY = YToTileY(y, subOriginY, subPeriodY) + dstOffsetY; |
| int outMaxX = XToTileX(x + w - 1, subOriginX, subPeriodX) + dstOffsetX; |
| int outMaxY = YToTileY(y + h - 1, subOriginY, subPeriodY) + dstOffsetY; |
| int outWidth = outMaxX - outMinX + 1; |
| int outHeight = outMaxY - outMinY + 1; |
| |
| if(outWidth <= 0 || outHeight <= 0) return null; |
| |
| int inMinX = (outMinX - dstOffsetX)*subPeriodX + subOriginX; |
| int inMaxX = (outMaxX - dstOffsetX)*subPeriodX + subOriginX; |
| int inWidth = inMaxX - inMinX + 1; |
| int inMinY = (outMinY - dstOffsetY)*subPeriodY + subOriginY; |
| int inMaxY = (outMaxY - dstOffsetY)*subPeriodY + subOriginY; |
| int inHeight = inMaxY - inMinY + 1; |
| |
| WritableRaster wr = |
| raster.createCompatibleWritableRaster(outMinX, outMinY, |
| outWidth, outHeight); |
| |
| int jMax = inMinY + inHeight; |
| |
| if(t == DataBuffer.TYPE_FLOAT) { |
| float[] fsamples = new float[inWidth]; |
| float[] fsubsamples = new float[outWidth]; |
| |
| for(int k = 0; k < b; k++) { |
| int outY = outMinY; |
| for(int j = inMinY; j < jMax; j += subPeriodY) { |
| raster.getSamples(inMinX, j, inWidth, 1, k, fsamples); |
| int s = 0; |
| for(int i = 0; i < inWidth; i += subPeriodX) { |
| fsubsamples[s++] = fsamples[i]; |
| } |
| wr.setSamples(outMinX, outY++, outWidth, 1, k, |
| fsubsamples); |
| } |
| } |
| } else if (t == DataBuffer.TYPE_DOUBLE) { |
| double[] dsamples = new double[inWidth]; |
| double[] dsubsamples = new double[outWidth]; |
| |
| for(int k = 0; k < b; k++) { |
| int outY = outMinY; |
| for(int j = inMinY; j < jMax; j += subPeriodY) { |
| raster.getSamples(inMinX, j, inWidth, 1, k, dsamples); |
| int s = 0; |
| for(int i = 0; i < inWidth; i += subPeriodX) { |
| dsubsamples[s++] = dsamples[i]; |
| } |
| wr.setSamples(outMinX, outY++, outWidth, 1, k, |
| dsubsamples); |
| } |
| } |
| } else { |
| int[] samples = new int[inWidth]; |
| int[] subsamples = new int[outWidth]; |
| |
| for(int k = 0; k < b; k++) { |
| int outY = outMinY; |
| for(int j = inMinY; j < jMax; j += subPeriodY) { |
| raster.getSamples(inMinX, j, inWidth, 1, k, samples); |
| int s = 0; |
| for(int i = 0; i < inWidth; i += subPeriodX) { |
| subsamples[s++] = samples[i]; |
| } |
| wr.setSamples(outMinX, outY++, outWidth, 1, k, |
| subsamples); |
| } |
| } |
| } |
| |
| return wr.createChild(outMinX, outMinY, |
| target.width, target.height, |
| target.x, target.y, |
| sourceBands); |
| } |
| |
| public void replacePixels(RenderedImage image, ImageWriteParam param) |
| throws IOException { |
| |
| synchronized(replacePixelsLock) { |
| // Check state and parameters vis-a-vis ImageWriter specification. |
| if (stream == null) { |
| throw new IllegalStateException("stream == null!"); |
| } |
| |
| if (image == null) { |
| throw new IllegalArgumentException("image == null!"); |
| } |
| |
| if (!inReplacePixelsNest) { |
| throw new IllegalStateException |
| ("No previous call to prepareReplacePixels!"); |
| } |
| |
| // Subsampling values. |
| int stepX = 1, stepY = 1, gridX = 0, gridY = 0; |
| |
| // Initialize the ImageWriteParam. |
| if (param == null) { |
| // Use the default. |
| param = getDefaultWriteParam(); |
| } else { |
| // Make a copy of the ImageWriteParam. |
| ImageWriteParam paramCopy = getDefaultWriteParam(); |
| |
| // Force uncompressed. |
| paramCopy.setCompressionMode(ImageWriteParam.MODE_DISABLED); |
| |
| // Force tiling to remain as in the already written image. |
| paramCopy.setTilingMode(ImageWriteParam.MODE_COPY_FROM_METADATA); |
| |
| // Retain source and destination region and band settings. |
| paramCopy.setDestinationOffset(param.getDestinationOffset()); |
| paramCopy.setSourceBands(param.getSourceBands()); |
| paramCopy.setSourceRegion(param.getSourceRegion()); |
| |
| // Save original subsampling values for subsampling the |
| // replacement data - not the data re-read from the image. |
| stepX = param.getSourceXSubsampling(); |
| stepY = param.getSourceYSubsampling(); |
| gridX = param.getSubsamplingXOffset(); |
| gridY = param.getSubsamplingYOffset(); |
| |
| // Replace the param. |
| param = paramCopy; |
| } |
| |
| // Check band count and bit depth compatibility. |
| TIFFField f = |
| replacePixelsMetadata.getTIFFField(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE); |
| if(f == null) { |
| throw new IIOException |
| ("Cannot read destination BitsPerSample"); |
| } |
| int[] dstBitsPerSample = f.getAsInts(); |
| int[] srcBitsPerSample = image.getSampleModel().getSampleSize(); |
| int[] sourceBands = param.getSourceBands(); |
| if(sourceBands != null) { |
| if(sourceBands.length != dstBitsPerSample.length) { |
| throw new IIOException |
| ("Source and destination have different SamplesPerPixel"); |
| } |
| for(int i = 0; i < sourceBands.length; i++) { |
| if(dstBitsPerSample[i] != |
| srcBitsPerSample[sourceBands[i]]) { |
| throw new IIOException |
| ("Source and destination have different BitsPerSample"); |
| } |
| } |
| } else { |
| int srcNumBands = image.getSampleModel().getNumBands(); |
| if(srcNumBands != dstBitsPerSample.length) { |
| throw new IIOException |
| ("Source and destination have different SamplesPerPixel"); |
| } |
| for(int i = 0; i < srcNumBands; i++) { |
| if(dstBitsPerSample[i] != srcBitsPerSample[i]) { |
| throw new IIOException |
| ("Source and destination have different BitsPerSample"); |
| } |
| } |
| } |
| |
| // Get the source image bounds. |
| Rectangle srcImageBounds = |
| new Rectangle(image.getMinX(), image.getMinY(), |
| image.getWidth(), image.getHeight()); |
| |
| // Initialize the source rect. |
| Rectangle srcRect = param.getSourceRegion(); |
| if(srcRect == null) { |
| srcRect = srcImageBounds; |
| } |
| |
| // Set subsampling grid parameters. |
| int subPeriodX = stepX; |
| int subPeriodY = stepY; |
| int subOriginX = gridX + srcRect.x; |
| int subOriginY = gridY + srcRect.y; |
| |
| // Intersect with the source bounds. |
| if(!srcRect.equals(srcImageBounds)) { |
| srcRect = srcRect.intersection(srcImageBounds); |
| if(srcRect.isEmpty()) { |
| throw new IllegalArgumentException |
| ("Source region does not intersect source image!"); |
| } |
| } |
| |
| // Get the destination offset. |
| Point dstOffset = param.getDestinationOffset(); |
| |
| // Forward map source rectangle to determine destination width. |
| int dMinX = XToTileX(srcRect.x, subOriginX, subPeriodX) + |
| dstOffset.x; |
| int dMinY = YToTileY(srcRect.y, subOriginY, subPeriodY) + |
| dstOffset.y; |
| int dMaxX = XToTileX(srcRect.x + srcRect.width, |
| subOriginX, subPeriodX) + dstOffset.x; |
| int dMaxY = YToTileY(srcRect.y + srcRect.height, |
| subOriginY, subPeriodY) + dstOffset.y; |
| |
| // Initialize the destination rectangle. |
| Rectangle dstRect = |
| new Rectangle(dstOffset.x, dstOffset.y, |
| dMaxX - dMinX, dMaxY - dMinY); |
| |
| // Intersect with the replacement region. |
| dstRect = dstRect.intersection(replacePixelsRegion); |
| if(dstRect.isEmpty()) { |
| throw new IllegalArgumentException |
| ("Forward mapped source region does not intersect destination region!"); |
| } |
| |
| // Backward map to the active source region. |
| int activeSrcMinX = (dstRect.x - dstOffset.x)*subPeriodX + |
| subOriginX; |
| int sxmax = |
| (dstRect.x + dstRect.width - 1 - dstOffset.x)*subPeriodX + |
| subOriginX; |
| int activeSrcWidth = sxmax - activeSrcMinX + 1; |
| |
| int activeSrcMinY = (dstRect.y - dstOffset.y)*subPeriodY + |
| subOriginY; |
| int symax = |
| (dstRect.y + dstRect.height - 1 - dstOffset.y)*subPeriodY + |
| subOriginY; |
| int activeSrcHeight = symax - activeSrcMinY + 1; |
| Rectangle activeSrcRect = |
| new Rectangle(activeSrcMinX, activeSrcMinY, |
| activeSrcWidth, activeSrcHeight); |
| if(activeSrcRect.intersection(srcImageBounds).isEmpty()) { |
| throw new IllegalArgumentException |
| ("Backward mapped destination region does not intersect source image!"); |
| } |
| |
| if(reader == null) { |
| reader = new TIFFImageReader(new TIFFImageReaderSpi()); |
| } else { |
| reader.reset(); |
| } |
| |
| stream.mark(); |
| |
| try { |
| stream.seek(headerPosition); |
| reader.setInput(stream); |
| |
| this.imageMetadata = replacePixelsMetadata; |
| this.param = param; |
| SampleModel sm = image.getSampleModel(); |
| ColorModel cm = image.getColorModel(); |
| this.numBands = sm.getNumBands(); |
| this.imageType = new ImageTypeSpecifier(image); |
| this.periodX = param.getSourceXSubsampling(); |
| this.periodY = param.getSourceYSubsampling(); |
| this.sourceBands = null; |
| int[] sBands = param.getSourceBands(); |
| if (sBands != null) { |
| this.sourceBands = sBands; |
| this.numBands = sourceBands.length; |
| } |
| setupMetadata(cm, sm, |
| reader.getWidth(replacePixelsIndex), |
| reader.getHeight(replacePixelsIndex)); |
| int[] scaleSampleSize = sm.getSampleSize(); |
| initializeScaleTables(scaleSampleSize); |
| |
| // Determine whether bilevel. |
| this.isBilevel = ImageUtil.isBinary(image.getSampleModel()); |
| |
| // Check for photometric inversion. |
| this.isInverted = |
| (nativePhotometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO && |
| photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO) || |
| (nativePhotometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO && |
| photometricInterpretation == |
| BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO); |
| |
| // Analyze image data suitability for direct copy. |
| this.isImageSimple = |
| (isBilevel || |
| (!isInverted && ImageUtil.imageIsContiguous(image))) && |
| !isRescaling && // no value rescaling |
| sourceBands == null && // no subbanding |
| periodX == 1 && periodY == 1 && // no subsampling |
| colorConverter == null; |
| |
| int minTileX = XToTileX(dstRect.x, 0, tileWidth); |
| int minTileY = YToTileY(dstRect.y, 0, tileLength); |
| int maxTileX = XToTileX(dstRect.x + dstRect.width - 1, |
| 0, tileWidth); |
| int maxTileY = YToTileY(dstRect.y + dstRect.height - 1, |
| 0, tileLength); |
| |
| TIFFCompressor encoder = new TIFFNullCompressor(); |
| encoder.setWriter(this); |
| encoder.setStream(stream); |
| encoder.setMetadata(this.imageMetadata); |
| |
| Rectangle tileRect = new Rectangle(); |
| for(int ty = minTileY; ty <= maxTileY; ty++) { |
| for(int tx = minTileX; tx <= maxTileX; tx++) { |
| int tileIndex = ty*tilesAcross + tx; |
| boolean isEmpty = |
| replacePixelsByteCounts[tileIndex] == 0L; |
| WritableRaster raster; |
| if(isEmpty) { |
| SampleModel tileSM = |
| sm.createCompatibleSampleModel(tileWidth, |
| tileLength); |
| raster = Raster.createWritableRaster(tileSM, null); |
| } else { |
| BufferedImage tileImage = |
| reader.readTile(replacePixelsIndex, tx, ty); |
| raster = tileImage.getRaster(); |
| } |
| |
| tileRect.setLocation(tx*tileWidth, |
| ty*tileLength); |
| tileRect.setSize(raster.getWidth(), |
| raster.getHeight()); |
| raster = |
| raster.createWritableTranslatedChild(tileRect.x, |
| tileRect.y); |
| |
| Rectangle replacementRect = |
| tileRect.intersection(dstRect); |
| |
| int srcMinX = |
| (replacementRect.x - dstOffset.x)*subPeriodX + |
| subOriginX; |
| int srcXmax = |
| (replacementRect.x + replacementRect.width - 1 - |
| dstOffset.x)*subPeriodX + subOriginX; |
| int srcWidth = srcXmax - srcMinX + 1; |
| |
| int srcMinY = |
| (replacementRect.y - dstOffset.y)*subPeriodY + |
| subOriginY; |
| int srcYMax = |
| (replacementRect.y + replacementRect.height - 1 - |
| dstOffset.y)*subPeriodY + subOriginY; |
| int srcHeight = srcYMax - srcMinY + 1; |
| Rectangle srcTileRect = |
| new Rectangle(srcMinX, srcMinY, |
| srcWidth, srcHeight); |
| |
| Raster replacementData = image.getData(srcTileRect); |
| if(subPeriodX == 1 && subPeriodY == 1 && |
| subOriginX == 0 && subOriginY == 0) { |
| replacementData = |
| replacementData.createChild(srcTileRect.x, |
| srcTileRect.y, |
| srcTileRect.width, |
| srcTileRect.height, |
| replacementRect.x, |
| replacementRect.y, |
| sourceBands); |
| } else { |
| replacementData = subsample(replacementData, |
| sourceBands, |
| subOriginX, |
| subOriginY, |
| subPeriodX, |
| subPeriodY, |
| dstOffset.x, |
| dstOffset.y, |
| replacementRect); |
| if(replacementData == null) { |
| continue; |
| } |
| } |
| |
| raster.setRect(replacementData); |
| |
| if(isEmpty) { |
| stream.seek(nextSpace); |
| } else { |
| stream.seek(replacePixelsTileOffsets[tileIndex]); |
| } |
| |
| this.image = new SingleTileRenderedImage(raster, cm); |
| |
| int numBytes = writeTile(tileRect, encoder); |
| |
| if(isEmpty) { |
| // Update Strip/TileOffsets and |
| // Strip/TileByteCounts fields. |
| stream.mark(); |
| stream.seek(replacePixelsOffsetsPosition + |
| 4*tileIndex); |
| stream.writeInt((int)nextSpace); |
| stream.seek(replacePixelsByteCountsPosition + |
| 4*tileIndex); |
| stream.writeInt(numBytes); |
| stream.reset(); |
| |
| // Increment location of next available space. |
| nextSpace += numBytes; |
| } |
| } |
| } |
| |
| } catch(IOException e) { |
| throw e; |
| } finally { |
| stream.reset(); |
| } |
| } |
| } |
| |
| public void replacePixels(Raster raster, ImageWriteParam param) |
| throws IOException { |
| if (raster == null) { |
| throw new NullPointerException("raster == null!"); |
| } |
| |
| replacePixels(new SingleTileRenderedImage(raster, |
| image.getColorModel()), |
| param); |
| } |
| |
| public void endReplacePixels() throws IOException { |
| synchronized(replacePixelsLock) { |
| if(!this.inReplacePixelsNest) { |
| throw new IllegalStateException |
| ("No previous call to prepareReplacePixels()!"); |
| } |
| replacePixelsIndex = -1; |
| replacePixelsMetadata = null; |
| replacePixelsTileOffsets = null; |
| replacePixelsByteCounts = null; |
| replacePixelsOffsetsPosition = 0L; |
| replacePixelsByteCountsPosition = 0L; |
| replacePixelsRegion = null; |
| inReplacePixelsNest = false; |
| } |
| } |
| |
| // ----- END replacePixels methods ----- |
| |
| // Save stream positions for use when aborted. |
| private void markPositions() throws IOException { |
| prevStreamPosition = stream.getStreamPosition(); |
| prevHeaderPosition = headerPosition; |
| prevNextSpace = nextSpace; |
| } |
| |
| // Reset to positions saved by markPositions(). |
| private void resetPositions() throws IOException { |
| stream.seek(prevStreamPosition); |
| headerPosition = prevHeaderPosition; |
| nextSpace = prevNextSpace; |
| } |
| |
| public void reset() { |
| super.reset(); |
| |
| stream = null; |
| image = null; |
| imageType = null; |
| byteOrder = null; |
| param = null; |
| compressor = null; |
| colorConverter = null; |
| streamMetadata = null; |
| imageMetadata = null; |
| |
| isRescaling = false; |
| |
| isWritingSequence = false; |
| isWritingEmpty = false; |
| isInsertingEmpty = false; |
| |
| replacePixelsIndex = -1; |
| replacePixelsMetadata = null; |
| replacePixelsTileOffsets = null; |
| replacePixelsByteCounts = null; |
| replacePixelsOffsetsPosition = 0L; |
| replacePixelsByteCountsPosition = 0L; |
| replacePixelsRegion = null; |
| inReplacePixelsNest = false; |
| } |
| } |
| |
| class EmptyImage extends SimpleRenderedImage { |
| EmptyImage(int minX, int minY, int width, int height, |
| int tileGridXOffset, int tileGridYOffset, |
| int tileWidth, int tileHeight, |
| SampleModel sampleModel, ColorModel colorModel) { |
| this.minX = minX; |
| this.minY = minY; |
| this.width = width; |
| this.height = height; |
| this.tileGridXOffset = tileGridXOffset; |
| this.tileGridYOffset = tileGridYOffset; |
| this.tileWidth = tileWidth; |
| this.tileHeight = tileHeight; |
| this.sampleModel = sampleModel; |
| this.colorModel = colorModel; |
| } |
| |
| public Raster getTile(int tileX, int tileY) { |
| return null; |
| } |
| } |