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