    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 java.awt.Dimension;
    020import java.awt.Rectangle;
    021import java.awt.image.BufferedImage;
    022import java.io.IOException;
    023import java.io.OutputStream;
    024import java.io.PrintWriter;
    025import java.nio.ByteOrder;
    026import java.nio.charset.StandardCharsets;
    027import java.util.ArrayList;
    028import java.util.List;
    029
    030import org.apache.commons.imaging.AbstractImageParser;
    031import org.apache.commons.imaging.FormatCompliance;
    032import org.apache.commons.imaging.ImageFormat;
    033import org.apache.commons.imaging.ImageFormats;
    034import org.apache.commons.imaging.ImageInfo;
    035import org.apache.commons.imaging.ImagingException;
    036import org.apache.commons.imaging.bytesource.ByteSource;
    037import org.apache.commons.imaging.common.Allocator;
    038import org.apache.commons.imaging.common.ImageBuilder;
    039import org.apache.commons.imaging.common.ImageMetadata;
    040import org.apache.commons.imaging.common.XmpEmbeddable;
    041import org.apache.commons.imaging.common.XmpImagingParameters;
    042import org.apache.commons.imaging.formats.tiff.TiffDirectory.ImageDataElement;
    043import org.apache.commons.imaging.formats.tiff.constants.TiffConstants;
    044import org.apache.commons.imaging.formats.tiff.constants.TiffEpTagConstants;
    045import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration;
    046import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
    047import org.apache.commons.imaging.formats.tiff.datareaders.AbstractImageDataReader;
    048import org.apache.commons.imaging.formats.tiff.photometricinterpreters.AbstractPhotometricInterpreter;
    049import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterBiLevel;
    050import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterCieLab;
    051import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterCmyk;
    052import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterLogLuv;
    053import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterPalette;
    054import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterRgb;
    055import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterYCbCr;
    056import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
    057
    058/**
    059 * Implements methods for reading and writing TIFF files. Instances of this class are invoked from the general Imaging class. Applications that require the use
    060 * of TIFF-specific features may instantiate and access this class directly.
    061 */
    062public class TiffImageParser extends AbstractImageParser<TiffImagingParameters> implements XmpEmbeddable<TiffImagingParameters> {
    063
    064    private static final String DEFAULT_EXTENSION = ImageFormats.TIFF.getDefaultExtension();
    065    private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.TIFF.getExtensions();
    066
    067    /**
    068     * Constructs a new instance with the big-endian byte order.
    069     */
    070    public TiffImageParser() {
    071        // empty
    072    }
    073
    074    private Rectangle checkForSubImage(final TiffImagingParameters params) {
    075        // the params class enforces a correct specification for the
    076        // sub-image, but does not have knowledge of the actual
    077        // dimensions of the image that is being read. This method
    078        // returns the sub-image specification, if any, and leaves
    079        // further tests to the calling module.
    080        if (params != null && params.isSubImageSet()) {
    081            final int ix0 = params.getSubImageX();
    082            final int iy0 = params.getSubImageY();
    083            final int iwidth = params.getSubImageWidth();
    084            final int iheight = params.getSubImageHeight();
    085            return new Rectangle(ix0, iy0, iwidth, iheight);
    086        }
    087        return null;
    088    }
    089
    090    public List<byte[]> collectRawImageData(final ByteSource byteSource, final TiffImagingParameters params) throws ImagingException, IOException {
    091        final FormatCompliance formatCompliance = FormatCompliance.getDefault();
    092        final TiffContents contents = new TiffReader(params != null && params.isStrict()).readDirectories(byteSource, true, formatCompliance);
    093
    094        final List<byte[]> result = new ArrayList<>();
    095        for (int i = 0; i < contents.directories.size(); i++) {
    096            final TiffDirectory directory = contents.directories.get(i);
    097            final List<ImageDataElement> dataElements = directory.getTiffRawImageDataElements();
    098            for (final ImageDataElement element : dataElements) {
    099                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}