    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.imaging.formats.tiff.write;
    018
    019import java.awt.image.BufferedImage;
    020import java.awt.image.ColorModel;
    021import java.io.IOException;
    022import java.io.OutputStream;
    023import java.nio.ByteOrder;
    024import java.nio.charset.StandardCharsets;
    025import java.util.ArrayList;
    026import java.util.Arrays;
    027import java.util.Collections;
    028import java.util.HashMap;
    029import java.util.HashSet;
    030import java.util.List;
    031import java.util.Map;
    032
    033import org.apache.commons.imaging.ImagingException;
    034import org.apache.commons.imaging.PixelDensity;
    035import org.apache.commons.imaging.common.AbstractBinaryOutputStream;
    036import org.apache.commons.imaging.common.Allocator;
    037import org.apache.commons.imaging.common.PackBits;
    038import org.apache.commons.imaging.common.RationalNumber;
    039import org.apache.commons.imaging.common.ZlibDeflate;
    040import org.apache.commons.imaging.formats.tiff.AbstractTiffElement;
    041import org.apache.commons.imaging.formats.tiff.AbstractTiffImageData;
    042import org.apache.commons.imaging.formats.tiff.TiffImagingParameters;
    043import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
    044import org.apache.commons.imaging.formats.tiff.constants.TiffConstants;
    045import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants;
    046import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
    047import org.apache.commons.imaging.formats.tiff.itu_t4.T4AndT6Compression;
    048import org.apache.commons.imaging.mylzw.MyLzwCompressor;
    049
    050public abstract class AbstractTiffImageWriter {
    051
    052    private static final int MAX_PIXELS_FOR_RGB = 1024 * 1024;
    053
    054    protected static int imageDataPaddingLength(final int dataLength) {
    055        return (4 - dataLength % 4) % 4;
    056    }
    057
    058    protected final ByteOrder byteOrder;
    059
    060    public AbstractTiffImageWriter() {
    061        this.byteOrder = TiffConstants.DEFAULT_TIFF_BYTE_ORDER;
    062    }
    063
    064    public AbstractTiffImageWriter(final ByteOrder byteOrder) {
    065        this.byteOrder = byteOrder;
    066    }
    067
    068    private void applyPredictor(final int width, final int bytesPerSample, final byte[] b) {
    069        final int nBytesPerRow = bytesPerSample * width;
    070        final int nRows = b.length / nBytesPerRow;
    071        for (int iRow = 0; iRow < nRows; iRow++) {
    072            final int offset = iRow * nBytesPerRow;
    073            for (int i = nBytesPerRow - 1; i >= bytesPerSample; i--) {
    074                b[offset + i] -= b[offset + i - bytesPerSample];
    075            }
    076        }
    077    }
    078
    079    /**
    080     * Check an image to see if any of its pixels are non-opaque.
    081     *
    082     * @param src a valid image
    083     * @return true if at least one non-opaque pixel is found.
    084     */
    085    private boolean checkForActualAlpha(final BufferedImage src) {
    086        // to conserve memory, very large images may be read
    087        // in pieces.
    088        final int width = src.getWidth();
    089        final int height = src.getHeight();
    090        int nRowsPerRead = MAX_PIXELS_FOR_RGB / width;
    091        if (nRowsPerRead < 1) {
    092            nRowsPerRead = 1;
    093        }
    094        final int nReads = (height + nRowsPerRead - 1) / nRowsPerRead;
    095        final int[] argb = Allocator.intArray(nRowsPerRead * width);
    096        for (int iRead = 0; iRead < nReads; iRead++) {
    097            final int i0 = iRead * nRowsPerRead;
    098            final int i1 = i0 + nRowsPerRead > height ? height : i0 + nRowsPerRead;
    099            src.getRGB(0, i0, width, i1 - i0, argb, 0, width);
    100            final int n = (i1 - i0) * width;
    101            for (int i = 0; i < n; i++) {
    102                if ((argb[i] & 0xff000000) != 0xff000000) {
    103                    return true;
    104                }
    105            }
    106        }
    107        return false;
    108    }
    109
    110    private void combineUserExifIntoFinalExif(final TiffOutputSet userExif, final TiffOutputSet outputSet) throws ImagingException {
    111        final List<TiffOutputDirectory> outputDirectories = outputSet.getDirectories();
    112        outputDirectories.sort(TiffOutputDirectory.COMPARATOR);
    113        for (final TiffOutputDirectory userDirectory : userExif.getDirectories()) {
    114            final int location = Collections.binarySearch(outputDirectories, userDirectory, TiffOutputDirectory.COMPARATOR);
    115            if (location < 0) {
    116                outputSet.addDirectory(userDirectory);
    117            } else {
    118                final TiffOutputDirectory outputDirectory = outputDirectories.get(location);
    119                for (final TiffOutputField userField : userDirectory) {
    120                    if (outputDirectory.findField(userField.tagInfo) == null) {
    121                        outputDirectory.add(userField);
    122                    }
    123                }
    124            }
    125        }
    126    }
    127
    128    private byte[][] getStrips(final BufferedImage src, final int samplesPerPixel, final int bitsPerSample, final int rowsPerStrip) {
    129        final int width = src.getWidth();
    130        final int height = src.getHeight();
    131
    132        final int stripCount = (height + rowsPerStrip - 1) / rowsPerStrip;
    133
    134        // Write Strips
    135        final byte[][] result = new byte[Allocator.check(stripCount)][];
    136
    137        int remainingRows = height;
    138
    139        for (int i = 0; i < stripCount; i++) {
    140            final int rowsInStrip = Math.min(rowsPerStrip, remainingRows);
    141            remainingRows -= rowsInStrip;
    142
    143            final int bitsInRow = bitsPerSample * samplesPerPixel * width;
    144            final int bytesPerRow = (bitsInRow + 7) / 8;
    145            final int bytesInStrip = rowsInStrip * bytesPerRow;
    146
    147            final byte[] uncompressed = Allocator.byteArray(bytesInStrip);
    148
    149            int counter = 0;
    150            int y = i * rowsPerStrip;
    151            final int stop = i * rowsPerStrip + rowsPerStrip;
    152
    153            for (; y < height && y < stop; y++) {
    154                int bitCache = 0;
    155                int bitsInCache = 0;
    156                for (int x = 0; x < width; x++) {
    157                    final int rgb = src.getRGB(x, y);
    158                    final int red = 0xff & rgb >> 16;
    159                    final int green = 0xff & rgb >> 8;
    160                    final int blue = 0xff & rgb >> 0;
    161
    162                    if (bitsPerSample == 1) {
    163                        int sample = (red + green + blue) / 3;
    164                        if (sample > 127) {
    165                            sample = 0;
    166                        } else {
    167                            sample = 1;
    168                        }
    169                        bitCache <<= 1;
    170                        bitCache |= sample;
    171                        bitsInCache++;
    172                        if (bitsInCache == 8) {
    173                            uncompressed[counter++] = (byte) bitCache;
    174                            bitCache = 0;
    175                            bitsInCache = 0;
    176                        }
    177                    } else if (samplesPerPixel == 4) {
    178                        uncompressed[counter++] = (byte) red;
    179                        uncompressed[counter++] = (byte) green;
    180                        uncompressed[counter++] = (byte) blue;
    181                        uncompressed[counter++] = (byte) (rgb >> 24);
    182                    } else {
    183                        // samples per pixel is 3
    184                        uncompressed[counter++] = (byte) red;
    185                        uncompressed[counter++] = (byte) green;
    186                        uncompressed[counter++] = (byte) blue;
    187                    }
    188                }
    189                if (bitsInCache > 0) {
    190                    bitCache <<= 8 - bitsInCache;
    191                    uncompressed[counter++] = (byte) bitCache;
    192                }
    193            }
    194
    195            result[i] = uncompressed;
    196        }
    197
    198        return result;
    199    }
    200
    201    protected TiffOutputSummary validateDirectories(final TiffOutputSet outputSet) throws ImagingException {
    202        if (outputSet.isEmpty()) {
    203            throw new ImagingException("No directories.");
    204        }
    205
    206        TiffOutputDirectory exifDirectory = null;
    207        TiffOutputDirectory gpsDirectory = null;
    208        TiffOutputDirectory interoperabilityDirectory = null;
    209        TiffOutputField exifDirectoryOffsetField = null;
    210        TiffOutputField gpsDirectoryOffsetField = null;
    211        TiffOutputField interoperabilityDirectoryOffsetField = null;
    212
    213        final List<Integer> directoryIndices = new ArrayList<>();
    214        final Map<Integer, TiffOutputDirectory> directoryTypeMap = new HashMap<>();
    215        for (final TiffOutputDirectory directory : outputSet) {
    216            final int dirType = directory.getType();
    217            directoryTypeMap.put(dirType, directory);
    218            // Debug.debug("validating dirType", dirType + " ("
    219            // + directory.getFields().size() + " fields)");
    220
    221            if (dirType < 0) {
    222                switch (dirType) {
    223                case TiffDirectoryConstants.DIRECTORY_TYPE_EXIF:
    224                    if (exifDirectory != null) {
    225                        throw new ImagingException("More than one EXIF directory.");
    226                    }
    227                    exifDirectory = directory;
    228                    break;
    229
    230                case TiffDirectoryConstants.DIRECTORY_TYPE_GPS:
    231                    if (gpsDirectory != null) {
    232                        throw new ImagingException("More than one GPS directory.");
    233                    }
    234                    gpsDirectory = directory;
    235                    break;
    236
    237                case TiffDirectoryConstants.DIRECTORY_TYPE_INTEROPERABILITY:
    238                    if (interoperabilityDirectory != null) {
    239                        throw new ImagingException("More than one Interoperability directory.");
    240                    }
    241                    interoperabilityDirectory = directory;
    242                    break;
    243                default:
    244                    throw new ImagingException("Unknown directory: " + dirType);
    245                }
    246            } else {
    247                if (directoryIndices.contains(dirType)) {
    248                    throw new ImagingException("More than one directory with index: " + dirType + ".");
    249                }
    250                directoryIndices.add(dirType);
    251                // dirMap.put(arg0, arg1)
    252            }
    253
    254            final HashSet<Integer> fieldTags = new HashSet<>();
    255            for (final TiffOutputField field : directory) {
    256                if (fieldTags.contains(field.tag)) {
    257                    throw new ImagingException("Tag (" + field.tagInfo.getDescription() + ") appears twice in directory.");
    258                }
    259                fieldTags.add(field.tag);
    260
    261                if (field.tag == ExifTagConstants.EXIF_TAG_EXIF_OFFSET.tag) {
    262                    if (exifDirectoryOffsetField != null) {
    263                        throw new ImagingException("More than one Exif directory offset field.");
    264                    }
    265                    exifDirectoryOffsetField = field;
    266                } else if (field.tag == ExifTagConstants.EXIF_TAG_INTEROP_OFFSET.tag) {
    267                    if (interoperabilityDirectoryOffsetField != null) {
    268                        throw new ImagingException("More than one Interoperability directory offset field.");
    269                    }
    270                    interoperabilityDirectoryOffsetField = field;
    271                } else if (field.tag == ExifTagConstants.EXIF_TAG_GPSINFO.tag) {
    272                    if (gpsDirectoryOffsetField != null) {
    273                        throw new ImagingException("More than one GPS directory offset field.");
    274                    }
    275                    gpsDirectoryOffsetField = field;
    276                }
    277            }
    278            // directory.
    279        }
    280
    281        if (directoryIndices.isEmpty()) {
    282            throw new ImagingException("Missing root directory.");
    283        }
    284
    285        // "normal" TIFF directories should have continous indices starting with
    286        // 0, ie. 0, 1, 2...
    287        Collections.sort(directoryIndices);
    288
    289        TiffOutputDirectory previousDirectory = null;
    290        for (int i = 0; i < directoryIndices.size(); i++) {
    291            final Integer index = directoryIndices.get(i);
    292            if (index != i) {
    293                throw new ImagingException("Missing directory: " + i + ".");
    294            }
    295
    296            // set up chain of directory references for "normal" directories.
    297            final TiffOutputDirectory directory = directoryTypeMap.get(index);
    298            if (null != previousDirectory) {
    299                previousDirectory.setNextDirectory(directory);
    300            }
    301            previousDirectory = directory;
    302        }
    303
    304        final TiffOutputDirectory rootDirectory = directoryTypeMap.get(TiffDirectoryConstants.DIRECTORY_TYPE_ROOT);
    305
    306        // prepare results
    307        final TiffOutputSummary result = new TiffOutputSummary(byteOrder, rootDirectory, directoryTypeMap);
    308
    309        if (interoperabilityDirectory == null && interoperabilityDirectoryOffsetField != null) {
    310            // perhaps we should just discard field?
    311            throw new ImagingException("Output set has Interoperability Directory Offset field, but no Interoperability Directory");
    312        }
    313        if (interoperabilityDirectory != null) {
    314            if (exifDirectory == null) {
    315                exifDirectory = outputSet.addExifDirectory();
    316            }
    317
    318            if (interoperabilityDirectoryOffsetField == null) {
    319                interoperabilityDirectoryOffsetField = TiffOutputField.createOffsetField(ExifTagConstants.EXIF_TAG_INTEROP_OFFSET, byteOrder);
    320                exifDirectory.add(interoperabilityDirectoryOffsetField);
    321            }
    322
    323            result.add(interoperabilityDirectory, interoperabilityDirectoryOffsetField);
    324        }
    325
    326        // make sure offset fields and offset'd directories correspond.
    327        if (exifDirectory == null && exifDirectoryOffsetField != null) {
    328            // perhaps we should just discard field?
    329            throw new ImagingException("Output set has Exif Directory Offset field, but no Exif Directory");
    330        }
    331        if (exifDirectory != null) {
    332            if (exifDirectoryOffsetField == null) {
    333                exifDirectoryOffsetField = TiffOutputField.createOffsetField(ExifTagConstants.EXIF_TAG_EXIF_OFFSET, byteOrder);
    334                rootDirectory.add(exifDirectoryOffsetField);
    335            }
    336
    337            result.add(exifDirectory, exifDirectoryOffsetField);
    338        }
    339
    340        if (gpsDirectory == null && gpsDirectoryOffsetField != null) {
    341            // perhaps we should just discard field?
    342            throw new ImagingException("Output set has GPS Directory Offset field, but no GPS Directory");
    343        }
    344        if (gpsDirectory != null) {
    345            if (gpsDirectoryOffsetField == null) {
    346                gpsDirectoryOffsetField = TiffOutputField.createOffsetField(ExifTagConstants.EXIF_TAG_GPSINFO, byteOrder);
    347                rootDirectory.add(gpsDirectoryOffsetField);
    348            }
    349
    350            result.add(gpsDirectory, gpsDirectoryOffsetField);
    351        }
    352
    353        return result;
    354
    355        // Debug.debug();
    356    }
    357
    358    public abstract void write(OutputStream os, TiffOutputSet outputSet) throws IOException, ImagingException;
    359
    360    public void writeImage(final BufferedImage src, final OutputStream os, final TiffImagingParameters params) throws ImagingException, IOException {
    361        final TiffOutputSet userExif = params.getOutputSet();
    362
    363        final String xmpXml = params.getXmpXml();
    364
    365        PixelDensity pixelDensity = params.getPixelDensity();
    366        if (pixelDensity == null) {
    367            pixelDensity = PixelDensity.createFromPixelsPerInch(72, 72);
    368        }
    369
    370        final int width = src.getWidth();
    371        final int height = src.getHeight();
    372
    373        // If the source image has a color model that supports alpha,
    374        // this module performs a call to checkForActualAlpha() to see whether
    375        // the image that was supplied to the API actually contains
    376        // non-opaque data in its alpha channel. It is common for applications
    377        // to create a BufferedImage using TYPE_INT_ARGB, and fill the entire
    378        // image with opaque pixels. In such a case, the file size of the output
    379        // can be reduced by 25 percent by storing the image in an 3-byte RGB
    380        // format. This approach will also make a small reduction in the runtime
    381        // to read the resulting file when it is accessed by an application.
    382        final ColorModel cModel = src.getColorModel();
    383        final boolean hasAlpha = cModel.hasAlpha() && checkForActualAlpha(src);
    384
    385        // 10/2020: In the case of an image with pre-multiplied alpha
    386        // (what the TIFF specification calls "associated alpha"), the
    387        // Java getRGB method adjusts the value to a non-premultiplied
    388        // alpha state. However, this class could access the pre-multiplied
    389        // alpha data by obtaining the underlying raster. At this time,
    390        // the value of such a little-used feature does not seem
    391        // commensurate with the complexity of the extra code it would require.
    392
    393        int compression = TiffConstants.COMPRESSION_LZW;
    394        short predictor = TiffTagConstants.PREDICTOR_VALUE_NONE;
    395
    396        int stripSizeInBits = 64000; // the default from legacy implementation
    397        final Integer compressionParameter = params.getCompression();
    398        if (compressionParameter != null) {
    399            compression = compressionParameter;
    400            final Integer stripSizeInBytes = params.getLzwCompressionBlockSize();
    401            if (stripSizeInBytes != null) {
    402                if (stripSizeInBytes < 8000) {
    403                    throw new ImagingException("Block size parameter " + stripSizeInBytes + " is less than 8000 minimum");
    404                }
    405                stripSizeInBits = stripSizeInBytes * 8;
    406            }
    407        }
    408
    409        final int samplesPerPixel;
    410        final int bitsPerSample;
    411        final int photometricInterpretation;
    412        if (compression == TiffConstants.COMPRESSION_CCITT_1D || compression == TiffConstants.COMPRESSION_CCITT_GROUP_3
    413                || compression == TiffConstants.COMPRESSION_CCITT_GROUP_4) {
    414            samplesPerPixel = 1;
    415            bitsPerSample = 1;
    416            photometricInterpretation = 0;
    417        } else {
    418            samplesPerPixel = hasAlpha ? 4 : 3;
    419            bitsPerSample = 8;
    420            photometricInterpretation = 2;
    421        }
    422
    423        int rowsPerStrip = stripSizeInBits / (width * bitsPerSample * samplesPerPixel);
    424        rowsPerStrip = Math.max(1, rowsPerStrip); // must have at least one.
    425
    426        final byte[][] strips = getStrips(src, samplesPerPixel, bitsPerSample, rowsPerStrip);
    427
    428        // System.out.println("width: " + width);
    429        // System.out.println("height: " + height);
    430        // System.out.println("fRowsPerStrip: " + fRowsPerStrip);
    431        // System.out.println("fSamplesPerPixel: " + fSamplesPerPixel);
    432        // System.out.println("stripCount: " + stripCount);
    433
    434        int t4Options = 0;
    435        int t6Options = 0;
    436        switch (compression) {
    437        case TiffConstants.COMPRESSION_CCITT_1D:
    438            for (int i = 0; i < strips.length; i++) {
    439                strips[i] = T4AndT6Compression.compressModifiedHuffman(strips[i], width, strips[i].length / ((width + 7) / 8));
    440            }
    441            break;
    442        case TiffConstants.COMPRESSION_CCITT_GROUP_3: {
    443            final Integer t4Parameter = params.getT4Options();
    444            if (t4Parameter != null) {
    445                t4Options = t4Parameter.intValue();
    446            }
    447            t4Options &= 0x7;
    448            final boolean is2D = (t4Options & 1) != 0;
    449            final boolean usesUncompressedMode = (t4Options & 2) != 0;
    450            if (usesUncompressedMode) {
    451                throw new ImagingException("T.4 compression with the uncompressed mode extension is not yet supported");
    452            }
    453            final boolean hasFillBitsBeforeEOL = (t4Options & 4) != 0;
    454            for (int i = 0; i < strips.length; i++) {
    455                if (is2D) {
    456                    strips[i] = T4AndT6Compression.compressT4_2D(strips[i], width, strips[i].length / ((width + 7) / 8), hasFillBitsBeforeEOL, rowsPerStrip);
    457                } else {
    458                    strips[i] = T4AndT6Compression.compressT4_1D(strips[i], width, strips[i].length / ((width + 7) / 8), hasFillBitsBeforeEOL);
    459                }
    460            }
    461            break;
    462        }
    463        case TiffConstants.COMPRESSION_CCITT_GROUP_4: {
    464            final Integer t6Parameter = params.getT6Options();
    465            if (t6Parameter != null) {
    466                t6Options = t6Parameter.intValue();
    467            }
    468            t6Options &= 0x4;
    469            final boolean usesUncompressedMode = (t6Options & TiffConstants.FLAG_T6_OPTIONS_UNCOMPRESSED_MODE) != 0;
    470            if (usesUncompressedMode) {
    471                throw new ImagingException("T.6 compression with the uncompressed mode extension is not yet supported");
    472            }
    473            for (int i = 0; i < strips.length; i++) {
    474                strips[i] = T4AndT6Compression.compressT6(strips[i], width, strips[i].length / ((width + 7) / 8));
    475            }
    476            break;
    477        }
    478        case TiffConstants.COMPRESSION_PACKBITS:
    479            for (int i = 0; i < strips.length; i++) {
    480                strips[i] = PackBits.compress(strips[i]);
    481            }
    482            break;
    483        case TiffConstants.COMPRESSION_LZW:
    484            predictor = TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING;
    485            for (int i = 0; i < strips.length; i++) {
    486                final byte[] uncompressed = strips[i];
    487                applyPredictor(width, samplesPerPixel, strips[i]);
    488
    489                final int LZW_MINIMUM_CODE_SIZE = 8;
    490                final MyLzwCompressor compressor = new MyLzwCompressor(LZW_MINIMUM_CODE_SIZE, ByteOrder.BIG_ENDIAN, true);
    491                final byte[] compressed = compressor.compress(uncompressed);
    492                strips[i] = compressed;
    493            }
    494            break;
    495        case TiffConstants.COMPRESSION_DEFLATE_ADOBE:
    496            predictor = TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING;
    497            for (int i = 0; i < strips.length; i++) {
    498                applyPredictor(width, samplesPerPixel, strips[i]);
    499                strips[i] = ZlibDeflate.compress(strips[i]);
    500            }
    501            break;
    502        case TiffConstants.COMPRESSION_UNCOMPRESSED:
    503            break;
    504        default:
    505            throw new ImagingException(
    506                    "Invalid compression parameter (Only CCITT 1D/Group 3/Group 4, LZW, Packbits, Zlib Deflate and uncompressed supported).");
    507        }
    508
    509        final AbstractTiffElement.DataElement[] imageData = new AbstractTiffElement.DataElement[strips.length];
    510        Arrays.setAll(imageData, i -> new AbstractTiffImageData.Data(0, strips[i].length, strips[i]));
    511
    512        final TiffOutputSet outputSet = new TiffOutputSet(byteOrder);
    513        final TiffOutputDirectory directory = outputSet.addRootDirectory();
    514
    515        // WriteField stripOffsetsField;
    516
    517        directory.add(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, width);
    518        directory.add(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, height);
    519        directory.add(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION, (short) photometricInterpretation);
    520        directory.add(TiffTagConstants.TIFF_TAG_COMPRESSION, (short) compression);
    521        directory.add(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL, (short) samplesPerPixel);
    522
    523        switch (samplesPerPixel) {
    524        case 3:
    525            directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample, (short) bitsPerSample, (short) bitsPerSample);
    526            break;
    527        case 4:
    528            directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample, (short) bitsPerSample, (short) bitsPerSample,
    529                    (short) bitsPerSample);
    530            directory.add(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES, (short) TiffTagConstants.EXTRA_SAMPLE_UNASSOCIATED_ALPHA);
    531            break;
    532        case 1:
    533            directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample);
    534            break;
    535        default:
    536            break;
    537        }
    538        // {
    539        // stripOffsetsField = new WriteField(TIFF_TAG_STRIP_OFFSETS,
    540        // FIELD_TYPE_LONG, stripOffsets.length, FIELD_TYPE_LONG
    541        // .writeData(stripOffsets, byteOrder));
    542        // directory.add(stripOffsetsField);
    543        // }
    544        // {
    545        // WriteField field = new WriteField(TIFF_TAG_STRIP_BYTE_COUNTS,
    546        // FIELD_TYPE_LONG, stripByteCounts.length,
    547        // FIELD_TYPE_LONG.writeData(stripByteCounts,
    548        // WRITE_BYTE_ORDER));
    549        // directory.add(field);
    550        // }
    551        directory.add(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP, rowsPerStrip);
    552        if (pixelDensity.isUnitless()) {
    553            directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, (short) 0);
    554            directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, RationalNumber.valueOf(pixelDensity.getRawHorizontalDensity()));
    555            directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, RationalNumber.valueOf(pixelDensity.getRawVerticalDensity()));
    556        } else if (pixelDensity.isInInches()) {
    557            directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, (short) 2);
    558            directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, RationalNumber.valueOf(pixelDensity.horizontalDensityInches()));
    559            directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, RationalNumber.valueOf(pixelDensity.verticalDensityInches()));
    560        } else {
    561            directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, (short) 1);
    562            directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, RationalNumber.valueOf(pixelDensity.horizontalDensityCentimetres()));
    563            directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, RationalNumber.valueOf(pixelDensity.verticalDensityCentimetres()));
    564        }
    565        if (t4Options != 0) {
    566            directory.add(TiffTagConstants.TIFF_TAG_T4_OPTIONS, t4Options);
    567        }
    568        if (t6Options != 0) {
    569            directory.add(TiffTagConstants.TIFF_TAG_T6_OPTIONS, t6Options);
    570        }
    571
    572        if (null != xmpXml) {
    573            final byte[] xmpXmlBytes = xmpXml.getBytes(StandardCharsets.UTF_8);
    574            directory.add(TiffTagConstants.TIFF_TAG_XMP, xmpXmlBytes);
    575        }
    576
    577        if (predictor == TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING) {
    578            directory.add(TiffTagConstants.TIFF_TAG_PREDICTOR, predictor);
    579        }
    580
    581        final AbstractTiffImageData abstractTiffImageData = new AbstractTiffImageData.Strips(imageData, rowsPerStrip);
    582        directory.setTiffImageData(abstractTiffImageData);
    583
    584        if (userExif != null) {
    585            combineUserExifIntoFinalExif(userExif, outputSet);
    586        }
    587
    588        write(os, outputSet);
    589    }
    590
    591    protected void writeImageFileHeader(final AbstractBinaryOutputStream bos) throws IOException {
    592        writeImageFileHeader(bos, TiffConstants.HEADER_SIZE);
    593    }
    594
    595    protected void writeImageFileHeader(final AbstractBinaryOutputStream bos, final long offsetToFirstIFD) throws IOException {
    596        if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
    597            bos.write('I');
    598            bos.write('I');
    599        } else {
    600            bos.write('M');
    601            bos.write('M');
    602        }
    603
    604        bos.write2Bytes(42); // tiffVersion
    605
    606        bos.write4Bytes((int) offsetToFirstIFD);
    607    }
    608
    609}