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 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
81
82
83
84
85 private boolean checkForActualAlpha(final BufferedImage src) {
86
87
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
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
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
219
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
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
279 }
280
281 if (directoryIndices.isEmpty()) {
282 throw new ImagingException("Missing root directory.");
283 }
284
285
286
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
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
307 final TiffOutputSummary result = new TiffOutputSummary(byteOrder, rootDirectory, directoryTypeMap);
308
309 if (interoperabilityDirectory == null && interoperabilityDirectoryOffsetField != null) {
310
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
327 if (exifDirectory == null && exifDirectoryOffsetField != null) {
328
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
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
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
374
375
376
377
378
379
380
381
382 final ColorModel cModel = src.getColorModel();
383 final boolean hasAlpha = cModel.hasAlpha() && checkForActualAlpha(src);
384
385
386
387
388
389
390
391
392
393 int compression = TiffConstants.COMPRESSION_LZW;
394 short predictor = TiffTagConstants.PREDICTOR_VALUE_NONE;
395
396 int stripSizeInBits = 64000;
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);
425
426 final byte[][] strips = getStrips(src, samplesPerPixel, bitsPerSample, rowsPerStrip);
427
428
429
430
431
432
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
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
540
541
542
543
544
545
546
547
548
549
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);
605
606 bos.write4Bytes((int) offsetToFirstIFD);
607 }
608
609 }