View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.tiff.write;
18  
19  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.DEFAULT_TIFF_BYTE_ORDER;
20  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_1D;
21  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_3;
22  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_4;
23  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_DEFLATE_ADOBE;
24  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_LZW;
25  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_PACKBITS;
26  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_UNCOMPRESSED;
27  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE;
28  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_HEADER_SIZE;
29  
30  import java.awt.image.BufferedImage;
31  import java.awt.image.ColorModel;
32  import java.io.IOException;
33  import java.io.OutputStream;
34  import java.nio.ByteOrder;
35  import java.nio.charset.StandardCharsets;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.Collections;
39  import java.util.HashMap;
40  import java.util.HashSet;
41  import java.util.List;
42  import java.util.Map;
43  
44  import org.apache.commons.imaging.ImagingException;
45  import org.apache.commons.imaging.PixelDensity;
46  import org.apache.commons.imaging.common.Allocator;
47  import org.apache.commons.imaging.common.BinaryOutputStream;
48  import org.apache.commons.imaging.common.PackBits;
49  import org.apache.commons.imaging.common.RationalNumber;
50  import org.apache.commons.imaging.common.ZlibDeflate;
51  import org.apache.commons.imaging.formats.tiff.AbstractTiffElement;
52  import org.apache.commons.imaging.formats.tiff.AbstractTiffImageData;
53  import org.apache.commons.imaging.formats.tiff.TiffImagingParameters;
54  import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
55  import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants;
56  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
57  import org.apache.commons.imaging.formats.tiff.itu_t4.T4AndT6Compression;
58  import org.apache.commons.imaging.mylzw.MyLzwCompressor;
59  
60  public abstract class AbstractTiffImageWriter {
61  
62      private static final int MAX_PIXELS_FOR_RGB = 1024 * 1024;
63  
64      protected static int imageDataPaddingLength(final int dataLength) {
65          return (4 - dataLength % 4) % 4;
66      }
67  
68      protected final ByteOrder byteOrder;
69  
70      public AbstractTiffImageWriter() {
71          this.byteOrder = DEFAULT_TIFF_BYTE_ORDER;
72      }
73  
74      public AbstractTiffImageWriter(final ByteOrder byteOrder) {
75          this.byteOrder = byteOrder;
76      }
77  
78      private void applyPredictor(final int width, final int bytesPerSample, final byte[] b) {
79          final int nBytesPerRow = bytesPerSample * width;
80          final int nRows = b.length / nBytesPerRow;
81          for (int iRow = 0; iRow < nRows; iRow++) {
82              final int offset = iRow * nBytesPerRow;
83              for (int i = nBytesPerRow - 1; i >= bytesPerSample; i--) {
84                  b[offset + i] -= b[offset + i - bytesPerSample];
85              }
86          }
87      }
88  
89      /**
90       * Check an image to see if any of its pixels are non-opaque.
91       *
92       * @param src a valid image
93       * @return true if at least one non-opaque pixel is found.
94       */
95      private boolean checkForActualAlpha(final BufferedImage src) {
96          // to conserve memory, very large images may be read
97          // in pieces.
98          final int width = src.getWidth();
99          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 }