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}