blob: 5a2f909e56394ee3fa07326ca83b9aca21059701 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.imaging.formats.tiff.write;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.DEFAULT_TIFF_BYTE_ORDER;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_1D;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_3;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_4;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_DEFLATE_ADOBE;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_LZW;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_PACKBITS;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_UNCOMPRESSED;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_HEADER_SIZE;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.apache.commons.imaging.ImagingException;
import org.apache.commons.imaging.PixelDensity;
import org.apache.commons.imaging.common.Allocator;
import org.apache.commons.imaging.common.BinaryOutputStream;
import org.apache.commons.imaging.common.PackBits;
import org.apache.commons.imaging.common.RationalNumber;
import org.apache.commons.imaging.common.ZlibDeflate;
import org.apache.commons.imaging.formats.tiff.AbstractTiffElement;
import org.apache.commons.imaging.formats.tiff.AbstractTiffImageData;
import org.apache.commons.imaging.formats.tiff.TiffImagingParameters;
import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants;
import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
import org.apache.commons.imaging.formats.tiff.itu_t4.T4AndT6Compression;
import org.apache.commons.imaging.mylzw.MyLzwCompressor;
public abstract class AbstractTiffImageWriter {
private static final int MAX_PIXELS_FOR_RGB = 1024 * 1024;
protected static int imageDataPaddingLength(final int dataLength) {
return (4 - (dataLength % 4)) % 4;
}
protected final ByteOrder byteOrder;
public AbstractTiffImageWriter() {
this.byteOrder = DEFAULT_TIFF_BYTE_ORDER;
}
public AbstractTiffImageWriter(final ByteOrder byteOrder) {
this.byteOrder = byteOrder;
}
private void applyPredictor(final int width, final int bytesPerSample, final byte[] b) {
final int nBytesPerRow = bytesPerSample * width;
final int nRows = b.length / nBytesPerRow;
for (int iRow = 0; iRow < nRows; iRow++) {
final int offset = iRow * nBytesPerRow;
for (int i = nBytesPerRow - 1; i >= bytesPerSample; i--) {
b[offset + i] -= b[offset + i - bytesPerSample];
}
}
}
/**
* Check an image to see if any of its pixels are non-opaque.
* @param src a valid image
* @return true if at least one non-opaque pixel is found.
*/
private boolean checkForActualAlpha(final BufferedImage src) {
// to conserve memory, very large images may be read
// in pieces.
final int width = src.getWidth();
final int height = src.getHeight();
int nRowsPerRead = MAX_PIXELS_FOR_RGB / width;
if (nRowsPerRead < 1) {
nRowsPerRead = 1;
}
final int nReads = (height + nRowsPerRead - 1) / nRowsPerRead;
final int[] argb = Allocator.intArray(nRowsPerRead * width);
for (int iRead = 0; iRead < nReads; iRead++) {
final int i0 = iRead * nRowsPerRead;
final int i1 = i0 + nRowsPerRead > height ? height : i0 + nRowsPerRead;
src.getRGB(0, i0, width, i1 - i0, argb, 0, width);
final int n = (i1 - i0) * width;
for (int i = 0; i < n; i++) {
if ((argb[i] & 0xff000000) != 0xff000000) {
return true;
}
}
}
return false;
}
private void combineUserExifIntoFinalExif(final TiffOutputSet userExif,
final TiffOutputSet outputSet) throws ImagingException {
final List<TiffOutputDirectory> outputDirectories = outputSet.getDirectories();
outputDirectories.sort(TiffOutputDirectory.COMPARATOR);
for (final TiffOutputDirectory userDirectory : userExif.getDirectories()) {
final int location = Collections.binarySearch(outputDirectories,
userDirectory, TiffOutputDirectory.COMPARATOR);
if (location < 0) {
outputSet.addDirectory(userDirectory);
} else {
final TiffOutputDirectory outputDirectory = outputDirectories.get(location);
for (final TiffOutputField userField : userDirectory) {
if (outputDirectory.findField(userField.tagInfo) == null) {
outputDirectory.add(userField);
}
}
}
}
}
private byte[][] getStrips(final BufferedImage src, final int samplesPerPixel, final int bitsPerSample, final int rowsPerStrip) {
final int width = src.getWidth();
final int height = src.getHeight();
final int stripCount = (height + rowsPerStrip - 1) / rowsPerStrip;
// Write Strips
byte[][] result = new byte[Allocator.check(stripCount)][];
int remainingRows = height;
for (int i = 0; i < stripCount; i++) {
final int rowsInStrip = Math.min(rowsPerStrip, remainingRows);
remainingRows -= rowsInStrip;
final int bitsInRow = bitsPerSample * samplesPerPixel * width;
final int bytesPerRow = (bitsInRow + 7) / 8;
final int bytesInStrip = rowsInStrip * bytesPerRow;
final byte[] uncompressed = Allocator.byteArray(bytesInStrip);
int counter = 0;
int y = i * rowsPerStrip;
final int stop = i * rowsPerStrip + rowsPerStrip;
for (; (y < height) && (y < stop); y++) {
int bitCache = 0;
int bitsInCache = 0;
for (int x = 0; x < width; x++) {
final int rgb = src.getRGB(x, y);
final int red = 0xff & (rgb >> 16);
final int green = 0xff & (rgb >> 8);
final int blue = 0xff & (rgb >> 0);
if (bitsPerSample == 1) {
int sample = (red + green + blue) / 3;
if (sample > 127) {
sample = 0;
} else {
sample = 1;
}
bitCache <<= 1;
bitCache |= sample;
bitsInCache++;
if (bitsInCache == 8) {
uncompressed[counter++] = (byte) bitCache;
bitCache = 0;
bitsInCache = 0;
}
} else if (samplesPerPixel == 4) {
uncompressed[counter++] = (byte) red;
uncompressed[counter++] = (byte) green;
uncompressed[counter++] = (byte) blue;
uncompressed[counter++] = (byte) (rgb >> 24);
} else {
// samples per pixel is 3
uncompressed[counter++] = (byte) red;
uncompressed[counter++] = (byte) green;
uncompressed[counter++] = (byte) blue;
}
}
if (bitsInCache > 0) {
bitCache <<= (8 - bitsInCache);
uncompressed[counter++] = (byte) bitCache;
}
}
result[i] = uncompressed;
}
return result;
}
protected TiffOutputSummary validateDirectories(final TiffOutputSet outputSet)
throws ImagingException {
if (outputSet.isEmpty()) {
throw new ImagingException("No directories.");
}
TiffOutputDirectory exifDirectory = null;
TiffOutputDirectory gpsDirectory = null;
TiffOutputDirectory interoperabilityDirectory = null;
TiffOutputField exifDirectoryOffsetField = null;
TiffOutputField gpsDirectoryOffsetField = null;
TiffOutputField interoperabilityDirectoryOffsetField = null;
final List<Integer> directoryIndices = new ArrayList<>();
final Map<Integer, TiffOutputDirectory> directoryTypeMap = new HashMap<>();
for (final TiffOutputDirectory directory : outputSet) {
final int dirType = directory.getType();
directoryTypeMap.put(dirType, directory);
// Debug.debug("validating dirType", dirType + " ("
// + directory.getFields().size() + " fields)");
if (dirType < 0) {
switch (dirType) {
case TiffDirectoryConstants.DIRECTORY_TYPE_EXIF:
if (exifDirectory != null) {
throw new ImagingException(
"More than one EXIF directory.");
}
exifDirectory = directory;
break;
case TiffDirectoryConstants.DIRECTORY_TYPE_GPS:
if (gpsDirectory != null) {
throw new ImagingException(
"More than one GPS directory.");
}
gpsDirectory = directory;
break;
case TiffDirectoryConstants.DIRECTORY_TYPE_INTEROPERABILITY:
if (interoperabilityDirectory != null) {
throw new ImagingException(
"More than one Interoperability directory.");
}
interoperabilityDirectory = directory;
break;
default:
throw new ImagingException("Unknown directory: "
+ dirType);
}
} else {
if (directoryIndices.contains(dirType)) {
throw new ImagingException(
"More than one directory with index: " + dirType
+ ".");
}
directoryIndices.add(dirType);
// dirMap.put(arg0, arg1)
}
final HashSet<Integer> fieldTags = new HashSet<>();
for (final TiffOutputField field : directory) {
if (fieldTags.contains(field.tag)) {
throw new ImagingException("Tag ("
+ field.tagInfo.getDescription()
+ ") appears twice in directory.");
}
fieldTags.add(field.tag);
if (field.tag == ExifTagConstants.EXIF_TAG_EXIF_OFFSET.tag) {
if (exifDirectoryOffsetField != null) {
throw new ImagingException(
"More than one Exif directory offset field.");
}
exifDirectoryOffsetField = field;
} else if (field.tag == ExifTagConstants.EXIF_TAG_INTEROP_OFFSET.tag) {
if (interoperabilityDirectoryOffsetField != null) {
throw new ImagingException(
"More than one Interoperability directory offset field.");
}
interoperabilityDirectoryOffsetField = field;
} else if (field.tag == ExifTagConstants.EXIF_TAG_GPSINFO.tag) {
if (gpsDirectoryOffsetField != null) {
throw new ImagingException(
"More than one GPS directory offset field.");
}
gpsDirectoryOffsetField = field;
}
}
// directory.
}
if (directoryIndices.isEmpty()) {
throw new ImagingException("Missing root directory.");
}
// "normal" TIFF directories should have continous indices starting with
// 0, ie. 0, 1, 2...
directoryIndices.sort(null);
TiffOutputDirectory previousDirectory = null;
for (int i = 0; i < directoryIndices.size(); i++) {
final Integer index = directoryIndices.get(i);
if (index != i) {
throw new ImagingException("Missing directory: " + i + ".");
}
// set up chain of directory references for "normal" directories.
final TiffOutputDirectory directory = directoryTypeMap.get(index);
if (null != previousDirectory) {
previousDirectory.setNextDirectory(directory);
}
previousDirectory = directory;
}
final TiffOutputDirectory rootDirectory = directoryTypeMap.get(
TiffDirectoryConstants.DIRECTORY_TYPE_ROOT);
// prepare results
final TiffOutputSummary result = new TiffOutputSummary(byteOrder,
rootDirectory, directoryTypeMap);
if (interoperabilityDirectory == null
&& interoperabilityDirectoryOffsetField != null) {
// perhaps we should just discard field?
throw new ImagingException(
"Output set has Interoperability Directory Offset field, but no Interoperability Directory");
}
if (interoperabilityDirectory != null) {
if (exifDirectory == null) {
exifDirectory = outputSet.addExifDirectory();
}
if (interoperabilityDirectoryOffsetField == null) {
interoperabilityDirectoryOffsetField =
TiffOutputField.createOffsetField(
ExifTagConstants.EXIF_TAG_INTEROP_OFFSET,
byteOrder);
exifDirectory.add(interoperabilityDirectoryOffsetField);
}
result.add(interoperabilityDirectory,
interoperabilityDirectoryOffsetField);
}
// make sure offset fields and offset'd directories correspond.
if (exifDirectory == null && exifDirectoryOffsetField != null) {
// perhaps we should just discard field?
throw new ImagingException(
"Output set has Exif Directory Offset field, but no Exif Directory");
}
if (exifDirectory != null) {
if (exifDirectoryOffsetField == null) {
exifDirectoryOffsetField = TiffOutputField.createOffsetField(
ExifTagConstants.EXIF_TAG_EXIF_OFFSET, byteOrder);
rootDirectory.add(exifDirectoryOffsetField);
}
result.add(exifDirectory, exifDirectoryOffsetField);
}
if (gpsDirectory == null && gpsDirectoryOffsetField != null) {
// perhaps we should just discard field?
throw new ImagingException(
"Output set has GPS Directory Offset field, but no GPS Directory");
}
if (gpsDirectory != null) {
if (gpsDirectoryOffsetField == null) {
gpsDirectoryOffsetField = TiffOutputField.createOffsetField(
ExifTagConstants.EXIF_TAG_GPSINFO, byteOrder);
rootDirectory.add(gpsDirectoryOffsetField);
}
result.add(gpsDirectory, gpsDirectoryOffsetField);
}
return result;
// Debug.debug();
}
public abstract void write(OutputStream os, TiffOutputSet outputSet)
throws IOException, ImagingException;
public void writeImage(final BufferedImage src, final OutputStream os, final TiffImagingParameters params)
throws ImagingException, IOException {
final TiffOutputSet userExif = params.getOutputSet();
final String xmpXml = params.getXmpXml();
PixelDensity pixelDensity = params.getPixelDensity();
if (pixelDensity == null) {
pixelDensity = PixelDensity.createFromPixelsPerInch(72, 72);
}
final int width = src.getWidth();
final int height = src.getHeight();
// If the source image has a color model that supports alpha,
// this module performs a call to checkForActualAlpha() to see whether
// the image that was supplied to the API actually contains
// non-opaque data in its alpha channel. It is common for applications
// to create a BufferedImage using TYPE_INT_ARGB, and fill the entire
// image with opaque pixels. In such a case, the file size of the output
// can be reduced by 25 percent by storing the image in an 3-byte RGB
// format. This approach will also make a small reduction in the runtime
// to read the resulting file when it is accessed by an application.
final ColorModel cModel = src.getColorModel();
final boolean hasAlpha = cModel.hasAlpha() && checkForActualAlpha(src);
// 10/2020: In the case of an image with pre-multiplied alpha
// (what the TIFF specification calls "associated alpha"), the
// Java getRGB method adjusts the value to a non-premultiplied
// alpha state. However, this class could access the pre-multiplied
// alpha data by obtaining the underlying raster. At this time,
// the value of such a little-used feature does not seem
// commensurate with the complexity of the extra code it would require.
int compression = TIFF_COMPRESSION_LZW;
short predictor = TiffTagConstants.PREDICTOR_VALUE_NONE;
int stripSizeInBits = 64000; // the default from legacy implementation
final Integer compressionParameter = params.getCompression();
if (compressionParameter != null) {
compression = compressionParameter;
final Integer stripSizeInBytes = params.getLzwCompressionBlockSize();
if (stripSizeInBytes != null) {
if (stripSizeInBytes < 8000) {
throw new ImagingException("Block size parameter " + stripSizeInBytes + " is less than 8000 minimum");
}
stripSizeInBits = stripSizeInBytes * 8;
}
}
int samplesPerPixel;
int bitsPerSample;
int photometricInterpretation;
if (compression == TIFF_COMPRESSION_CCITT_1D || compression == TIFF_COMPRESSION_CCITT_GROUP_3 || compression == TIFF_COMPRESSION_CCITT_GROUP_4) {
samplesPerPixel = 1;
bitsPerSample = 1;
photometricInterpretation = 0;
} else {
samplesPerPixel = hasAlpha ? 4 : 3;
bitsPerSample = 8;
photometricInterpretation = 2;
}
int rowsPerStrip = stripSizeInBits / (width * bitsPerSample * samplesPerPixel);
rowsPerStrip = Math.max(1, rowsPerStrip); // must have at least one.
final byte[][] strips = getStrips(src, samplesPerPixel, bitsPerSample, rowsPerStrip);
// System.out.println("width: " + width);
// System.out.println("height: " + height);
// System.out.println("fRowsPerStrip: " + fRowsPerStrip);
// System.out.println("fSamplesPerPixel: " + fSamplesPerPixel);
// System.out.println("stripCount: " + stripCount);
int t4Options = 0;
int t6Options = 0;
switch (compression) {
case TIFF_COMPRESSION_CCITT_1D:
for (int i = 0; i < strips.length; i++) {
strips[i] = T4AndT6Compression.compressModifiedHuffman(strips[i], width, strips[i].length / ((width + 7) / 8));
}
break;
case TIFF_COMPRESSION_CCITT_GROUP_3: {
final Integer t4Parameter = params.getT4Options();
if (t4Parameter != null) {
t4Options = t4Parameter.intValue();
}
t4Options &= 0x7;
final boolean is2D = (t4Options & 1) != 0;
final boolean usesUncompressedMode = (t4Options & 2) != 0;
if (usesUncompressedMode) {
throw new ImagingException("T.4 compression with the uncompressed mode extension is not yet supported");
}
final boolean hasFillBitsBeforeEOL = (t4Options & 4) != 0;
for (int i = 0; i < strips.length; i++) {
if (is2D) {
strips[i] = T4AndT6Compression.compressT4_2D(strips[i], width, strips[i].length / ((width + 7) / 8), hasFillBitsBeforeEOL, rowsPerStrip);
} else {
strips[i] = T4AndT6Compression.compressT4_1D(strips[i], width, strips[i].length / ((width + 7) / 8), hasFillBitsBeforeEOL);
}
}
break;
}
case TIFF_COMPRESSION_CCITT_GROUP_4: {
final Integer t6Parameter = params.getT6Options();
if (t6Parameter != null) {
t6Options = t6Parameter.intValue();
}
t6Options &= 0x4;
final boolean usesUncompressedMode = (t6Options & TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE) != 0;
if (usesUncompressedMode) {
throw new ImagingException("T.6 compression with the uncompressed mode extension is not yet supported");
}
for (int i = 0; i < strips.length; i++) {
strips[i] = T4AndT6Compression.compressT6(strips[i], width, strips[i].length / ((width + 7) / 8));
}
break;
}
case TIFF_COMPRESSION_PACKBITS:
for (int i = 0; i < strips.length; i++) {
strips[i] = new PackBits().compress(strips[i]);
}
break;
case TIFF_COMPRESSION_LZW:
predictor = TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING;
for (int i = 0; i < strips.length; i++) {
final byte[] uncompressed = strips[i];
this.applyPredictor(width, samplesPerPixel, strips[i]);
final int LZW_MINIMUM_CODE_SIZE = 8;
final MyLzwCompressor compressor = new MyLzwCompressor(LZW_MINIMUM_CODE_SIZE, ByteOrder.BIG_ENDIAN, true);
final byte[] compressed = compressor.compress(uncompressed);
strips[i] = compressed;
}
break;
case TIFF_COMPRESSION_DEFLATE_ADOBE:
predictor = TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING;
for (int i = 0; i < strips.length; i++) {
this.applyPredictor(width, samplesPerPixel, strips[i]);
strips[i] = ZlibDeflate.compress(strips[i]);
}
break;
case TIFF_COMPRESSION_UNCOMPRESSED:
break;
default:
throw new ImagingException(
"Invalid compression parameter (Only CCITT 1D/Group 3/Group 4, LZW, Packbits, Zlib Deflate and uncompressed supported).");
}
final AbstractTiffElement.DataElement[] imageData = new AbstractTiffElement.DataElement[strips.length];
Arrays.setAll(imageData, i -> new AbstractTiffImageData.Data(0, strips[i].length, strips[i]));
final TiffOutputSet outputSet = new TiffOutputSet(byteOrder);
final TiffOutputDirectory directory = outputSet.addRootDirectory();
// WriteField stripOffsetsField;
directory.add(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, width);
directory.add(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, height);
directory.add(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION, (short) photometricInterpretation);
directory.add(TiffTagConstants.TIFF_TAG_COMPRESSION, (short) compression);
directory.add(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL, (short) samplesPerPixel);
switch (samplesPerPixel) {
case 3:
directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample, (short) bitsPerSample, (short) bitsPerSample);
break;
case 4:
directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample, (short) bitsPerSample, (short) bitsPerSample,
(short) bitsPerSample);
directory.add(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES, (short) TiffTagConstants.EXTRA_SAMPLE_UNASSOCIATED_ALPHA);
break;
case 1:
directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample);
break;
default:
break;
}
// {
// stripOffsetsField = new WriteField(TIFF_TAG_STRIP_OFFSETS,
// FIELD_TYPE_LONG, stripOffsets.length, FIELD_TYPE_LONG
// .writeData(stripOffsets, byteOrder));
// directory.add(stripOffsetsField);
// }
// {
// WriteField field = new WriteField(TIFF_TAG_STRIP_BYTE_COUNTS,
// FIELD_TYPE_LONG, stripByteCounts.length,
// FIELD_TYPE_LONG.writeData(stripByteCounts,
// WRITE_BYTE_ORDER));
// directory.add(field);
// }
directory.add(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP, rowsPerStrip);
if (pixelDensity.isUnitless()) {
directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, (short) 0);
directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, RationalNumber.valueOf(pixelDensity.getRawHorizontalDensity()));
directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, RationalNumber.valueOf(pixelDensity.getRawVerticalDensity()));
} else if (pixelDensity.isInInches()) {
directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, (short) 2);
directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, RationalNumber.valueOf(pixelDensity.horizontalDensityInches()));
directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, RationalNumber.valueOf(pixelDensity.verticalDensityInches()));
} else {
directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, (short) 1);
directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, RationalNumber.valueOf(pixelDensity.horizontalDensityCentimetres()));
directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, RationalNumber.valueOf(pixelDensity.verticalDensityCentimetres()));
}
if (t4Options != 0) {
directory.add(TiffTagConstants.TIFF_TAG_T4_OPTIONS, t4Options);
}
if (t6Options != 0) {
directory.add(TiffTagConstants.TIFF_TAG_T6_OPTIONS, t6Options);
}
if (null != xmpXml) {
final byte[] xmpXmlBytes = xmpXml.getBytes(StandardCharsets.UTF_8);
directory.add(TiffTagConstants.TIFF_TAG_XMP, xmpXmlBytes);
}
if (predictor == TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING) {
directory.add(TiffTagConstants.TIFF_TAG_PREDICTOR, predictor);
}
final AbstractTiffImageData abstractTiffImageData = new AbstractTiffImageData.Strips(imageData, rowsPerStrip);
directory.setTiffImageData(abstractTiffImageData);
if (userExif != null) {
combineUserExifIntoFinalExif(userExif, outputSet);
}
write(os, outputSet);
}
protected void writeImageFileHeader(final BinaryOutputStream bos)
throws IOException {
writeImageFileHeader(bos, TIFF_HEADER_SIZE);
}
protected void writeImageFileHeader(final BinaryOutputStream bos,
final long offsetToFirstIFD) throws IOException {
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
bos.write('I');
bos.write('I');
} else {
bos.write('M');
bos.write('M');
}
bos.write2Bytes(42); // tiffVersion
bos.write4Bytes((int) offsetToFirstIFD);
}
}