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