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