    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}