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