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;
18  
19  import java.awt.Dimension;
20  import java.awt.Rectangle;
21  import java.awt.image.BufferedImage;
22  import java.io.IOException;
23  import java.io.OutputStream;
24  import java.io.PrintWriter;
25  import java.nio.ByteOrder;
26  import java.nio.charset.StandardCharsets;
27  import java.util.ArrayList;
28  import java.util.List;
29  
30  import org.apache.commons.imaging.AbstractImageParser;
31  import org.apache.commons.imaging.FormatCompliance;
32  import org.apache.commons.imaging.ImageFormat;
33  import org.apache.commons.imaging.ImageFormats;
34  import org.apache.commons.imaging.ImageInfo;
35  import org.apache.commons.imaging.ImagingException;
36  import org.apache.commons.imaging.bytesource.ByteSource;
37  import org.apache.commons.imaging.common.Allocator;
38  import org.apache.commons.imaging.common.ImageBuilder;
39  import org.apache.commons.imaging.common.ImageMetadata;
40  import org.apache.commons.imaging.common.XmpEmbeddable;
41  import org.apache.commons.imaging.common.XmpImagingParameters;
42  import org.apache.commons.imaging.formats.tiff.TiffDirectory.ImageDataElement;
43  import org.apache.commons.imaging.formats.tiff.constants.TiffConstants;
44  import org.apache.commons.imaging.formats.tiff.constants.TiffEpTagConstants;
45  import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration;
46  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
47  import org.apache.commons.imaging.formats.tiff.datareaders.AbstractImageDataReader;
48  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.AbstractPhotometricInterpreter;
49  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterBiLevel;
50  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterCieLab;
51  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterCmyk;
52  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterLogLuv;
53  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterPalette;
54  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterRgb;
55  import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterYCbCr;
56  import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
57  
58  /**
59   * Implements methods for reading and writing TIFF files. Instances of this class are invoked from the general Imaging class. Applications that require the use
60   * of TIFF-specific features may instantiate and access this class directly.
61   */
62  public class TiffImageParser extends AbstractImageParser<TiffImagingParameters> implements XmpEmbeddable<TiffImagingParameters> {
63  
64      private static final String DEFAULT_EXTENSION = ImageFormats.TIFF.getDefaultExtension();
65      private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.TIFF.getExtensions();
66  
67      /**
68       * Constructs a new instance with the big-endian byte order.
69       */
70      public TiffImageParser() {
71          // empty
72      }
73  
74      private Rectangle checkForSubImage(final TiffImagingParameters params) {
75          // the params class enforces a correct specification for the
76          // sub-image, but does not have knowledge of the actual
77          // dimensions of the image that is being read. This method
78          // returns the sub-image specification, if any, and leaves
79          // further tests to the calling module.
80          if (params != null && params.isSubImageSet()) {
81              final int ix0 = params.getSubImageX();
82              final int iy0 = params.getSubImageY();
83              final int iwidth = params.getSubImageWidth();
84              final int iheight = params.getSubImageHeight();
85              return new Rectangle(ix0, iy0, iwidth, iheight);
86          }
87          return null;
88      }
89  
90      public List<byte[]> collectRawImageData(final ByteSource byteSource, final TiffImagingParameters params) throws ImagingException, IOException {
91          final FormatCompliance formatCompliance = FormatCompliance.getDefault();
92          final TiffContents contents = new TiffReader(params != null && params.isStrict()).readDirectories(byteSource, true, formatCompliance);
93  
94          final List<byte[]> result = new ArrayList<>();
95          for (int i = 0; i < contents.directories.size(); i++) {
96              final TiffDirectory directory = contents.directories.get(i);
97              final List<ImageDataElement> dataElements = directory.getTiffRawImageDataElements();
98              for (final ImageDataElement element : dataElements) {
99                  final byte[] bytes = byteSource.getByteArray(element.offset, element.length);
100                 result.add(bytes);
101             }
102         }
103         return result;
104     }
105 
106     @Override
107     public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
108         try {
109             pw.println("tiff.dumpImageFile");
110 
111             {
112                 final ImageInfo imageData = getImageInfo(byteSource);
113                 if (imageData == null) {
114                     return false;
115                 }
116 
117                 imageData.toString(pw, "");
118             }
119 
120             pw.println("");
121 
122             // try
123             {
124                 final FormatCompliance formatCompliance = FormatCompliance.getDefault();
125                 final TiffImagingParameters params = new TiffImagingParameters();
126                 final TiffContents contents = new TiffReader(true).readContents(byteSource, params, formatCompliance);
127 
128                 final List<TiffDirectory> directories = contents.directories;
129                 if (directories == null) {
130                     return false;
131                 }
132 
133                 for (int d = 0; d < directories.size(); d++) {
134                     final TiffDirectory directory = directories.get(d);
135 
136                     // Debug.debug("directory offset", directory.offset);
137 
138                     for (final TiffField field : directory) {
139                         field.dump(pw, Integer.toString(d));
140                     }
141                 }
142 
143                 pw.println("");
144             }
145             // catch (Exception e)
146             // {
147             // Debug.debug(e);
148             // pw.println("");
149             // return false;
150             // }
151 
152             return true;
153         } finally {
154             pw.println("");
155         }
156     }
157 
158     @Override
159     protected String[] getAcceptedExtensions() {
160         return ACCEPTED_EXTENSIONS;
161     }
162 
163     @Override
164     protected ImageFormat[] getAcceptedTypes() {
165         return new ImageFormat[] { ImageFormats.TIFF, //
166         };
167     }
168 
169     @Override
170     public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource) throws ImagingException, IOException {
171         final FormatCompliance formatCompliance = FormatCompliance.getDefault();
172         final TiffReader tiffReader = new TiffReader(true);
173         final TiffContents contents = tiffReader.readDirectories(byteSource, true, formatCompliance);
174         final List<BufferedImage> results = new ArrayList<>();
175         for (int i = 0; i < contents.directories.size(); i++) {
176             final TiffDirectory directory = contents.directories.get(i);
177             final BufferedImage result = directory.getTiffImage(tiffReader.getByteOrder(), null);
178             if (result != null) {
179                 results.add(result);
180             }
181         }
182         return results;
183     }
184 
185     /**
186      * <p>
187      * Gets a buffered image specified by the byte source. The TiffImageParser class features support for a number of options that are unique to the TIFF
188      * format. These options can be specified by supplying the appropriate parameters using the keys from the TiffConstants class and the params argument for
189      * this method.
190      * </p>
191      *
192      * <p>
193      * <strong>Loading Partial Images</strong>
194      * </p>
195      *
196      * <p>
197      * The TIFF parser includes support for loading partial images without committing significantly more memory resources than are necessary to store the image.
198      * This feature is useful for conserving memory in applications that require a relatively small sub image from a very large TIFF file. The specifications
199      * for partial images are as follows:
200      * </p>
201      *
202      * <pre>
203      * TiffImagingParameters params = new TiffImagingParameters();
204      * params.setSubImageX(x);
205      * params.setSubImageY(y);
206      * params.setSubImageWidth(width);
207      * params.setSubImageHeight(height);
208      * </pre>
209      *
210      * <p>
211      * Note that the arguments x, y, width, and height must specify a valid rectangular region that is fully contained within the source TIFF image.
212      * </p>
213      *
214      * @param byteSource A valid instance of ByteSource
215      * @param params     Optional instructions for special-handling or interpretation of the input data (null objects are permitted and must be supported by
216      *                   implementations).
217      * @return A valid instance of BufferedImage.
218      * @throws ImagingException In the event that the specified content does not conform to the format of the specific parser implementation.
219      * @throws IOException      In the event of unsuccessful read or access operation.
220      */
221     @Override
222     public BufferedImage getBufferedImage(final ByteSource byteSource, TiffImagingParameters params) throws ImagingException, IOException {
223         if (params == null) {
224             params = new TiffImagingParameters();
225         }
226         final FormatCompliance formatCompliance = FormatCompliance.getDefault();
227         final TiffReader reader = new TiffReader(params.isStrict());
228         final TiffContents contents = reader.readFirstDirectory(byteSource, true, formatCompliance);
229         final ByteOrder byteOrder = reader.getByteOrder();
230         final TiffDirectory directory = contents.directories.get(0);
231         final BufferedImage result = directory.getTiffImage(byteOrder, params);
232         if (null == result) {
233             throw new ImagingException("TIFF does not contain an image.");
234         }
235         return result;
236     }
237 
238     protected BufferedImage getBufferedImage(final TiffDirectory directory, final ByteOrder byteOrder, final TiffImagingParameters params)
239             throws ImagingException, IOException {
240         final short compressionFieldValue;
241         if (directory.findField(TiffTagConstants.TIFF_TAG_COMPRESSION) != null) {
242             compressionFieldValue = directory.getFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION);
243         } else {
244             compressionFieldValue = TiffConstants.COMPRESSION_UNCOMPRESSED_1;
245         }
246         final int compression = 0xffff & compressionFieldValue;
247         final int width = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH);
248         final int height = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH);
249 
250         final Rectangle subImage = checkForSubImage(params);
251         if (subImage != null) {
252             // Check for valid subimage specification. The following checks
253             // are consistent with BufferedImage.getSubimage()
254             if (subImage.width <= 0) {
255                 throw new ImagingException("Negative or zero subimage width.");
256             }
257             if (subImage.height <= 0) {
258                 throw new ImagingException("Negative or zero subimage height.");
259             }
260             if (subImage.x < 0 || subImage.x >= width) {
261                 throw new ImagingException("Subimage x is outside raster.");
262             }
263             if (subImage.x + subImage.width > width) {
264                 throw new ImagingException("Subimage (x+width) is outside raster.");
265             }
266             if (subImage.y < 0 || subImage.y >= height) {
267                 throw new ImagingException("Subimage y is outside raster.");
268             }
269             if (subImage.y + subImage.height > height) {
270                 throw new ImagingException("Subimage (y+height) is outside raster.");
271             }
272         }
273 
274         int samplesPerPixel = 1;
275         final TiffField samplesPerPixelField = directory.findField(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL);
276         if (samplesPerPixelField != null) {
277             samplesPerPixel = samplesPerPixelField.getIntValue();
278         }
279         int[] bitsPerSample = { 1 };
280         int bitsPerPixel = samplesPerPixel;
281         final TiffField bitsPerSampleField = directory.findField(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE);
282         if (bitsPerSampleField != null) {
283             bitsPerSample = bitsPerSampleField.getIntArrayValue();
284             bitsPerPixel = bitsPerSampleField.getIntValueOrArraySum();
285         }
286 
287         // int bitsPerPixel = getTagAsValueOrArraySum(entries,
288         // TIFF_TAG_BITS_PER_SAMPLE);
289 
290         int predictor = -1;
291         {
292             // dumpOptionalNumberTag(entries, TIFF_TAG_FILL_ORDER);
293             // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_BYTE_COUNTS);
294             // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_OFFSETS);
295             // dumpOptionalNumberTag(entries, TIFF_TAG_ORIENTATION);
296             // dumpOptionalNumberTag(entries, TIFF_TAG_PLANAR_CONFIGURATION);
297             final TiffField predictorField = directory.findField(TiffTagConstants.TIFF_TAG_PREDICTOR);
298             if (null != predictorField) {
299                 predictor = predictorField.getIntValueOrArraySum();
300             }
301         }
302 
303         if (samplesPerPixel != bitsPerSample.length) {
304             throw new ImagingException("Tiff: samplesPerPixel (" + samplesPerPixel + ")!=fBitsPerSample.length (" + bitsPerSample.length + ")");
305         }
306 
307         final int photometricInterpretation = 0xffff & directory.getFieldValue(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION);
308 
309         boolean hasAlpha = false;
310         boolean isAlphaPremultiplied = false;
311         if (photometricInterpretation == TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_RGB && samplesPerPixel == 4) {
312             final TiffField extraSamplesField = directory.findField(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES);
313             if (extraSamplesField == null) {
314                 // this state is not defined in the TIFF specification
315                 // and so this code will interpret it as meaning that the
316                 // proper handling would be ARGB.
317                 hasAlpha = true;
318                 isAlphaPremultiplied = false;
319             } else {
320                 final int extraSamplesValue = extraSamplesField.getIntValue();
321                 switch (extraSamplesValue) {
322                 case TiffTagConstants.EXTRA_SAMPLE_UNASSOCIATED_ALPHA:
323                     hasAlpha = true;
324                     isAlphaPremultiplied = false;
325                     break;
326                 case TiffTagConstants.EXTRA_SAMPLE_ASSOCIATED_ALPHA:
327                     hasAlpha = true;
328                     isAlphaPremultiplied = true;
329                     break;
330                 case 0:
331                 default:
332                     hasAlpha = false;
333                     isAlphaPremultiplied = false;
334                     break;
335                 }
336             }
337         }
338 
339         AbstractPhotometricInterpreter photometricInterpreter = params == null ? null : params.getCustomPhotometricInterpreter();
340         if (photometricInterpreter == null) {
341             photometricInterpreter = getPhotometricInterpreter(directory, photometricInterpretation, bitsPerPixel, bitsPerSample, predictor, samplesPerPixel,
342                     width, height);
343         }
344 
345         // Obtain the planar configuration
346         final TiffField pcField = directory.findField(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION);
347         final TiffPlanarConfiguration planarConfiguration = pcField == null ? TiffPlanarConfiguration.CHUNKY
348                 : TiffPlanarConfiguration.lenientValueOf(pcField.getIntValue());
349 
350         if (planarConfiguration == TiffPlanarConfiguration.PLANAR) {
351             // currently, we support the non-interleaved (non-chunky)
352             // option only in the case of a 24-bit RBG photometric interpreter
353             // and for strips (not for tiles).
354             if (photometricInterpretation != TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_RGB || bitsPerPixel != 24) {
355                 throw new ImagingException("For planar configuration 2, only 24 bit RGB is currently supported");
356             }
357             if (null == directory.findField(TiffTagConstants.TIFF_TAG_STRIP_OFFSETS)) {
358                 throw new ImagingException("For planar configuration 2, only strips-organization is supported");
359             }
360         }
361 
362         final AbstractTiffImageData imageData = directory.getTiffImageData();
363 
364         final AbstractImageDataReader dataReader = imageData.getDataReader(directory, photometricInterpreter, bitsPerPixel, bitsPerSample, predictor,
365                 samplesPerPixel, width, height, compression, planarConfiguration, byteOrder);
366         final ImageBuilder iBuilder = dataReader.readImageData(subImage, hasAlpha, isAlphaPremultiplied);
367         return iBuilder.getBufferedImage();
368     }
369 
370     @Override
371     public String getDefaultExtension() {
372         return DEFAULT_EXTENSION;
373     }
374 
375     @Override
376     public TiffImagingParameters getDefaultParameters() {
377         return new TiffImagingParameters();
378     }
379 
380     @Override
381     public FormatCompliance getFormatCompliance(final ByteSource byteSource) throws ImagingException, IOException {
382         final FormatCompliance formatCompliance = FormatCompliance.getDefault();
383         final TiffImagingParameters params = new TiffImagingParameters();
384         new TiffReader(params.isStrict()).readContents(byteSource, params, formatCompliance);
385         return formatCompliance;
386     }
387 
388     @Override
389     public byte[] getIccProfileBytes(final ByteSource byteSource, final TiffImagingParameters params) throws ImagingException, IOException {
390         final FormatCompliance formatCompliance = FormatCompliance.getDefault();
391         final TiffContents contents = new TiffReader(params != null && params.isStrict()).readFirstDirectory(byteSource, false, formatCompliance);
392         final TiffDirectory directory = contents.directories.get(0);
393 
394         return directory.getFieldValue(TiffEpTagConstants.EXIF_TAG_INTER_COLOR_PROFILE, false);
395     }
396 
397     @Override
398     public ImageInfo getImageInfo(final ByteSource byteSource, final TiffImagingParameters params) throws ImagingException, IOException {
399         final FormatCompliance formatCompliance = FormatCompliance.getDefault();
400         final TiffContents contents = new TiffReader(params != null && params.isStrict()).readDirectories(byteSource, false, formatCompliance);
401         final TiffDirectory directory = contents.directories.get(0);
402 
403         final TiffField widthField = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, true);
404         final TiffField heightField = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, true);
405 
406         if (widthField == null || heightField == null) {
407             throw new ImagingException("TIFF image missing size info.");
408         }
409 
410         final int height = heightField.getIntValue();
411         final int width = widthField.getIntValue();
412 
413         final TiffField resolutionUnitField = directory.findField(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT);
414         int resolutionUnit = 2; // Inch
415         if (resolutionUnitField != null && resolutionUnitField.getValue() != null) {
416             resolutionUnit = resolutionUnitField.getIntValue();
417         }
418 
419         double unitsPerInch = -1;
420         switch (resolutionUnit) {
421         case 1:
422             break;
423         case 2: // Inch
424             unitsPerInch = 1.0;
425             break;
426         case 3: // Centimeter
427             unitsPerInch = 2.54;
428             break;
429         default:
430             break;
431 
432         }
433 
434         int physicalWidthDpi = -1;
435         float physicalWidthInch = -1;
436         int physicalHeightDpi = -1;
437         float physicalHeightInch = -1;
438 
439         if (unitsPerInch > 0) {
440             final TiffField xResolutionField = directory.findField(TiffTagConstants.TIFF_TAG_XRESOLUTION);
441             final TiffField yResolutionField = directory.findField(TiffTagConstants.TIFF_TAG_YRESOLUTION);
442 
443             if (xResolutionField != null && xResolutionField.getValue() != null) {
444                 final double xResolutionPixelsPerUnit = xResolutionField.getDoubleValue();
445                 physicalWidthDpi = (int) Math.round(xResolutionPixelsPerUnit * unitsPerInch);
446                 physicalWidthInch = (float) (width / (xResolutionPixelsPerUnit * unitsPerInch));
447             }
448             if (yResolutionField != null && yResolutionField.getValue() != null) {
449                 final double yResolutionPixelsPerUnit = yResolutionField.getDoubleValue();
450                 physicalHeightDpi = (int) Math.round(yResolutionPixelsPerUnit * unitsPerInch);
451                 physicalHeightInch = (float) (height / (yResolutionPixelsPerUnit * unitsPerInch));
452             }
453         }
454 
455         final TiffField bitsPerSampleField = directory.findField(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE);
456 
457         int bitsPerSample = 1;
458         if (bitsPerSampleField != null && bitsPerSampleField.getValue() != null) {
459             bitsPerSample = bitsPerSampleField.getIntValueOrArraySum();
460         }
461 
462         final int bitsPerPixel = bitsPerSample; // assume grayscale;
463         // dunno if this handles colormapped images correctly.
464 
465         final List<String> comments = Allocator.arrayList(directory.size());
466         for (final TiffField field : directory) {
467             comments.add(field.toString());
468         }
469 
470         final ImageFormat format = ImageFormats.TIFF;
471         final String formatName = "TIFF Tag-based Image File Format";
472         final String mimeType = "image/tiff";
473         final int numberOfImages = contents.directories.size();
474         // not accurate ... only reflects first
475         final boolean progressive = false;
476         // is TIFF ever interlaced/progressive?
477 
478         final String formatDetails = "TIFF v." + contents.header.tiffVersion;
479 
480         boolean transparent = false; // TODO: wrong
481         boolean usesPalette = false;
482         final TiffField colorMapField = directory.findField(TiffTagConstants.TIFF_TAG_COLOR_MAP);
483         if (colorMapField != null) {
484             usesPalette = true;
485         }
486 
487         final int photoInterp = 0xffff & directory.getFieldValue(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION);
488         final TiffField extraSamplesField = directory.findField(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES);
489         final int extraSamples;
490         if (extraSamplesField == null) {
491             extraSamples = 0; // no extra samples value
492         } else {
493             extraSamples = extraSamplesField.getIntValue();
494         }
495         final TiffField samplesPerPixelField = directory.findField(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL);
496         final int samplesPerPixel;
497         if (samplesPerPixelField == null) {
498             samplesPerPixel = 1;
499         } else {
500             samplesPerPixel = samplesPerPixelField.getIntValue();
501         }
502 
503         final ImageInfo.ColorType colorType;
504         switch (photoInterp) {
505         case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_BLACK_IS_ZERO:
506         case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_WHITE_IS_ZERO:
507             // the ImageInfo.ColorType enumeration does not distinguish
508             // between monotone white is zero or black is zero
509             colorType = ImageInfo.ColorType.BW;
510             break;
511         case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_RGB:
512             colorType = ImageInfo.ColorType.RGB;
513             // even if 4 samples per pixel are included, TIFF
514             // doesn't specify transparent unless the optional "extra samples"
515             // field is supplied with a non-zero value
516             transparent = samplesPerPixel == 4 && extraSamples != 0;
517             break;
518         case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_RGB_PALETTE:
519             colorType = ImageInfo.ColorType.RGB;
520             usesPalette = true;
521             break;
522         case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_CMYK:
523             colorType = ImageInfo.ColorType.CMYK;
524             break;
525         case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_YCB_CR:
526             colorType = ImageInfo.ColorType.YCbCr;
527             break;
528         default:
529             colorType = ImageInfo.ColorType.UNKNOWN;
530         }
531 
532         final short compressionFieldValue;
533         if (directory.findField(TiffTagConstants.TIFF_TAG_COMPRESSION) != null) {
534             compressionFieldValue = directory.getFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION);
535         } else {
536             compressionFieldValue = TiffConstants.COMPRESSION_UNCOMPRESSED_1;
537         }
538         final int compression = 0xffff & compressionFieldValue;
539         final ImageInfo.CompressionAlgorithm compressionAlgorithm;
540 
541         switch (compression) {
542         case TiffConstants.COMPRESSION_UNCOMPRESSED_1:
543             compressionAlgorithm = ImageInfo.CompressionAlgorithm.NONE;
544             break;
545         case TiffConstants.COMPRESSION_CCITT_1D:
546             compressionAlgorithm = ImageInfo.CompressionAlgorithm.CCITT_1D;
547             break;
548         case TiffConstants.COMPRESSION_CCITT_GROUP_3:
549             compressionAlgorithm = ImageInfo.CompressionAlgorithm.CCITT_GROUP_3;
550             break;
551         case TiffConstants.COMPRESSION_CCITT_GROUP_4:
552             compressionAlgorithm = ImageInfo.CompressionAlgorithm.CCITT_GROUP_4;
553             break;
554         case TiffConstants.COMPRESSION_LZW:
555             compressionAlgorithm = ImageInfo.CompressionAlgorithm.LZW;
556             break;
557         case TiffConstants.COMPRESSION_JPEG_OBSOLETE:
558             compressionAlgorithm = ImageInfo.CompressionAlgorithm.JPEG_TIFF_OBSOLETE;
559             break;
560         case TiffConstants.COMPRESSION_JPEG:
561             compressionAlgorithm = ImageInfo.CompressionAlgorithm.JPEG;
562             break;
563         case TiffConstants.COMPRESSION_UNCOMPRESSED_2:
564             compressionAlgorithm = ImageInfo.CompressionAlgorithm.NONE;
565             break;
566         case TiffConstants.COMPRESSION_PACKBITS:
567             compressionAlgorithm = ImageInfo.CompressionAlgorithm.PACKBITS;
568             break;
569         case TiffConstants.COMPRESSION_DEFLATE_PKZIP:
570         case TiffConstants.COMPRESSION_DEFLATE_ADOBE:
571             compressionAlgorithm = ImageInfo.CompressionAlgorithm.DEFLATE;
572             break;
573         default:
574             compressionAlgorithm = ImageInfo.CompressionAlgorithm.UNKNOWN;
575             break;
576         }
577         return new ImageInfo(formatDetails, bitsPerPixel, comments, format, formatName, height, mimeType, numberOfImages, physicalHeightDpi, physicalHeightInch,
578                 physicalWidthDpi, physicalWidthInch, width, progressive, transparent, usesPalette, colorType, compressionAlgorithm);
579     }
580 
581     @Override
582     public Dimension getImageSize(final ByteSource byteSource, final TiffImagingParameters params) throws ImagingException, IOException {
583         final FormatCompliance formatCompliance = FormatCompliance.getDefault();
584         final TiffContents contents = new TiffReader(params != null && params.isStrict()).readFirstDirectory(byteSource, false, formatCompliance);
585         final TiffDirectory directory = contents.directories.get(0);
586 
587         final TiffField widthField = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, true);
588         final TiffField heightField = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, true);
589 
590         if (widthField == null || heightField == null) {
591             throw new ImagingException("TIFF image missing size info.");
592         }
593 
594         final int height = heightField.getIntValue();
595         final int width = widthField.getIntValue();
596 
597         return new Dimension(width, height);
598     }
599 
600     @Override
601     public ImageMetadata getMetadata(final ByteSource byteSource, TiffImagingParameters params) throws ImagingException, IOException {
602         if (params == null) {
603             params = getDefaultParameters();
604         }
605         final FormatCompliance formatCompliance = FormatCompliance.getDefault();
606         final TiffReader tiffReader = new TiffReader(params.isStrict());
607         final TiffContents contents = tiffReader.readContents(byteSource, params, formatCompliance);
608 
609         final List<TiffDirectory> directories = contents.directories;
610 
611         final TiffImageMetadata result = new TiffImageMetadata(contents);
612 
613         for (final TiffDirectory dir : directories) {
614             final TiffImageMetadata.Directory metadataDirectory = new TiffImageMetadata.Directory(tiffReader.getByteOrder(), dir);
615 
616             final List<TiffField> entries = dir.getDirectoryEntries();
617 
618             entries.forEach(metadataDirectory::add);
619 
620             result.add(metadataDirectory);
621         }
622 
623         return result;
624     }
625 
626     @Override
627     public String getName() {
628         return "Tiff-Custom";
629     }
630 
631     private AbstractPhotometricInterpreter getPhotometricInterpreter(final TiffDirectory directory, final int photometricInterpretation, final int bitsPerPixel,
632             final int[] bitsPerSample, final int predictor, final int samplesPerPixel, final int width, final int height) throws ImagingException {
633         switch (photometricInterpretation) {
634         case 0:
635         case 1:
636             final boolean invert = photometricInterpretation == 0;
637             return new PhotometricInterpreterBiLevel(samplesPerPixel, bitsPerSample, predictor, width, height, invert);
638         case 3: {
639             // Palette
640             final int[] colorMap = directory.findField(TiffTagConstants.TIFF_TAG_COLOR_MAP, true).getIntArrayValue();
641             final int expectedColormapSize = 3 * (1 << bitsPerPixel);
642             if (colorMap.length != expectedColormapSize) {
643                 throw new ImagingException("Tiff: fColorMap.length (" + colorMap.length + ") != expectedColormapSize (" + expectedColormapSize + ")");
644             }
645             return new PhotometricInterpreterPalette(samplesPerPixel, bitsPerSample, predictor, width, height, colorMap);
646         }
647         case 2: // RGB
648             return new PhotometricInterpreterRgb(samplesPerPixel, bitsPerSample, predictor, width, height);
649         case 5: // CMYK
650             return new PhotometricInterpreterCmyk(samplesPerPixel, bitsPerSample, predictor, width, height);
651         case 6: {
652 //            final double[] yCbCrCoefficients = directory.findField(
653 //                    TiffTagConstants.TIFF_TAG_YCBCR_COEFFICIENTS, true)
654 //                    .getDoubleArrayValue();
655 //
656 //            final int[] yCbCrPositioning = directory.findField(
657 //                    TiffTagConstants.TIFF_TAG_YCBCR_POSITIONING, true)
658 //                    .getIntArrayValue();
659 //            final int[] yCbCrSubSampling = directory.findField(
660 //                    TiffTagConstants.TIFF_TAG_YCBCR_SUB_SAMPLING, true)
661 //                    .getIntArrayValue();
662 //
663 //            final double[] referenceBlackWhite = directory.findField(
664 //                    TiffTagConstants.TIFF_TAG_REFERENCE_BLACK_WHITE, true)
665 //                    .getDoubleArrayValue();
666             return new PhotometricInterpreterYCbCr(samplesPerPixel, bitsPerSample, predictor, width, height);
667         }
668         case 8:
669             return new PhotometricInterpreterCieLab(samplesPerPixel, bitsPerSample, predictor, width, height);
670         case 32844:
671         case 32845: {
672 //            final boolean yonly = (photometricInterpretation == 32844);
673             return new PhotometricInterpreterLogLuv(samplesPerPixel, bitsPerSample, predictor, width, height);
674         }
675         default:
676             throw new ImagingException("TIFF: Unknown fPhotometricInterpretation: " + photometricInterpretation);
677         }
678     }
679 
680     /**
681      * Reads the content of a TIFF file that contains numerical data samples rather than image-related pixels.
682      * <p>
683      * If desired, sub-image data can be read from the file by using a Java {@code TiffImagingParameters} instance to specify the subsection of the image that
684      * is required. The following code illustrates the approach:
685      *
686      * <pre>
687      * int x; // coordinate (column) of corner of sub-image
688      * int y; // coordinate (row) of corner of sub-image
689      * int width; // width of sub-image
690      * int height; // height of sub-image
691      *
692      * TiffImagingParameters params = new TiffImagingParameters();
693      * params.setSubImageX(x);
694      * params.setSubImageY(y);
695      * params.setSubImageWidth(width);
696      * params.setSubImageHeight(height);
697      * TiffRasterData raster = readFloatingPointRasterData(directory, byteOrder, params);
698      * </pre>
699      *
700      * @param directory the TIFF directory pointing to the data to be extracted (TIFF files may contain multiple directories)
701      * @param byteOrder the byte order of the data to be extracted
702      * @param params    an optional parameter object instance
703      * @return a valid instance
704      * @throws ImagingException in the event of incompatible or malformed data
705      * @throws IOException      in the event of an I/O error
706      */
707     AbstractTiffRasterData getRasterData(final TiffDirectory directory, final ByteOrder byteOrder, TiffImagingParameters params)
708             throws ImagingException, IOException {
709         if (params == null) {
710             params = getDefaultParameters();
711         }
712         final short[] sSampleFmt = directory.getFieldValue(TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, true);
713         if (sSampleFmt == null || sSampleFmt.length < 1) {
714             throw new ImagingException("Directory does not specify numeric raster data");
715         }
716         int samplesPerPixel = 1;
717         final TiffField samplesPerPixelField = directory.findField(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL);
718         if (samplesPerPixelField != null) {
719             samplesPerPixel = samplesPerPixelField.getIntValue();
720         }
721         int[] bitsPerSample = { 1 };
722         int bitsPerPixel = samplesPerPixel;
723         final TiffField bitsPerSampleField = directory.findField(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE);
724         if (bitsPerSampleField != null) {
725             bitsPerSample = bitsPerSampleField.getIntArrayValue();
726             bitsPerPixel = bitsPerSampleField.getIntValueOrArraySum();
727         }
728         final short compressionFieldValue;
729         if (directory.findField(TiffTagConstants.TIFF_TAG_COMPRESSION) != null) {
730             compressionFieldValue = directory.getFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION);
731         } else {
732             compressionFieldValue = TiffConstants.COMPRESSION_UNCOMPRESSED_1;
733         }
734         final int compression = 0xffff & compressionFieldValue;
735         final int width = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH);
736         final int height = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH);
737         Rectangle subImage = checkForSubImage(params);
738         if (subImage != null) {
739             // Check for valid subimage specification. The following checks
740             // are consistent with BufferedImage.getSubimage()
741             if (subImage.width <= 0) {
742                 throw new ImagingException("Negative or zero subimage width.");
743             }
744             if (subImage.height <= 0) {
745                 throw new ImagingException("Negative or zero subimage height.");
746             }
747             if (subImage.x < 0 || subImage.x >= width) {
748                 throw new ImagingException("Subimage x is outside raster.");
749             }
750             if (subImage.x + subImage.width > width) {
751                 throw new ImagingException("Subimage (x+width) is outside raster.");
752             }
753             if (subImage.y < 0 || subImage.y >= height) {
754                 throw new ImagingException("Subimage y is outside raster.");
755             }
756             if (subImage.y + subImage.height > height) {
757                 throw new ImagingException("Subimage (y+height) is outside raster.");
758             }
759             // if the subimage is just the same thing as the whole
760             // image, suppress the subimage processing
761             if (subImage.x == 0 && subImage.y == 0 && subImage.width == width && subImage.height == height) {
762                 subImage = null;
763             }
764         }
765         // int bitsPerPixel = getTagAsValueOrArraySum(entries,
766         // TIFF_TAG_BITS_PER_SAMPLE);
767         int predictor = -1;
768         {
769             // dumpOptionalNumberTag(entries, TIFF_TAG_FILL_ORDER);
770             // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_BYTE_COUNTS);
771             // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_OFFSETS);
772             // dumpOptionalNumberTag(entries, TIFF_TAG_ORIENTATION);
773             // dumpOptionalNumberTag(entries, TIFF_TAG_PLANAR_CONFIGURATION);
774             final TiffField predictorField = directory.findField(TiffTagConstants.TIFF_TAG_PREDICTOR);
775             if (null != predictorField) {
776                 predictor = predictorField.getIntValueOrArraySum();
777             }
778         }
779         // Obtain the planar configuration
780         final TiffField pcField = directory.findField(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION);
781         final TiffPlanarConfiguration planarConfiguration = pcField == null ? TiffPlanarConfiguration.CHUNKY
782                 : TiffPlanarConfiguration.lenientValueOf(pcField.getIntValue());
783         if (sSampleFmt[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT) {
784             if (bitsPerSample[0] != 32 && bitsPerSample[0] != 64) {
785                 throw new ImagingException("TIFF floating-point data uses unsupported bits-per-sample: " + bitsPerSample[0]);
786             }
787             if (predictor != -1 && predictor != TiffTagConstants.PREDICTOR_VALUE_NONE
788                     && predictor != TiffTagConstants.PREDICTOR_VALUE_FLOATING_POINT_DIFFERENCING) {
789                 throw new ImagingException("TIFF floating-point data uses unsupported horizontal-differencing predictor");
790             }
791         } else if (sSampleFmt[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER) {
792             if (samplesPerPixel != 1) {
793                 throw new ImagingException("TIFF integer data uses unsupported samples per pixel: " + samplesPerPixel);
794             }
795             if (bitsPerPixel != 16 && bitsPerPixel != 32) {
796                 throw new ImagingException("TIFF integer data uses unsupported bits-per-pixel: " + bitsPerPixel);
797             }
798             if (predictor != -1 && predictor != TiffTagConstants.PREDICTOR_VALUE_NONE
799                     && predictor != TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING) {
800                 throw new ImagingException("TIFF integer data uses unsupported horizontal-differencing predictor");
801             }
802         } else {
803             throw new ImagingException("TIFF does not provide a supported raster-data format");
804         }
805         // The photometric interpreter is not used, but the image-based
806         // data reader classes require one. So we create a dummy interpreter.
807         final AbstractPhotometricInterpreter photometricInterpreter = new PhotometricInterpreterBiLevel(samplesPerPixel, bitsPerSample, predictor, width,
808                 height, false);
809         final AbstractTiffImageData imageData = directory.getTiffImageData();
810         final AbstractImageDataReader dataReader = imageData.getDataReader(directory, photometricInterpreter, bitsPerPixel, bitsPerSample, predictor,
811                 samplesPerPixel, width, height, compression, planarConfiguration, byteOrder);
812         return dataReader.readRasterData(subImage);
813     }
814 
815     @Override
816     public String getXmpXml(final ByteSource byteSource, XmpImagingParameters<TiffImagingParameters> params) throws ImagingException, IOException {
817         if (params == null) {
818             params = new XmpImagingParameters<>();
819         }
820         final FormatCompliance formatCompliance = FormatCompliance.getDefault();
821         final TiffContents contents = new TiffReader(params.isStrict()).readDirectories(byteSource, false, formatCompliance);
822         final TiffDirectory directory = contents.directories.get(0);
823 
824         final byte[] bytes = directory.getFieldValue(TiffTagConstants.TIFF_TAG_XMP, false);
825         if (bytes == null) {
826             return null;
827         }
828 
829         // segment data is UTF-8 encoded xml.
830         return new String(bytes, StandardCharsets.UTF_8);
831     }
832 
833     @Override
834     public void writeImage(final BufferedImage src, final OutputStream os, TiffImagingParameters params) throws ImagingException, IOException {
835         if (params == null) {
836             params = new TiffImagingParameters();
837         }
838         new TiffImageWriterLossy().writeImage(src, os, params);
839     }
840 
841 }