1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
91
92
93
94
95 private boolean checkForActualAlpha(final BufferedImage src) {
96
97
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
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
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
229
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
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
289 }
290
291 if (directoryIndices.isEmpty()) {
292 throw new ImagingException("Missing root directory.");
293 }
294
295
296
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
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
317 final TiffOutputSummary result = new TiffOutputSummary(byteOrder, rootDirectory, directoryTypeMap);
318
319 if (interoperabilityDirectory == null && interoperabilityDirectoryOffsetField != null) {
320
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
337 if (exifDirectory == null && exifDirectoryOffsetField != null) {
338
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
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
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
384
385
386
387
388
389
390
391
392 final ColorModel cModel = src.getColorModel();
393 final boolean hasAlpha = cModel.hasAlpha() && checkForActualAlpha(src);
394
395
396
397
398
399
400
401
402
403 int compression = TIFF_COMPRESSION_LZW;
404 short predictor = TiffTagConstants.PREDICTOR_VALUE_NONE;
405
406 int stripSizeInBits = 64000;
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);
434
435 final byte[][] strips = getStrips(src, samplesPerPixel, bitsPerSample, rowsPerStrip);
436
437
438
439
440
441
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
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
549
550
551
552
553
554
555
556
557
558
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);
614
615 bos.write4Bytes((int) offsetToFirstIFD);
616 }
617
618 }