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