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  
21  import java.awt.Dimension;
22  import java.awt.image.BufferedImage;
23  import java.io.IOException;
24  import java.io.PrintWriter;
25  import java.nio.charset.StandardCharsets;
26  import java.text.NumberFormat;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.Collections;
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.AbstractGenericSegment;
48  import org.apache.commons.imaging.formats.jpeg.segments.AbstractSegment;
49  import org.apache.commons.imaging.formats.jpeg.segments.App13Segment;
50  import org.apache.commons.imaging.formats.jpeg.segments.App14Segment;
51  import org.apache.commons.imaging.formats.jpeg.segments.App2Segment;
52  import org.apache.commons.imaging.formats.jpeg.segments.ComSegment;
53  import org.apache.commons.imaging.formats.jpeg.segments.DqtSegment;
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  import org.apache.commons.lang3.ArrayUtils;
65  
66  public class JpegImageParser extends AbstractImageParser<JpegImagingParameters> implements XmpEmbeddable<JpegImagingParameters> {
67  
68      private static final Logger LOGGER = Logger.getLogger(JpegImageParser.class.getName());
69  
70      private static final String DEFAULT_EXTENSION = ImageFormats.JPEG.getDefaultExtension();
71      private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.JPEG.getExtensions();
72  
73      public static boolean isExifApp1Segment(final AbstractGenericSegment segment) {
74          return JpegConstants.EXIF_IDENTIFIER_CODE.isStartOf(segment.getSegmentData());
75      }
76  
77      /**
78       * Constructs a new instance with the big-endian byte order.
79       */
80      public JpegImageParser() {
81          // empty
82      }
83  
84      private byte[] assembleSegments(final List<App2Segment> segments) throws ImagingException {
85          try {
86              return assembleSegments(segments, false);
87          } catch (final ImagingException e) {
88              return assembleSegments(segments, true);
89          }
90      }
91  
92      private byte[] assembleSegments(final List<App2Segment> segments, final boolean startWithZero) throws ImagingException {
93          if (segments.isEmpty()) {
94              throw new ImagingException("No App2 Segments Found.");
95          }
96  
97          final int markerCount = segments.get(0).numMarkers;
98  
99          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 }