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