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 java.awt.image.BufferedImage;
20  import java.awt.image.ColorModel;
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.nio.ByteOrder;
24  import java.nio.charset.StandardCharsets;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.List;
31  import java.util.Map;
32  
33  import org.apache.commons.imaging.ImagingException;
34  import org.apache.commons.imaging.PixelDensity;
35  import org.apache.commons.imaging.common.AbstractBinaryOutputStream;
36  import org.apache.commons.imaging.common.Allocator;
37  import org.apache.commons.imaging.common.PackBits;
38  import org.apache.commons.imaging.common.RationalNumber;
39  import org.apache.commons.imaging.common.ZlibDeflate;
40  import org.apache.commons.imaging.formats.tiff.AbstractTiffElement;
41  import org.apache.commons.imaging.formats.tiff.AbstractTiffImageData;
42  import org.apache.commons.imaging.formats.tiff.TiffImagingParameters;
43  import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
44  import org.apache.commons.imaging.formats.tiff.constants.TiffConstants;
45  import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants;
46  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
47  import org.apache.commons.imaging.formats.tiff.itu_t4.T4AndT6Compression;
48  import org.apache.commons.imaging.mylzw.MyLzwCompressor;
49  
50  public abstract class AbstractTiffImageWriter {
51  
52      private static final int MAX_PIXELS_FOR_RGB = 1024 * 1024;
53  
54      protected static int imageDataPaddingLength(final int dataLength) {
55          return (4 - dataLength % 4) % 4;
56      }
57  
58      protected final ByteOrder byteOrder;
59  
60      public AbstractTiffImageWriter() {
61          this.byteOrder = TiffConstants.DEFAULT_TIFF_BYTE_ORDER;
62      }
63  
64      public AbstractTiffImageWriter(final ByteOrder byteOrder) {
65          this.byteOrder = byteOrder;
66      }
67  
68      private void applyPredictor(final int width, final int bytesPerSample, final byte[] b) {
69          final int nBytesPerRow = bytesPerSample * width;
70          final int nRows = b.length / nBytesPerRow;
71          for (int iRow = 0; iRow < nRows; iRow++) {
72              final int offset = iRow * nBytesPerRow;
73              for (int i = nBytesPerRow - 1; i >= bytesPerSample; i--) {
74                  b[offset + i] -= b[offset + i - bytesPerSample];
75              }
76          }
77      }
78  
79      /**
80       * Check an image to see if any of its pixels are non-opaque.
81       *
82       * @param src a valid image
83       * @return true if at least one non-opaque pixel is found.
84       */
85      private boolean checkForActualAlpha(final BufferedImage src) {
86          // to conserve memory, very large images may be read
87          // in pieces.
88          final int width = src.getWidth();
89          final int height = src.getHeight();
90          int nRowsPerRead = MAX_PIXELS_FOR_RGB / width;
91          if (nRowsPerRead < 1) {
92              nRowsPerRead = 1;
93          }
94          final int nReads = (height + nRowsPerRead - 1) / nRowsPerRead;
95          final int[] argb = Allocator.intArray(nRowsPerRead * width);
96          for (int iRead = 0; iRead < nReads; iRead++) {
97              final int i0 = iRead * nRowsPerRead;
98              final int i1 = i0 + nRowsPerRead > height ? height : i0 + nRowsPerRead;
99              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 }