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