in src/main/java/org/apache/commons/imaging/formats/tiff/write/AbstractTiffImageWriter.java [360:589]
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 = TiffConstants.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;
}
}
final int samplesPerPixel;
final int bitsPerSample;
final int photometricInterpretation;
if (compression == TiffConstants.COMPRESSION_CCITT_1D || compression == TiffConstants.COMPRESSION_CCITT_GROUP_3
|| compression == TiffConstants.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 TiffConstants.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 TiffConstants.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 TiffConstants.COMPRESSION_CCITT_GROUP_4: {
final Integer t6Parameter = params.getT6Options();
if (t6Parameter != null) {
t6Options = t6Parameter.intValue();
}
t6Options &= 0x4;
final boolean usesUncompressedMode = (t6Options & TiffConstants.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 TiffConstants.COMPRESSION_PACKBITS:
for (int i = 0; i < strips.length; i++) {
strips[i] = PackBits.compress(strips[i]);
}
break;
case TiffConstants.COMPRESSION_LZW:
predictor = TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING;
for (int i = 0; i < strips.length; i++) {
final byte[] uncompressed = strips[i];
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 TiffConstants.COMPRESSION_DEFLATE_ADOBE:
predictor = TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING;
for (int i = 0; i < strips.length; i++) {
applyPredictor(width, samplesPerPixel, strips[i]);
strips[i] = ZlibDeflate.compress(strips[i]);
}
break;
case TiffConstants.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);
}