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