    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}