View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.jpeg;
18  
19  import static org.apache.commons.imaging.common.BinaryFunctions.remainingBytes;
20  import static org.apache.commons.imaging.common.BinaryFunctions.startsWith;
21  
22  import java.awt.Dimension;
23  import java.awt.image.BufferedImage;
24  import java.io.IOException;
25  import java.io.PrintWriter;
26  import java.nio.charset.StandardCharsets;
27  import java.text.NumberFormat;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.List;
31  import java.util.logging.Level;
32  import java.util.logging.Logger;
33  
34  import org.apache.commons.imaging.AbstractImageParser;
35  import org.apache.commons.imaging.ImageFormat;
36  import org.apache.commons.imaging.ImageFormats;
37  import org.apache.commons.imaging.ImageInfo;
38  import org.apache.commons.imaging.ImagingException;
39  import org.apache.commons.imaging.bytesource.ByteSource;
40  import org.apache.commons.imaging.common.Allocator;
41  import org.apache.commons.imaging.common.ImageMetadata;
42  import org.apache.commons.imaging.common.XmpEmbeddable;
43  import org.apache.commons.imaging.common.XmpImagingParameters;
44  import org.apache.commons.imaging.formats.jpeg.decoder.JpegDecoder;
45  import org.apache.commons.imaging.formats.jpeg.iptc.IptcParser;
46  import org.apache.commons.imaging.formats.jpeg.iptc.PhotoshopApp13Data;
47  import org.apache.commons.imaging.formats.jpeg.segments.AbstractSegment;
48  import org.apache.commons.imaging.formats.jpeg.segments.App13Segment;
49  import org.apache.commons.imaging.formats.jpeg.segments.App14Segment;
50  import org.apache.commons.imaging.formats.jpeg.segments.App2Segment;
51  import org.apache.commons.imaging.formats.jpeg.segments.ComSegment;
52  import org.apache.commons.imaging.formats.jpeg.segments.DqtSegment;
53  import org.apache.commons.imaging.formats.jpeg.segments.GenericSegment;
54  import org.apache.commons.imaging.formats.jpeg.segments.JfifSegment;
55  import org.apache.commons.imaging.formats.jpeg.segments.SofnSegment;
56  import org.apache.commons.imaging.formats.jpeg.segments.UnknownSegment;
57  import org.apache.commons.imaging.formats.jpeg.xmp.JpegXmpParser;
58  import org.apache.commons.imaging.formats.tiff.TiffField;
59  import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
60  import org.apache.commons.imaging.formats.tiff.TiffImageParser;
61  import org.apache.commons.imaging.formats.tiff.TiffImagingParameters;
62  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
63  import org.apache.commons.imaging.internal.Debug;
64  
65  public class JpegImageParser extends AbstractImageParser<JpegImagingParameters> implements XmpEmbeddable<JpegImagingParameters> {
66  
67      private static final Logger LOGGER = Logger.getLogger(JpegImageParser.class.getName());
68  
69      private static final String DEFAULT_EXTENSION = ImageFormats.JPEG.getDefaultExtension();
70      private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.JPEG.getExtensions();
71  
72      public static boolean isExifApp1Segment(final GenericSegment segment) {
73          return startsWith(segment.getSegmentData(), JpegConstants.EXIF_IDENTIFIER_CODE);
74      }
75  
76      private byte[] assembleSegments(final List<App2Segment> segments) throws ImagingException {
77          try {
78              return assembleSegments(segments, false);
79          } catch (final ImagingException e) {
80              return assembleSegments(segments, true);
81          }
82      }
83  
84      private byte[] assembleSegments(final List<App2Segment> segments, final boolean startWithZero) throws ImagingException {
85          if (segments.isEmpty()) {
86              throw new ImagingException("No App2 Segments Found.");
87          }
88  
89          final int markerCount = segments.get(0).numMarkers;
90  
91          if (segments.size() != markerCount) {
92              throw new ImagingException("App2 Segments Missing.  Found: " + segments.size() + ", Expected: " + markerCount + ".");
93          }
94  
95          segments.sort(null);
96  
97          final int offset = startWithZero ? 0 : 1;
98  
99          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 }