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}