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}