    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.jpeg;
    018
    019import static org.apache.commons.imaging.common.BinaryFunctions.remainingBytes;
    020
    021import java.awt.Dimension;
    022import java.awt.image.BufferedImage;
    023import java.io.IOException;
    024import java.io.PrintWriter;
    025import java.nio.charset.StandardCharsets;
    026import java.text.NumberFormat;
    027import java.util.ArrayList;
    028import java.util.Arrays;
    029import java.util.Collections;
    030import java.util.List;
    031import java.util.logging.Level;
    032import java.util.logging.Logger;
    033
    034import org.apache.commons.imaging.AbstractImageParser;
    035import org.apache.commons.imaging.ImageFormat;
    036import org.apache.commons.imaging.ImageFormats;
    037import org.apache.commons.imaging.ImageInfo;
    038import org.apache.commons.imaging.ImagingException;
    039import org.apache.commons.imaging.bytesource.ByteSource;
    040import org.apache.commons.imaging.common.Allocator;
    041import org.apache.commons.imaging.common.ImageMetadata;
    042import org.apache.commons.imaging.common.XmpEmbeddable;
    043import org.apache.commons.imaging.common.XmpImagingParameters;
    044import org.apache.commons.imaging.formats.jpeg.decoder.JpegDecoder;
    045import org.apache.commons.imaging.formats.jpeg.iptc.IptcParser;
    046import org.apache.commons.imaging.formats.jpeg.iptc.PhotoshopApp13Data;
    047import org.apache.commons.imaging.formats.jpeg.segments.AbstractGenericSegment;
    048import org.apache.commons.imaging.formats.jpeg.segments.AbstractSegment;
    049import org.apache.commons.imaging.formats.jpeg.segments.App13Segment;
    050import org.apache.commons.imaging.formats.jpeg.segments.App14Segment;
    051import org.apache.commons.imaging.formats.jpeg.segments.App2Segment;
    052import org.apache.commons.imaging.formats.jpeg.segments.ComSegment;
    053import org.apache.commons.imaging.formats.jpeg.segments.DqtSegment;
    054import org.apache.commons.imaging.formats.jpeg.segments.JfifSegment;
    055import org.apache.commons.imaging.formats.jpeg.segments.SofnSegment;
    056import org.apache.commons.imaging.formats.jpeg.segments.UnknownSegment;
    057import org.apache.commons.imaging.formats.jpeg.xmp.JpegXmpParser;
    058import org.apache.commons.imaging.formats.tiff.TiffField;
    059import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
    060import org.apache.commons.imaging.formats.tiff.TiffImageParser;
    061import org.apache.commons.imaging.formats.tiff.TiffImagingParameters;
    062import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
    063import org.apache.commons.imaging.internal.Debug;
    064import org.apache.commons.lang3.ArrayUtils;
    065
    066public class JpegImageParser extends AbstractImageParser<JpegImagingParameters> implements XmpEmbeddable<JpegImagingParameters> {
    067
    068    private static final Logger LOGGER = Logger.getLogger(JpegImageParser.class.getName());
    069
    070    private static final String DEFAULT_EXTENSION = ImageFormats.JPEG.getDefaultExtension();
    071    private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.JPEG.getExtensions();
    072
    073    public static boolean isExifApp1Segment(final AbstractGenericSegment segment) {
    074        return JpegConstants.EXIF_IDENTIFIER_CODE.isStartOf(segment.getSegmentData());
    075    }
    076
    077    /**
    078     * Constructs a new instance with the big-endian byte order.
    079     */
    080    public JpegImageParser() {
    081        // empty
    082    }
    083
    084    private byte[] assembleSegments(final List<App2Segment> segments) throws ImagingException {
    085        try {
    086            return assembleSegments(segments, false);
    087        } catch (final ImagingException e) {
    088            return assembleSegments(segments, true);
    089        }
    090    }
    091
    092    private byte[] assembleSegments(final List<App2Segment> segments, final boolean startWithZero) throws ImagingException {
    093        if (segments.isEmpty()) {
    094            throw new ImagingException("No App2 Segments Found.");
    095        }
    096
    097        final int markerCount = segments.get(0).numMarkers;
    098
    099        if (segments.size() != markerCount) {
    100            throw new ImagingException("App2 Segments Missing.  Found: " + segments.size() + ", Expected: " + markerCount + ".");
    101        }
    102
    103        Collections.sort(segments);
    104
    105        final int offset = startWithZero ? 0 : 1;
    106
    107        int total = 0;
    108        for (int i = 0; i < segments.size(); i++) {
    109            final App2Segment segment = segments.get(i);
    110
    111            if (i + offset != segment.curMarker) {
    112                dumpSegments(segments);
    113                throw new ImagingException("Incoherent App2 Segment Ordering.  i: " + i + ", segment[" + i + "].curMarker: " + segment.curMarker + ".");
    114            }
    115
    116            if (markerCount != segment.numMarkers) {
    117                dumpSegments(segments);
    118                throw new ImagingException(
    119                        "Inconsistent App2 Segment Count info.  markerCount: " + markerCount + ", segment[" + i + "].numMarkers: " + segment.numMarkers + ".");
    120            }
    121
    122            if (segment.getIccBytes() != null) {
    123                total += segment.getIccBytes().length;
    124            }
    125        }
    126
    127        final byte[] result = Allocator.byteArray(total);
    128        int progress = 0;
    129
    130        for (final App2Segment segment : segments) {
    131            System.arraycopy(segment.getIccBytes(), 0, result, progress, segment.getIccBytes().length);
    132            progress += segment.getIccBytes().length;
    133        }
    134
    135        return result;
    136    }
    137
    138    @Override
    139    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
    140        pw.println("jpeg.dumpImageFile");
    141
    142        {
    143            final ImageInfo imageInfo = getImageInfo(byteSource);
    144            if (imageInfo == null) {
    145                return false;
    146            }
    147
    148            imageInfo.toString(pw, "");
    149        }
    150
    151        pw.println("");
    152
    153        {
    154            final List<AbstractSegment> abstractSegments = readSegments(byteSource, null, false);
    155
    156            if (abstractSegments == null) {
    157                throw new ImagingException("No Segments Found.");
    158            }
    159
    160            for (int d = 0; d < abstractSegments.size(); d++) {
    161
    162                final AbstractSegment abstractSegment = abstractSegments.get(d);
    163
    164                final NumberFormat nf = NumberFormat.getIntegerInstance();
    165                // this.debugNumber("found, marker: ", marker, 4);
    166                pw.println(d + ": marker: " + Integer.toHexString(abstractSegment.marker) + ", " + abstractSegment.getDescription() + " (length: "
    167                        + nf.format(abstractSegment.length) + ")");
    168                abstractSegment.dump(pw);
    169            }
    170
    171            pw.println("");
    172        }
    173
    174        return true;
    175    }
    176
    177    private void dumpSegments(final List<? extends AbstractSegment> v) {
    178        Debug.debug();
    179        Debug.debug("dumpSegments: " + v.size());
    180
    181        for (int i = 0; i < v.size(); i++) {
    182            final App2Segment segment = (App2Segment) v.get(i);
    183
    184            Debug.debug(i + ": " + segment.curMarker + " / " + segment.numMarkers);
    185        }
    186        Debug.debug();
    187    }
    188
    189    private List<AbstractSegment> filterApp1Segments(final List<AbstractSegment> abstractSegments) {
    190        final List<AbstractSegment> result = new ArrayList<>();
    191
    192        for (final AbstractSegment s : abstractSegments) {
    193            final AbstractGenericSegment segment = (AbstractGenericSegment) s;
    194            if (isExifApp1Segment(segment)) {
    195                result.add(segment);
    196            }
    197        }
    198
    199        return result;
    200    }
    201
    202    @Override
    203    protected String[] getAcceptedExtensions() {
    204        return ACCEPTED_EXTENSIONS;
    205    }
    206
    207    @Override
    208    protected ImageFormat[] getAcceptedTypes() {
    209        return new ImageFormat[] { ImageFormats.JPEG, //
    210        };
    211    }
    212
    213    @Override
    214    public final BufferedImage getBufferedImage(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException {
    215        final JpegDecoder jpegDecoder = new JpegDecoder();
    216        return jpegDecoder.decode(byteSource);
    217    }
    218
    219    @Override
    220    public String getDefaultExtension() {
    221        return DEFAULT_EXTENSION;
    222    }
    223
    224    @Override
    225    public JpegImagingParameters getDefaultParameters() {
    226        return new JpegImagingParameters();
    227    }
    228
    229    public TiffImageMetadata getExifMetadata(final ByteSource byteSource, TiffImagingParameters params) throws ImagingException, IOException {
    230        final byte[] bytes = getExifRawData(byteSource);
    231        if (null == bytes) {
    232            return null;
    233        }
    234
    235        if (params == null) {
    236            params = new TiffImagingParameters();
    237        }
    238        params.setReadThumbnails(Boolean.TRUE);
    239
    240        return (TiffImageMetadata) new TiffImageParser().getMetadata(bytes, params);
    241    }
    242
    243    public byte[] getExifRawData(final ByteSource byteSource) throws ImagingException, IOException {
    244        final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP1_MARKER, }, false);
    245
    246        if (abstractSegments == null || abstractSegments.isEmpty()) {
    247            return null;
    248        }
    249
    250        final List<AbstractSegment> exifSegments = filterApp1Segments(abstractSegments);
    251        if (LOGGER.isLoggable(Level.FINEST)) {
    252            LOGGER.finest("exifSegments.size()" + ": " + exifSegments.size());
    253        }
    254
    255        // Debug.debug("segments", segments);
    256        // Debug.debug("exifSegments", exifSegments);
    257
    258        // TODO: concatenate if multiple segments, need example.
    259        if (exifSegments.isEmpty()) {
    260            return null;
    261        }
    262        if (exifSegments.size() > 1) {
    263            throw new ImagingException(
    264                    "Imaging currently can't parse EXIF metadata split across multiple APP1 segments.  " + "Please send this image to the Imaging project.");
    265        }
    266
    267        final AbstractGenericSegment segment = (AbstractGenericSegment) exifSegments.get(0);
    268        final byte[] bytes = segment.getSegmentData();
    269
    270        // byte[] head = readBytearray("exif head", bytes, 0, 6);
    271        //
    272        // Debug.debug("head", head);
    273
    274        return remainingBytes("trimmed exif bytes", bytes, 6);
    275    }
    276
    277    @Override
    278    public byte[] getIccProfileBytes(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException {
    279        final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP2_MARKER, }, false);
    280
    281        final List<App2Segment> filtered = new ArrayList<>();
    282        if (abstractSegments != null) {
    283            // throw away non-icc profile app2 segments.
    284            for (final AbstractSegment s : abstractSegments) {
    285                final App2Segment segment = (App2Segment) s;
    286                if (segment.getIccBytes() != null) {
    287                    filtered.add(segment);
    288                }
    289            }
    290        }
    291
    292        if (filtered.isEmpty()) {
    293            return null;
    294        }
    295
    296        final byte[] bytes = assembleSegments(filtered);
    297
    298        if (LOGGER.isLoggable(Level.FINEST)) {
    299            LOGGER.finest("bytes" + ": " + bytes.length);
    300        }
    301
    302        return bytes;
    303    }
    304
    305    @Override
    306    public ImageInfo getImageInfo(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException {
    307        // List allSegments = readSegments(byteSource, null, false);
    308
    309        final List<AbstractSegment> SOF_segments = readSegments(byteSource, new int[] {
    310                // kJFIFMarker,
    311
    312                JpegConstants.SOF0_MARKER, JpegConstants.SOF1_MARKER, JpegConstants.SOF2_MARKER, JpegConstants.SOF3_MARKER, JpegConstants.SOF5_MARKER,
    313                JpegConstants.SOF6_MARKER, JpegConstants.SOF7_MARKER, JpegConstants.SOF9_MARKER, JpegConstants.SOF10_MARKER, JpegConstants.SOF11_MARKER,
    314                JpegConstants.SOF13_MARKER, JpegConstants.SOF14_MARKER, JpegConstants.SOF15_MARKER,
    315
    316        }, false);
    317
    318        if (SOF_segments == null) {
    319            throw new ImagingException("No SOFN Data Found.");
    320        }
    321
    322        // if (SOF_segments.size() != 1)
    323        // System.out.println("Incoherent SOFN Data Found: "
    324        // + SOF_segments.size());
    325
    326        final List<AbstractSegment> jfifSegments = readSegments(byteSource, new int[] { JpegConstants.JFIF_MARKER, }, true);
    327
    328        final SofnSegment fSOFNSegment = (SofnSegment) SOF_segments.get(0);
    329        // SofnSegment fSOFNSegment = (SofnSegment) findSegment(segments,
    330        // SOFNmarkers);
    331
    332        if (fSOFNSegment == null) {
    333            throw new ImagingException("No SOFN Data Found.");
    334        }
    335
    336        final int width = fSOFNSegment.width;
    337        final int height = fSOFNSegment.height;
    338
    339        JfifSegment jfifSegment = null;
    340
    341        if (jfifSegments != null && !jfifSegments.isEmpty()) {
    342            jfifSegment = (JfifSegment) jfifSegments.get(0);
    343        }
    344
    345        final List<AbstractSegment> app14Segments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP14_MARKER }, true);
    346        App14Segment app14Segment = null;
    347        if (app14Segments != null && !app14Segments.isEmpty()) {
    348            app14Segment = (App14Segment) app14Segments.get(0);
    349        }
    350
    351        // JfifSegment fTheJFIFSegment = (JfifSegment) findSegment(segments,
    352        // kJFIFMarker);
    353
    354        double xDensity = -1.0;
    355        double yDensity = -1.0;
    356        double unitsPerInch = -1.0;
    357        // int JFIF_major_version;
    358        // int JFIF_minor_version;
    359        final String formatDetails;
    360
    361        if (jfifSegment != null) {
    362            xDensity = jfifSegment.xDensity;
    363            yDensity = jfifSegment.yDensity;
    364            final int densityUnits = jfifSegment.densityUnits;
    365            // JFIF_major_version = fTheJFIFSegment.JFIF_major_version;
    366            // JFIF_minor_version = fTheJFIFSegment.JFIF_minor_version;
    367
    368            formatDetails = "Jpeg/JFIF v." + jfifSegment.jfifMajorVersion + "." + jfifSegment.jfifMinorVersion;
    369
    370            switch (densityUnits) {
    371            case 0:
    372                break;
    373            case 1: // inches
    374                unitsPerInch = 1.0;
    375                break;
    376            case 2: // cms
    377                unitsPerInch = 2.54;
    378                break;
    379            default:
    380                break;
    381            }
    382        } else {
    383            final JpegImageMetadata metadata = (JpegImageMetadata) getMetadata(byteSource, params);
    384
    385            if (metadata != null) {
    386                {
    387                    final TiffField field = metadata.findExifValue(TiffTagConstants.TIFF_TAG_XRESOLUTION);
    388                    if (field != null) {
    389                        xDensity = ((Number) field.getValue()).doubleValue();
    390                    }
    391                }
    392                {
    393                    final TiffField field = metadata.findExifValue(TiffTagConstants.TIFF_TAG_YRESOLUTION);
    394                    if (field != null) {
    395                        yDensity = ((Number) field.getValue()).doubleValue();
    396                    }
    397                }
    398                {
    399                    final TiffField field = metadata.findExifValue(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT);
    400                    if (field != null) {
    401                        final int densityUnits = ((Number) field.getValue()).intValue();
    402
    403                        switch (densityUnits) {
    404                        case 1:
    405                            break;
    406                        case 2: // inches
    407                            unitsPerInch = 1.0;
    408                            break;
    409                        case 3: // cms
    410                            unitsPerInch = 2.54;
    411                            break;
    412                        default:
    413                            break;
    414                        }
    415                    }
    416
    417                }
    418            }
    419
    420            formatDetails = "Jpeg/DCM";
    421
    422        }
    423
    424        int physicalHeightDpi = -1;
    425        float physicalHeightInch = -1;
    426        int physicalWidthDpi = -1;
    427        float physicalWidthInch = -1;
    428
    429        if (unitsPerInch > 0) {
    430            physicalWidthDpi = (int) Math.round(xDensity * unitsPerInch);
    431            physicalWidthInch = (float) (width / (xDensity * unitsPerInch));
    432            physicalHeightDpi = (int) Math.round(yDensity * unitsPerInch);
    433            physicalHeightInch = (float) (height / (yDensity * unitsPerInch));
    434        }
    435
    436        final List<AbstractSegment> commentSegments = readSegments(byteSource, new int[] { JpegConstants.COM_MARKER }, false);
    437        final List<String> comments = Allocator.arrayList(commentSegments.size());
    438        for (final AbstractSegment commentSegment : commentSegments) {
    439            final ComSegment comSegment = (ComSegment) commentSegment;
    440            comments.add(new String(comSegment.getComment(), StandardCharsets.UTF_8));
    441        }
    442
    443        final int numberOfComponents = fSOFNSegment.numberOfComponents;
    444        final int precision = fSOFNSegment.precision;
    445
    446        final int bitsPerPixel = numberOfComponents * precision;
    447        final ImageFormat format = ImageFormats.JPEG;
    448        final String formatName = "JPEG (Joint Photographic Experts Group) Format";
    449        final String mimeType = "image/jpeg";
    450        // TODO: we ought to count images, but don't yet.
    451        final int numberOfImages = 1;
    452        // not accurate ... only reflects first
    453        final boolean progressive = fSOFNSegment.marker == JpegConstants.SOF2_MARKER;
    454
    455        boolean transparent = false;
    456        final boolean usesPalette = false; // TODO: inaccurate.
    457
    458        // See https://docs.oracle.com/javase/8/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#color
    459        ImageInfo.ColorType colorType = ImageInfo.ColorType.UNKNOWN;
    460        // Some images have both JFIF/APP0 and APP14.
    461        // JFIF is meant to win but in them APP14 is clearly right, so make it win.
    462        if (app14Segment != null && app14Segment.isAdobeJpegSegment()) {
    463            final int colorTransform = app14Segment.getAdobeColorTransform();
    464            switch (colorTransform) {
    465            case App14Segment.ADOBE_COLOR_TRANSFORM_UNKNOWN:
    466                if (numberOfComponents == 3) {
    467                    colorType = ImageInfo.ColorType.RGB;
    468                } else if (numberOfComponents == 4) {
    469                    colorType = ImageInfo.ColorType.CMYK;
    470                }
    471                break;
    472            case App14Segment.ADOBE_COLOR_TRANSFORM_YCbCr:
    473                colorType = ImageInfo.ColorType.YCbCr;
    474                break;
    475            case App14Segment.ADOBE_COLOR_TRANSFORM_YCCK:
    476                colorType = ImageInfo.ColorType.YCCK;
    477                break;
    478            default:
    479                break;
    480            }
    481        } else if (jfifSegment != null) {
    482            if (numberOfComponents == 1) {
    483                colorType = ImageInfo.ColorType.GRAYSCALE;
    484            } else if (numberOfComponents == 3) {
    485                colorType = ImageInfo.ColorType.YCbCr;
    486            }
    487        } else {
    488            switch (numberOfComponents) {
    489            case 1:
    490                colorType = ImageInfo.ColorType.GRAYSCALE;
    491                break;
    492            case 2:
    493                colorType = ImageInfo.ColorType.GRAYSCALE;
    494                transparent = true;
    495                break;
    496            case 3:
    497            case 4:
    498                boolean have1 = false;
    499                boolean have2 = false;
    500                boolean have3 = false;
    501                boolean have4 = false;
    502                boolean haveOther = false;
    503                for (final SofnSegment.Component component : fSOFNSegment.getComponents()) {
    504                    final int id = component.componentIdentifier;
    505                    switch (id) {
    506                    case 1:
    507                        have1 = true;
    508                        break;
    509                    case 2:
    510                        have2 = true;
    511                        break;
    512                    case 3:
    513                        have3 = true;
    514                        break;
    515                    case 4:
    516                        have4 = true;
    517                        break;
    518                    default:
    519                        haveOther = true;
    520                        break;
    521                    }
    522                }
    523                if (numberOfComponents == 3 && have1 && have2 && have3 && !have4 && !haveOther) {
    524                    colorType = ImageInfo.ColorType.YCbCr;
    525                } else if (numberOfComponents == 4 && have1 && have2 && have3 && have4 && !haveOther) {
    526                    colorType = ImageInfo.ColorType.YCbCr;
    527                    transparent = true;
    528                } else {
    529                    boolean haveR = false;
    530                    boolean haveG = false;
    531                    boolean haveB = false;
    532                    boolean haveA = false;
    533                    boolean haveC = false;
    534                    boolean havec = false;
    535                    boolean haveY = false;
    536                    for (final SofnSegment.Component component : fSOFNSegment.getComponents()) {
    537                        final int id = component.componentIdentifier;
    538                        switch (id) {
    539                        case 'R':
    540                            haveR = true;
    541                            break;
    542                        case 'G':
    543                            haveG = true;
    544                            break;
    545                        case 'B':
    546                            haveB = true;
    547                            break;
    548                        case 'A':
    549                            haveA = true;
    550                            break;
    551                        case 'C':
    552                            haveC = true;
    553                            break;
    554                        case 'c':
    555                            havec = true;
    556                            break;
    557                        case 'Y':
    558                            haveY = true;
    559                            break;
    560                        default:
    561                            break;
    562                        }
    563                    }
    564                    if (haveR && haveG && haveB && !haveA && !haveC && !havec && !haveY) {
    565                        colorType = ImageInfo.ColorType.RGB;
    566                    } else if (haveR && haveG && haveB && haveA && !haveC && !havec && !haveY) {
    567                        colorType = ImageInfo.ColorType.RGB;
    568                        transparent = true;
    569                    } else if (haveY && haveC && havec && !haveR && !haveG && !haveB && !haveA) {
    570                        colorType = ImageInfo.ColorType.YCC;
    571                    } else if (haveY && haveC && havec && haveA && !haveR && !haveG && !haveB) {
    572                        colorType = ImageInfo.ColorType.YCC;
    573                        transparent = true;
    574                    } else {
    575                        int minHorizontalSamplingFactor = Integer.MAX_VALUE;
    576                        int maxHorizontalSmaplingFactor = Integer.MIN_VALUE;
    577                        int minVerticalSamplingFactor = Integer.MAX_VALUE;
    578                        int maxVerticalSamplingFactor = Integer.MIN_VALUE;
    579                        for (final SofnSegment.Component component : fSOFNSegment.getComponents()) {
    580                            if (minHorizontalSamplingFactor > component.horizontalSamplingFactor) {
    581                                minHorizontalSamplingFactor = component.horizontalSamplingFactor;
    582                            }
    583                            if (maxHorizontalSmaplingFactor < component.horizontalSamplingFactor) {
    584                                maxHorizontalSmaplingFactor = component.horizontalSamplingFactor;
    585                            }
    586                            if (minVerticalSamplingFactor > component.verticalSamplingFactor) {
    587                                minVerticalSamplingFactor = component.verticalSamplingFactor;
    588                            }
    589                            if (maxVerticalSamplingFactor < component.verticalSamplingFactor) {
    590                                maxVerticalSamplingFactor = component.verticalSamplingFactor;
    591                            }
    592                        }
    593                        final boolean isSubsampled = minHorizontalSamplingFactor != maxHorizontalSmaplingFactor
    594                                || minVerticalSamplingFactor != maxVerticalSamplingFactor;
    595                        if (numberOfComponents == 3) {
    596                            if (isSubsampled) {
    597                                colorType = ImageInfo.ColorType.YCbCr;
    598                            } else {
    599                                colorType = ImageInfo.ColorType.RGB;
    600                            }
    601                        } else if (numberOfComponents == 4) {
    602                            if (isSubsampled) {
    603                                colorType = ImageInfo.ColorType.YCCK;
    604                            } else {
    605                                colorType = ImageInfo.ColorType.CMYK;
    606                            }
    607                        }
    608                    }
    609                }
    610                break;
    611            default:
    612                break;
    613            }
    614        }
    615
    616        final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.JPEG;
    617
    618        return new ImageInfo(formatDetails, bitsPerPixel, comments, format, formatName, height, mimeType, numberOfImages, physicalHeightDpi, physicalHeightInch,
    619                physicalWidthDpi, physicalWidthInch, width, progressive, transparent, usesPalette, colorType, compressionAlgorithm);
    620    }
    621
    622    @Override
    623    public Dimension getImageSize(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException {
    624        final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] {
    625                // kJFIFMarker,
    626                JpegConstants.SOF0_MARKER, JpegConstants.SOF1_MARKER, JpegConstants.SOF2_MARKER, JpegConstants.SOF3_MARKER, JpegConstants.SOF5_MARKER,
    627                JpegConstants.SOF6_MARKER, JpegConstants.SOF7_MARKER, JpegConstants.SOF9_MARKER, JpegConstants.SOF10_MARKER, JpegConstants.SOF11_MARKER,
    628                JpegConstants.SOF13_MARKER, JpegConstants.SOF14_MARKER, JpegConstants.SOF15_MARKER,
    629
    630        }, true);
    631
    632        if (abstractSegments == null || abstractSegments.isEmpty()) {
    633            throw new ImagingException("No JFIF Data Found.");
    634        }
    635
    636        if (abstractSegments.size() > 1) {
    637            throw new ImagingException("Redundant JFIF Data Found.");
    638        }
    639
    640        final SofnSegment fSOFNSegment = (SofnSegment) abstractSegments.get(0);
    641
    642        return new Dimension(fSOFNSegment.width, fSOFNSegment.height);
    643    }
    644
    645    @Override
    646    public ImageMetadata getMetadata(final ByteSource byteSource, JpegImagingParameters params) throws ImagingException, IOException {
    647        if (params == null) {
    648            params = new JpegImagingParameters();
    649        }
    650        final TiffImageMetadata exif = getExifMetadata(byteSource, new TiffImagingParameters());
    651
    652        final JpegPhotoshopMetadata photoshop = getPhotoshopMetadata(byteSource, params);
    653
    654        if (null == exif && null == photoshop) {
    655            return null;
    656        }
    657
    658        return new JpegImageMetadata(photoshop, exif);
    659    }
    660
    661    @Override
    662    public String getName() {
    663        return "Jpeg-Custom";
    664    }
    665
    666    public JpegPhotoshopMetadata getPhotoshopMetadata(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException {
    667        final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP13_MARKER, }, false);
    668
    669        if (abstractSegments == null || abstractSegments.isEmpty()) {
    670            return null;
    671        }
    672
    673        PhotoshopApp13Data photoshopApp13Data = null;
    674
    675        for (final AbstractSegment s : abstractSegments) {
    676            final App13Segment segment = (App13Segment) s;
    677
    678            final PhotoshopApp13Data data = segment.parsePhotoshopSegment(params);
    679            if (data != null) {
    680                if (photoshopApp13Data != null) {
    681                    throw new ImagingException("JPEG contains more than one Photoshop App13 segment.");
    682                }
    683                photoshopApp13Data = data;
    684            }
    685        }
    686
    687        if (null == photoshopApp13Data) {
    688            return null;
    689        }
    690        return new JpegPhotoshopMetadata(photoshopApp13Data);
    691    }
    692
    693    /**
    694     * Extracts embedded XML metadata as XML string.
    695     * <p>
    696     *
    697     * @param byteSource File containing image data.
    698     * @param params     Map of optional parameters, defined in ImagingConstants.
    699     * @return Xmp Xml as String, if present. Otherwise, returns null.
    700     */
    701    @Override
    702    public String getXmpXml(final ByteSource byteSource, final XmpImagingParameters<JpegImagingParameters> params) throws ImagingException, IOException {
    703
    704        final List<String> result = new ArrayList<>();
    705
    706        final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
    707            // return false to exit before reading image data.
    708            @Override
    709            public boolean beginSos() {
    710                return false;
    711            }
    712
    713            // return false to exit traversal.
    714            @Override
    715            public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
    716                    final byte[] segmentData) throws ImagingException {
    717                if (marker == 0xffd9) {
    718                    return false;
    719                }
    720
    721                if (marker == JpegConstants.JPEG_APP1_MARKER && new JpegXmpParser().isXmpJpegSegment(segmentData)) {
    722                    result.add(new JpegXmpParser().parseXmpJpegSegment(segmentData));
    723                    return false;
    724                }
    725
    726                return true;
    727            }
    728
    729            @Override
    730            public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
    731                // don't need image data
    732            }
    733        };
    734        new JpegUtils().traverseJfif(byteSource, visitor);
    735
    736        if (result.isEmpty()) {
    737            return null;
    738        }
    739        if (result.size() > 1) {
    740            throw new ImagingException("JPEG file contains more than one XMP segment.");
    741        }
    742        return result.get(0);
    743    }
    744
    745    public boolean hasExifSegment(final ByteSource byteSource) throws ImagingException, IOException {
    746        final boolean[] result = { false, };
    747
    748        final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
    749            // return false to exit before reading image data.
    750            @Override
    751            public boolean beginSos() {
    752                return false;
    753            }
    754
    755            // return false to exit traversal.
    756            @Override
    757            public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
    758                    final byte[] segmentData) {
    759                if (marker == 0xffd9) {
    760                    return false;
    761                }
    762
    763                if (marker == JpegConstants.JPEG_APP1_MARKER && JpegConstants.EXIF_IDENTIFIER_CODE.isStartOf(segmentData)) {
    764                    result[0] = true;
    765                    return false;
    766                }
    767
    768                return true;
    769            }
    770
    771            @Override
    772            public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
    773                // don't need image data
    774            }
    775        };
    776
    777        new JpegUtils().traverseJfif(byteSource, visitor);
    778
    779        return result[0];
    780    }
    781
    782    public boolean hasIptcSegment(final ByteSource byteSource) throws ImagingException, IOException {
    783        final boolean[] result = { false, };
    784
    785        final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
    786            // return false to exit before reading image data.
    787            @Override
    788            public boolean beginSos() {
    789                return false;
    790            }
    791
    792            // return false to exit traversal.
    793            @Override
    794            public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
    795                    final byte[] segmentData) {
    796                if (marker == 0xffd9) {
    797                    return false;
    798                }
    799
    800                if (marker == JpegConstants.JPEG_APP13_MARKER && new IptcParser().isPhotoshopJpegSegment(segmentData)) {
    801                    result[0] = true;
    802                    return false;
    803                }
    804
    805                return true;
    806            }
    807
    808            @Override
    809            public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
    810                // don't need image data
    811            }
    812        };
    813
    814        new JpegUtils().traverseJfif(byteSource, visitor);
    815
    816        return result[0];
    817    }
    818
    819    public boolean hasXmpSegment(final ByteSource byteSource) throws ImagingException, IOException {
    820        final boolean[] result = { false, };
    821
    822        final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
    823            // return false to exit before reading image data.
    824            @Override
    825            public boolean beginSos() {
    826                return false;
    827            }
    828
    829            // return false to exit traversal.
    830            @Override
    831            public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
    832                    final byte[] segmentData) {
    833                if (marker == 0xffd9) {
    834                    return false;
    835                }
    836
    837                if (marker == JpegConstants.JPEG_APP1_MARKER && new JpegXmpParser().isXmpJpegSegment(segmentData)) {
    838                    result[0] = true;
    839                    return false;
    840                }
    841
    842                return true;
    843            }
    844
    845            @Override
    846            public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
    847                // don't need image data
    848            }
    849        };
    850        new JpegUtils().traverseJfif(byteSource, visitor);
    851
    852        return result[0];
    853    }
    854
    855    private boolean keepMarker(final int marker, final int[] markers) {
    856        return ArrayUtils.contains(markers, marker);
    857    }
    858
    859    public List<AbstractSegment> readSegments(final ByteSource byteSource, final int[] markers, final boolean returnAfterFirst)
    860            throws ImagingException, IOException {
    861        final List<AbstractSegment> result = new ArrayList<>();
    862        final int[] sofnSegments = {
    863                // kJFIFMarker,
    864                JpegConstants.SOF0_MARKER, JpegConstants.SOF1_MARKER, JpegConstants.SOF2_MARKER, JpegConstants.SOF3_MARKER, JpegConstants.SOF5_MARKER,
    865                JpegConstants.SOF6_MARKER, JpegConstants.SOF7_MARKER, JpegConstants.SOF9_MARKER, JpegConstants.SOF10_MARKER, JpegConstants.SOF11_MARKER,
    866                JpegConstants.SOF13_MARKER, JpegConstants.SOF14_MARKER, JpegConstants.SOF15_MARKER, };
    867
    868        final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
    869            // return false to exit before reading image data.
    870            @Override
    871            public boolean beginSos() {
    872                return false;
    873            }
    874
    875            // return false to exit traversal.
    876            @Override
    877            public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
    878                    final byte[] segmentData) throws ImagingException, IOException {
    879                if (marker == JpegConstants.EOI_MARKER) {
    880                    return false;
    881                }
    882
    883                // Debug.debug("visitSegment marker", marker);
    884                // // Debug.debug("visitSegment keepMarker(marker, markers)",
    885                // keepMarker(marker, markers));
    886                // Debug.debug("visitSegment keepMarker(marker, markers)",
    887                // keepMarker(marker, markers));
    888
    889                if (!keepMarker(marker, markers)) {
    890                    return true;
    891                }
    892
    893                switch (marker) {
    894                case JpegConstants.JPEG_APP13_MARKER:
    895                    // Debug.debug("app 13 segment data", segmentData.length);
    896                    result.add(new App13Segment(marker, segmentData));
    897                    break;
    898                case JpegConstants.JPEG_APP14_MARKER:
    899                    result.add(new App14Segment(marker, segmentData));
    900                    break;
    901                case JpegConstants.JPEG_APP2_MARKER:
    902                    result.add(new App2Segment(marker, segmentData));
    903                    break;
    904                case JpegConstants.JFIF_MARKER:
    905                    result.add(new JfifSegment(marker, segmentData));
    906                    break;
    907                default:
    908                    if (Arrays.binarySearch(sofnSegments, marker) >= 0) {
    909                        result.add(new SofnSegment(marker, segmentData));
    910                    } else if (marker == JpegConstants.DQT_MARKER) {
    911                        result.add(new DqtSegment(marker, segmentData));
    912                    } else if (marker >= JpegConstants.JPEG_APP1_MARKER && marker <= JpegConstants.JPEG_APP15_MARKER) {
    913                        result.add(new UnknownSegment(marker, segmentData));
    914                    } else if (marker == JpegConstants.COM_MARKER) {
    915                        result.add(new ComSegment(marker, segmentData));
    916                    }
    917                    break;
    918                }
    919
    920                return !returnAfterFirst;
    921            }
    922
    923            @Override
    924            public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
    925                // don't need image data
    926            }
    927        };
    928
    929        new JpegUtils().traverseJfif(byteSource, visitor);
    930
    931        return result;
    932    }
    933}