-
-
Notifications
You must be signed in to change notification settings - Fork 312
/
TIFFImageWriter.java
1200 lines (983 loc) · 52.4 KB
/
TIFFImageWriter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.color.ColorProfiles;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter;
import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.io.enc.EncoderStream;
import com.twelvemonkeys.io.enc.PackBitsEncoder;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.*;
import javax.imageio.event.IIOWriteWarningListener;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.color.*;
import java.awt.image.*;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME;
import static com.twelvemonkeys.imageio.plugins.tiff.TIFFStreamMetadata.configureStreamByteOrder;
/**
* TIFFImageWriter
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFFImageWriter.java,v 1.0 18.09.13 12:46 haraldk Exp$
*/
public final class TIFFImageWriter extends ImageWriterBase {
// Long term
// TODO: Support tiling
// TODO: Support thumbnails
// TODO: Support JPEG compression of CMYK data (pending JPEGImageWriter CMYK write support)
// ----
// TODO: Support use-case: Transcode multi-layer PSD to multi-page TIFF with metadata (hard, as Photoshop don't store layers as multi-page TIFF...)
// TODO: Support use-case: Transcode multi-page TIFF to multiple single-page TIFFs with metadata
// TODO: Support use-case: Losslessly transcode JPEG to JPEG-in-TIFF with (EXIF) metadata (and back)
// Very long term...
// TODO: Support JBIG compression via ImageIO plugin/delegate? Pending support in Reader
// TODO: Support JPEG2000 compression via ImageIO plugin/delegate? Pending support in Reader
// Done
// Create a basic writer that supports most inputs. Store them using the simplest possible format.
// Support no compression (None/1) - BASELINE
// Support predictor. See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
// Support PackBits compression (32773) - easy - BASELINE
// Support ZLIB (/Deflate) compression (8) - easy
// Support LZW compression (5)
// Support JPEG compression (7) - might need extra input to allow multiple images with single DQT
// Use sensible defaults for compression based on input? None is sensible... :-)
// Support resolution, resolution unit and software tags from ImageIO metadata
// Support CCITT Modified Huffman compression (2)
// Full "Baseline TIFF" support (pending CCITT compression 2)
// CCITT compressions T.4 and T.6
// Support storing multiple images in one stream (multi-page TIFF)
// Support more of the ImageIO metadata (ie. compression from metadata, etc)
/**
* Flag for active sequence writing
*/
private boolean writingSequence = false;
private int sequenceIndex = 0;
/**
* Metadata writer for sequence writing
*/
private TIFFWriter sequenceTIFFWriter = null;
/**
* Position of last IFD Pointer on active sequence writing
*/
private long sequenceLastIFDPos = -1;
TIFFImageWriter(final ImageWriterSpi provider) {
super(provider);
}
@Override
public void setOutput(final Object output) {
super.setOutput(output);
// TODO: Allow appending/partly overwrite of existing file...
}
@Override
public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam param) throws IOException {
prepareWriteSequence(streamMetadata);
writeToSequence(image, param);
endWriteSequence();
}
private long writePage(int imageIndex, IIOImage image, ImageWriteParam param, TIFFWriter tiffWriter, long lastIFDPointerOffset)
throws IOException {
RenderedImage renderedImage = image.getRenderedImage();
SampleModel sampleModel = renderedImage.getSampleModel();
// Need ImageTypeSpecifiers.createFromRenderedImage in this case, as the JDK method does not consider
// palette for TYPE_BYTE_BINARY/TYPE_BYTE_INDEXED...
ImageTypeSpecifier spec = ImageTypeSpecifiers.createFromRenderedImage(renderedImage);
// TODO: Handle case where convertImageMetadata returns null, due to unknown metadata format, or reconsider if that's a valid case...
TIFFImageMetadata metadata = image.getMetadata() != null
? convertImageMetadata(image.getMetadata(), spec, param)
: getDefaultImageMetadata(spec, param);
int numBands = sampleModel.getNumBands();
int pixelSize = computePixelSize(sampleModel);
int[] bandOffsets;
int[] bitOffsets;
if (sampleModel instanceof ComponentSampleModel) {
bandOffsets = ((ComponentSampleModel) sampleModel).getBandOffsets();
bitOffsets = null;
}
else if (sampleModel instanceof SinglePixelPackedSampleModel) {
bitOffsets = ((SinglePixelPackedSampleModel) sampleModel).getBitOffsets();
bandOffsets = null;
}
else if (sampleModel instanceof MultiPixelPackedSampleModel) {
bitOffsets = null;
bandOffsets = new int[] {0};
}
else {
throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel);
}
short offsetType = tiffWriter.offsetSize() == 4 ? TIFF.TYPE_LONG : TIFF.TYPE_LONG8;
Map<Integer, Entry> entries = new LinkedHashMap<>();
// Copy metadata to output
Directory metadataIFD = metadata.getIFD();
for (Entry entry : metadataIFD) {
entries.put((Integer) entry.getIdentifier(), entry);
}
entries.put(TIFF.TAG_IMAGE_WIDTH, new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth()));
entries.put(TIFF.TAG_IMAGE_HEIGHT, new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight()));
// TODO: RowsPerStrip - can be entire image (or even 2^32 -1), but it's recommended to write "about 8K bytes" per strip
entries.put(TIFF.TAG_ROWS_PER_STRIP, new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, renderedImage.getHeight()));
// StripByteCounts - for no compression, entire image data...
entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, offsetType, -1)); // Updated later
// StripOffsets - can be offset to single strip only
entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, offsetType, -1)); // Updated later
// TODO: If tiled, write tile indexes etc
// Depending on param.getTilingMode
long nextIFDPointerOffset = -1;
int compression = ((Number) entries.get(TIFF.TAG_COMPRESSION).getValue()).intValue();
if (compression == TIFFBaseline.COMPRESSION_NONE) {
// This implementation, allows semi-streaming-compatible uncompressed TIFFs
long streamPosition = imageOutput.getStreamPosition();
long ifdSize = tiffWriter.computeIFDSize(entries.values());
long stripOffset = streamPosition + tiffWriter.offsetSize() + ifdSize + tiffWriter.offsetSize();
long stripByteCount = renderedImage.getHeight() * (((long) renderedImage.getWidth() * pixelSize + 7L) / 8L);
entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, offsetType, stripOffset));
entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, offsetType, stripByteCount));
long ifdPointer = tiffWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes care of ordering tags
nextIFDPointerOffset = imageOutput.getStreamPosition();
// If we have a previous IFD, update pointer
if (streamPosition > lastIFDPointerOffset) {
imageOutput.seek(lastIFDPointerOffset);
tiffWriter.writeOffset(imageOutput, ifdPointer);
imageOutput.seek(nextIFDPointerOffset);
}
tiffWriter.writeOffset(imageOutput, 0); // Update next IFD pointer later
}
else {
tiffWriter.writeOffset(imageOutput, 0); // Update current IFD pointer later
}
long stripOffset = imageOutput.getStreamPosition();
// TODO: Create compressor stream per Tile/Strip
// TODO: Cache JPEGImageWriter, dispose in dispose() method
if (compression == TIFFExtension.COMPRESSION_JPEG) {
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("JPEG");
if (!writers.hasNext()) {
// This can only happen if someone deliberately uninstalled it
throw new IIOException("No JPEG ImageWriter found!");
}
ImageWriter jpegWriter = writers.next();
try {
jpegWriter.setOutput(new SubImageOutputStream(imageOutput));
ListenerDelegate listener = new ListenerDelegate(imageIndex);
jpegWriter.addIIOWriteProgressListener(listener);
jpegWriter.addIIOWriteWarningListener(listener);
jpegWriter.write(null, imageOnly(image), copyParams(param, jpegWriter));
}
finally {
jpegWriter.dispose();
}
}
else {
// Write image data
writeImageData(createCompressorStream(renderedImage, param, entries), imageIndex, renderedImage, numBands, bandOffsets, bitOffsets);
}
long stripByteCount = imageOutput.getStreamPosition() - stripOffset;
// Update IFD0-pointer, and write IFD
if (compression != TIFFBaseline.COMPRESSION_NONE) {
entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, offsetType, stripOffset));
entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, offsetType, stripByteCount));
long ifdPointer = tiffWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes care of ordering tags
nextIFDPointerOffset = imageOutput.getStreamPosition();
// TODO: This is slightly duped....
// However, need to update here, because to the writeIFD method writes the pointer, but at the incorrect offset
// TODO: Refactor writeIFD to take an offset
imageOutput.seek(lastIFDPointerOffset);
tiffWriter.writeOffset(imageOutput, ifdPointer);
imageOutput.seek(nextIFDPointerOffset);
tiffWriter.writeOffset(imageOutput, 0); // Next IFD pointer updated later
}
return nextIFDPointerOffset;
}
private IIOImage imageOnly(final IIOImage image) {
if (image.getMetadata() == null && image.getNumThumbnails() == 0) {
// Just image data here, no need to copy
return image;
}
return image.hasRaster()
? new IIOImage(image.getRaster(), null, null)
: new IIOImage(image.getRenderedImage(), null, null);
}
// TODO: Candidate util method
private ImageWriteParam copyParams(final ImageWriteParam param, final ImageWriter writer) {
if (param == null) {
return null;
}
// Always safe
ImageWriteParam writeParam = writer.getDefaultWriteParam();
writeParam.setSourceSubsampling(param.getSourceXSubsampling(), param.getSourceYSubsampling(), param.getSubsamplingXOffset(), param.getSubsamplingYOffset());
writeParam.setSourceRegion(param.getSourceRegion());
writeParam.setSourceBands(param.getSourceBands());
// Only if canWriteCompressed()
writeParam.setCompressionMode(param.getCompressionMode());
if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) {
writeParam.setCompressionQuality(param.getCompressionQuality());
}
return writeParam;
}
// TODO: Candidate util method
private int computePixelSize(final SampleModel sampleModel) {
int size = 0;
for (int i = 0; i < sampleModel.getNumBands(); i++) {
size += sampleModel.getSampleSize(i);
}
return size;
}
private DataOutput createCompressorStream(final RenderedImage image, final ImageWriteParam param, final Map<Integer, Entry> entries) {
/*
36 MB test data:
No compression:
Write time: 450 ms
output.length: 36000226
PackBits:
Write time: 688 ms
output.length: 30322187
Deflate, BEST_SPEED (1):
Write time: 1276 ms
output.length: 14128866
Deflate, 2:
Write time: 1297 ms
output.length: 13848735
Deflate, 3:
Write time: 1594 ms
output.length: 13103224
Deflate, 4:
Write time: 1663 ms
output.length: 13380899 (!!)
5
Write time: 1941 ms
output.length: 13171244
6
Write time: 2311 ms
output.length: 12845101
7: Write time: 2853 ms
output.length: 12759426
8:
Write time: 4429 ms
output.length: 12624517
Deflate: DEFAULT_COMPRESSION (6?):
Write time: 2357 ms
output.length: 12845101
Deflate, BEST_COMPRESSION (9):
Write time: 4998 ms
output.length: 12600399
*/
int samplesPerPixel = (Integer) entries.get(TIFF.TAG_SAMPLES_PER_PIXEL).getValue();
int bitPerSample = ((short[]) entries.get(TIFF.TAG_BITS_PER_SAMPLE).getValue())[0];
// Use predictor by default for LZW and ZLib/Deflate
// TODO: Unless explicitly disabled in TIFFImageWriteParam
int compression = ((Number) entries.get(TIFF.TAG_COMPRESSION).getValue()).intValue();
OutputStream stream;
switch (compression) {
case TIFFBaseline.COMPRESSION_NONE:
return imageOutput;
case TIFFBaseline.COMPRESSION_PACKBITS:
stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new EncoderStream(stream, new PackBitsEncoder(), true);
// NOTE: PackBits + Predictor is possible, but not generally supported, disable it by default
// (and probably not even allow it, see http://stackoverflow.com/questions/20337400/tiff-packbits-compression-with-predictor-step)
return new DataOutputStream(stream);
case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE:
// NOTE: This interpretation does the opposite of the JAI TIFFImageWriter, but seems more correct.
// API Docs says:
// A compression quality setting of 0.0 is most generically interpreted as "high compression is important,"
// while a setting of 1.0 is most generically interpreted as "high image quality is important."
// However, the JAI TIFFImageWriter uses:
// if (param & compression etc...) {
// float quality = param.getCompressionQuality();
// deflateLevel = (int)(1 + 8*quality);
// } else {
// deflateLevel = Deflater.DEFAULT_COMPRESSION;
// }
// (in other words, 0.0 means 1 == BEST_SPEED, 1.0 means 9 == BEST_COMPRESSION)
// PS: PNGImageWriter just uses hardcoded BEST_COMPRESSION... :-P
int deflateSetting = Deflater.BEST_SPEED; // This is consistent with default compression quality being 1.0 and 0 meaning max compression...
if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) {
deflateSetting = Deflater.BEST_COMPRESSION - Math.round((Deflater.BEST_COMPRESSION - 1) * param.getCompressionQuality());
}
stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new DeflaterOutputStream(stream, new Deflater(deflateSetting), 1024);
if (entries.containsKey(TIFF.TAG_PREDICTOR) && entries.get(TIFF.TAG_PREDICTOR).getValue().equals(TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)) {
stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), samplesPerPixel, bitPerSample, imageOutput.getByteOrder());
}
return new DataOutputStream(stream);
case TIFFExtension.COMPRESSION_LZW:
stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new EncoderStream(stream, new LZWEncoder((((long) image.getTileWidth() * samplesPerPixel * bitPerSample + 7) / 8) * image.getTileHeight()));
if (entries.containsKey(TIFF.TAG_PREDICTOR) && entries.get(TIFF.TAG_PREDICTOR).getValue().equals(TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)) {
stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), samplesPerPixel, bitPerSample, imageOutput.getByteOrder());
}
return new DataOutputStream(stream);
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
case TIFFExtension.COMPRESSION_CCITT_T4:
case TIFFExtension.COMPRESSION_CCITT_T6:
if (image.getSampleModel().getNumBands() != 1 || image.getSampleModel().getSampleSize(0) != 1) {
throw new IllegalArgumentException("CCITT compressions supports 1 sample/pixel, 1 bit/sample only");
}
long option = 0L;
if (compression != TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE) {
Entry optionsEntry = entries.get(compression == TIFFExtension.COMPRESSION_CCITT_T4 ? TIFF.TAG_GROUP3OPTIONS : TIFF.TAG_GROUP4OPTIONS);
option = ((Number) optionsEntry.getValue()).longValue();
}
Entry fillOrderEntry = entries.get(TIFF.TAG_FILL_ORDER);
int fillOrder = (int) (fillOrderEntry != null ? fillOrderEntry.getValue() : TIFFBaseline.FILL_LEFT_TO_RIGHT);
stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new CCITTFaxEncoderStream(stream, image.getTileWidth(), image.getTileHeight(), compression, fillOrder, option);
return new DataOutputStream(stream);
}
throw new IllegalArgumentException(String.format("Unsupported TIFF compression: %d", compression));
}
private int getPhotometricInterpretation(final ColorModel colorModel, int compression) {
if (colorModel.getPixelSize() == 1) {
if (colorModel instanceof IndexColorModel) {
if (colorModel.getRGB(0) == 0xFFFFFFFF && colorModel.getRGB(1) == 0xFF000000) {
return TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO;
}
else if (colorModel.getRGB(0) != 0xFF000000 || colorModel.getRGB(1) != 0xFFFFFFFF) {
return TIFFBaseline.PHOTOMETRIC_PALETTE;
}
// Else, fall through to default, BLACK_IS_ZERO
}
return TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO;
}
else if (colorModel instanceof IndexColorModel) {
return TIFFBaseline.PHOTOMETRIC_PALETTE;
}
switch (colorModel.getColorSpace().getType()) {
case ColorSpace.TYPE_GRAY:
return TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO;
case ColorSpace.TYPE_RGB:
return compression == TIFFExtension.COMPRESSION_JPEG ? TIFFExtension.PHOTOMETRIC_YCBCR : TIFFBaseline.PHOTOMETRIC_RGB;
case ColorSpace.TYPE_CMYK:
return TIFFExtension.PHOTOMETRIC_SEPARATED;
}
throw new IllegalArgumentException("Can't determine PhotometricInterpretation for color model: " + colorModel);
}
private short[] createColorMap(final IndexColorModel colorModel, final int sampleSize) {
// TIFF6.pdf p. 23:
// A TIFF color map is stored as type SHORT, count = 3 * (2^BitsPerSample)
// "In a TIFF ColorMap, all the Red values come first, followed by the Green values, then the Blue values.
// In the ColorMap, black is represented by 0,0,0 and white is represented by 65535, 65535, 65535."
short[] colorMap = new short[(int) (3 * Math.pow(2, sampleSize))];
for (int i = 0; i < colorModel.getMapSize(); i++) {
int color = colorModel.getRGB(i);
colorMap[i] = (short) upScale((color >> 16) & 0xff);
colorMap[i + colorMap.length / 3] = (short) upScale((color >> 8) & 0xff);
colorMap[i + 2 * colorMap.length / 3] = (short) upScale((color) & 0xff);
}
return colorMap;
}
private int upScale(final int color) {
return 257 * color;
}
private short[] asShortArray(final int[] integers) {
short[] shorts = new short[integers.length];
for (int i = 0; i < shorts.length; i++) {
shorts[i] = (short) integers[i];
}
return shorts;
}
private void writeImageData(DataOutput stream, int imageIndex, RenderedImage renderedImage, int numComponents, int[] bandOffsets, int[] bitOffsets) throws IOException {
// Store 3BYTE, 4BYTE as is (possibly need to re-arrange to RGB order)
// Store INT_RGB as 3BYTE, INT_ARGB as 4BYTE?, INT_ABGR must be re-arranged
// Store IndexColorModel as is
// Store BYTE_GRAY as is
// Store USHORT_GRAY as is
processImageStarted(imageIndex);
final int minTileY = renderedImage.getMinTileY();
final int maxYTiles = minTileY + renderedImage.getNumYTiles();
final int minTileX = renderedImage.getMinTileX();
final int maxXTiles = minTileX + renderedImage.getNumXTiles();
// Use buffer to have longer, better performing writes
final int tileHeight = renderedImage.getTileHeight();
final int tileWidth = renderedImage.getTileWidth();
// TODO: SampleSize may differ between bands/banks
final int sampleSize = renderedImage.getSampleModel().getSampleSize(0);
final int numBands = renderedImage.getSampleModel().getNumBands();
// TODO: This buffer should probably have order matching that of imageOutput, but only if writing "actual" 16 or 32 bit samples, not "packed" samples
final byte[] buffer = new byte[(tileWidth * numBands * sampleSize + 7) / 8];
int bufferPos = 0;
for (int yTile = minTileY; yTile < maxYTiles; yTile++) {
for (int xTile = minTileX; xTile < maxXTiles; xTile++) {
final Raster tile = renderedImage.getTile(xTile, yTile);
// Model translation
final int offsetX = tile.getMinX() - tile.getSampleModelTranslateX();
final int offsetY = tile.getMinY() - tile.getSampleModelTranslateY();
// Scanline stride, not accounting for model translation
final int stride = (tile.getSampleModel().getWidth() * sampleSize + 7) / 8;
final DataBuffer dataBuffer = tile.getDataBuffer();
switch (dataBuffer.getDataType()) {
case DataBuffer.TYPE_BYTE:
// System.err.println("Writing " + numBands + "BYTE -> " + numBands + "BYTE");
int steps = (tileWidth * sampleSize + 7) / 8;
// Shift needed for "packed" samples with "odd" offset
int shift = offsetX % 8;
// TODO: Generalize this code, to always use row raster
final WritableRaster rowRaster = shift != 0 ? tile.createCompatibleWritableRaster(tile.getWidth(), 1) : null;
final DataBuffer rowBuffer = shift != 0 ? rowRaster.getDataBuffer() : null;
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = offsetY; y < tileHeight + offsetY; y++) {
final int yOff = y * stride * numBands;
if (shift != 0) {
rowRaster.setDataElements(0, 0, tile.createChild(0, y - offsetY, tile.getWidth(), 1, 0, 0, null));
}
for (int x = offsetX; x < steps + offsetX; x++) {
final int xOff = yOff + x * numBands;
for (int s = 0; s < numBands; s++) {
if (sampleSize == 8 || shift == 0) {
// Normal interleaved/planar case
buffer[bufferPos++] = ((byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff));
}
else {
// "Packed" case
buffer[bufferPos++] = ((byte) (rowBuffer.getElem(b, x - offsetX + bandOffsets[s]) & 0xff));
}
}
}
flushBuffer(buffer, bufferPos, stream);
bufferPos = 0;
flushStream(stream);
}
}
break;
case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_SHORT:
final int shortStride = stride / 2;
if (numComponents == 1) {
// System.err.println("Writing USHORT -> " + numBands * 2 + "_BYTES");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = offsetY; y < tileHeight + offsetY; y++) {
final int yOff = y * shortStride;
for (int x = offsetX; x < tileWidth + offsetX; x++) {
int xOff = yOff + x;
int elem = dataBuffer.getElem(b, xOff);
buffer[bufferPos++] = (byte) ((elem >>> 8) & 0xff);
buffer[bufferPos++] = (byte) (elem & 0xff);
}
flushBuffer(buffer, bufferPos, stream);
bufferPos = 0;
flushStream(stream);
}
}
}
else {
// for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
// for (int y = 0; y < tileHeight; y++) {
// final int yOff = y * tileWidth;
//
// for (int x = 0; x < tileWidth; x++) {
// final int xOff = yOff + x;
// int element = dataBuffer.getElem(b, xOff);
//
// for (int s = 0; s < numBands; s++) {
// buffer.put((byte) ((element >> bitOffsets[s]) & 0xff));
// }
// }
//
// flushBuffer(buffer, stream);
// if (stream instanceof DataOutputStream) {
// DataOutputStream dataOutputStream = (DataOutputStream) stream;
// dataOutputStream.flush();
// }
// }
// }
throw new IllegalArgumentException("Not implemented for data type: " + dataBuffer.getDataType());
}
break;
case DataBuffer.TYPE_INT:
// TODO: This is incorrect for general 32 bits/sample, only works for packed (INT_(A)RGB) and single channel
final int intStride = stride / 4;
if (1 == numComponents) {
// System.err.println("Writing INT -> " + numBands * 4 + "_BYTES");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = offsetY; y < tileHeight + offsetY; y++) {
int yOff = y * intStride;
for (int x = offsetX; x < tileWidth + offsetX; x++) {
int xOff = yOff + x;
int elem = dataBuffer.getElem(b, xOff);
buffer[bufferPos++] = (byte) ((elem >>> 24) & 0xff);
buffer[bufferPos++] = (byte) ((elem >>> 16) & 0xff);
buffer[bufferPos++] = (byte) ((elem >>> 8) & 0xff);
buffer[bufferPos++] = (byte) (elem & 0xff);
}
flushBuffer(buffer, bufferPos, stream);
bufferPos = 0;
flushStream(stream);
}
}
}
else {
// System.err.println("Writing INT -> " + numBands + "_BYTES");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = 0; y < tileHeight; y++) {
final int yOff = y * tileWidth;
for (int x = 0; x < tileWidth; x++) {
final int xOff = yOff + x;
int element = dataBuffer.getElem(b, xOff);
for (int s = 0; s < numBands; s++) {
buffer[bufferPos++] = (byte) ((element >> bitOffsets[s]) & 0xff);
}
}
flushBuffer(buffer, bufferPos, stream);
bufferPos = 0;
flushStream(stream);
}
}
}
break;
default:
throw new IllegalArgumentException("Not implemented for data type: " + dataBuffer.getDataType());
}
}
flushStream(stream);
// TODO: Report better progress
processImageProgress((100f * (yTile + 1)) / maxYTiles);
}
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.close();
}
processImageComplete();
}
private static void flushStream(DataOutput stream) throws IOException {
// Need to flush/start new compression for each row, for proper LZW/PackBits/Deflate/ZLib
if (stream instanceof DataOutputStream) {
DataOutputStream dataOutputStream = (DataOutputStream) stream;
dataOutputStream.flush();
}
}
private static void flushBuffer(final byte[] buffer, final int bufferPos, final DataOutput stream) throws IOException {
stream.write(buffer, 0, bufferPos);
}
// Metadata
@Override
public TIFFImageMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) {
return initMeta(null, imageType, param);
}
@Override
public TIFFImageMetadata convertImageMetadata(final IIOMetadata inData,
final ImageTypeSpecifier imageType,
final ImageWriteParam param) {
Validate.notNull(inData, "inData");
Validate.notNull(imageType, "imageType");
Directory ifd;
if (inData instanceof TIFFImageMetadata) {
ifd = ((TIFFImageMetadata) inData).getIFD();
}
else {
TIFFImageMetadata outData = new TIFFImageMetadata(Collections.<Entry>emptySet());
try {
if (Arrays.asList(inData.getMetadataFormatNames()).contains(SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME)) {
outData.setFromTree(SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, inData.getAsTree(SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME));
}
else if (inData.isStandardMetadataFormatSupported()) {
outData.setFromTree(IIOMetadataFormatImpl.standardMetadataFormatName, inData.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName));
}
else {
// Unknown format, we can't convert it
return null;
}
}
catch (IIOInvalidTreeException e) {
processWarningOccurred(sequenceIndex, "Could not convert image meta data: " + e.getMessage());
}
ifd = outData.getIFD();
}
// Overwrite in values with values from imageType and param as needed
return initMeta(ifd, imageType, param);
}
private TIFFImageMetadata initMeta(final Directory ifd, final ImageTypeSpecifier imageType, final ImageWriteParam param) {
Validate.notNull(imageType, "imageType");
Map<Integer, Entry> entries = new LinkedHashMap<>(ifd != null ? ifd.size() + 10 : 20);
// Set software as default, may be overwritten
entries.put(TIFF.TAG_SOFTWARE, new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion()));
entries.put(TIFF.TAG_ORIENTATION, new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional)
mergeSafeMetadata(ifd, entries);
ColorModel colorModel = imageType.getColorModel();
SampleModel sampleModel = imageType.getSampleModel();
int numBands = sampleModel.getNumBands();
int pixelSize = computePixelSize(sampleModel);
entries.put(TIFF.TAG_BITS_PER_SAMPLE, new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize())));
// Compression field from param or metadata
int compression;
if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA)
&& ifd != null && ifd.getEntryById(TIFF.TAG_COMPRESSION) != null) {
compression = ((Number) ifd.getEntryById(TIFF.TAG_COMPRESSION).getValue()).intValue();
}
else {
compression = TIFFImageWriteParam.getCompressionType(param);
}
entries.put(TIFF.TAG_COMPRESSION, new TIFFEntry(TIFF.TAG_COMPRESSION, compression));
// TODO: Allow metadata to take precedence?
int photometricInterpretation = getPhotometricInterpretation(colorModel, compression);
entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFF.TYPE_SHORT, photometricInterpretation));
// If numComponents > numColorComponents, write ExtraSamples
if (numBands > colorModel.getNumColorComponents()) {
// TODO: Write per component > numColorComponents
if (colorModel.hasAlpha()) {
entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA));
}
else {
entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED));
}
}
switch (compression) {
case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE:
case TIFFExtension.COMPRESSION_LZW:
// TODO: Let param/metadata control predictor
// TODO: Depending on param.getCompressionMode(): DISABLED/EXPLICIT/COPY_FROM_METADATA/DEFAULT
if (pixelSize >= 8) {
entries.put(TIFF.TAG_PREDICTOR, new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING));
}
break;
case TIFFExtension.COMPRESSION_CCITT_T4:
Entry group3options = ifd != null ? ifd.getEntryById(TIFF.TAG_GROUP3OPTIONS) : null;
if (group3options == null) {
group3options = new TIFFEntry(TIFF.TAG_GROUP3OPTIONS, (long) TIFFExtension.GROUP3OPT_2DENCODING);
}
entries.put(TIFF.TAG_GROUP3OPTIONS, group3options);
break;
case TIFFExtension.COMPRESSION_CCITT_T6:
Entry group4options = ifd != null ? ifd.getEntryById(TIFF.TAG_GROUP4OPTIONS) : null;
if (group4options == null) {
group4options = new TIFFEntry(TIFF.TAG_GROUP4OPTIONS, 0L);
}
entries.put(TIFF.TAG_GROUP4OPTIONS, group4options);
break;
default:
}
if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) {
// TODO: Fix consistency between sampleModel.getSampleSize() and colorModel.getPixelSize()...
// We should be able to support 1, 2, 4 and 8 bits per sample at least, and probably 3, 5, 6 and 7 too
entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel, sampleModel.getSampleSize(0))));
entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
}
else {
entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numBands));
// Embed ICC profile if we have one that:
// * is not sRGB (assuming sRGB to be the default RGB interpretation), and
// * is not gray scale (assuming photometric either BlackIsZero or WhiteIsZero)
ColorSpace colorSpace = colorModel.getColorSpace();
if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB() && !ColorProfiles.isCS_GRAY(((ICC_ColorSpace) colorSpace).getProfile())) {
entries.put(TIFF.TAG_ICC_PROFILE, new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData()));
}
}
// Default sample format SAMPLEFORMAT_UINT need not be written
if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) {
entries.put(TIFF.TAG_SAMPLE_FORMAT, new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT));
}
// TODO: Float values!
return new TIFFImageMetadata(entries.values());
}
private void mergeSafeMetadata(final Directory ifd, final Map<Integer, Entry> entries) {
if (ifd == null) {
return;
}
for (Entry entry : ifd) {
int tagId = (Integer) entry.getIdentifier();
switch (tagId) {
// Baseline
case TIFF.TAG_SUBFILE_TYPE:
case TIFF.TAG_OLD_SUBFILE_TYPE:
case TIFF.TAG_IMAGE_DESCRIPTION:
case TIFF.TAG_MAKE:
case TIFF.TAG_MODEL:
case TIFF.TAG_ORIENTATION:
case TIFF.TAG_X_RESOLUTION:
case TIFF.TAG_Y_RESOLUTION:
case TIFF.TAG_RESOLUTION_UNIT:
case TIFF.TAG_SOFTWARE:
case TIFF.TAG_DATE_TIME:
case TIFF.TAG_ARTIST:
case TIFF.TAG_HOST_COMPUTER:
case TIFF.TAG_COPYRIGHT:
// Extension
case TIFF.TAG_DOCUMENT_NAME:
case TIFF.TAG_PAGE_NAME:
case TIFF.TAG_X_POSITION:
case TIFF.TAG_Y_POSITION:
case TIFF.TAG_PAGE_NUMBER:
case TIFF.TAG_XMP:
// Private/Custom
case TIFF.TAG_IPTC:
case TIFF.TAG_PHOTOSHOP:
case TIFF.TAG_PHOTOSHOP_IMAGE_SOURCE_DATA:
case TIFF.TAG_PHOTOSHOP_ANNOTATIONS:
case TIFF.TAG_EXIF_IFD:
case TIFF.TAG_GPS_IFD:
case TIFF.TAG_INTEROP_IFD:
entries.put(tagId, entry);
break;
default:
// Allow most extension and custom tags
if (tagId >= 1000 && tagId < 50706) {
entries.put(tagId, entry);
}
// Skip 50 706 - 57 080 (DNG tags)
else if (tagId > 50780 && tagId < 65000) {
entries.put(tagId, entry);
}
// Always allow "the reusable range"
else if (tagId >= 65000 && tagId <= 65535) {
entries.put(tagId, entry);
}
}
}
}
@Override
public IIOMetadata getDefaultStreamMetadata(final ImageWriteParam param) {
return super.getDefaultStreamMetadata(param);
}
@Override
public IIOMetadata convertStreamMetadata(final IIOMetadata inData, final ImageWriteParam param) {
return super.convertStreamMetadata(inData, param);
}
// Param
@Override
public ImageWriteParam getDefaultWriteParam() {
return new TIFFImageWriteParam();
}
@Override
public boolean canWriteSequence() {
return true;
}
@Override
public void prepareWriteSequence(final IIOMetadata streamMetadata) throws IOException {
if (writingSequence) {
throw new IllegalStateException("sequence writing has already been started!");
}
assertOutput();
configureStreamByteOrder(streamMetadata, imageOutput);
writingSequence = true;
sequenceTIFFWriter = new TIFFWriter(isBigTIFF() ? 8 : 4);
sequenceTIFFWriter.writeTIFFHeader(imageOutput);
sequenceLastIFDPos = imageOutput.getStreamPosition();
}
private boolean isBigTIFF() throws IOException {
return "bigtiff".equalsIgnoreCase(getFormatName());
}
@Override
public void writeToSequence(final IIOImage image, final ImageWriteParam param) throws IOException {
if (!writingSequence) {
throw new IllegalStateException("prepareWriteSequence() must be called before writeToSequence()!");
}
if (sequenceIndex > 0) {
imageOutput.flushBefore(sequenceLastIFDPos);
imageOutput.seek(imageOutput.length());
}
sequenceLastIFDPos = writePage(sequenceIndex++, image, param, sequenceTIFFWriter, sequenceLastIFDPos);
}
@Override