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