Changeset 8132 in josm


Ignore:
Timestamp:
2015-03-10T01:17:39+01:00 (4 years ago)
Author:
Don-vip
Message:

fix #11162 - update to metadata-extractor 2.7.2

Location:
trunk
Files:
61 added
31 deleted
49 edited

Legend:

Unmodified
Added
Removed
  • trunk/CONTRIBUTION

    r7937 r8132  
    3535
    3636The jpeg metadata extraction code is from Drew Noakes
    37 (http://code.google.com/p/metadata-extractor/) and licensed
     37(https://github.com/drewnoakes/metadata-extractor) and licensed
    3838with Apache license version 2.0.
    3939
  • trunk/src/com/drew/imaging/ImageProcessingException.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.imaging;
     
    2626/**
    2727 * An exception class thrown upon an unexpected condition that was fatal for the processing of an image.
    28  * 
    29  * @author Drew Noakes http://drewnoakes.com
     28 *
     29 * @author Drew Noakes https://drewnoakes.com
    3030 */
    3131public class ImageProcessingException extends CompoundException
  • trunk/src/com/drew/imaging/PhotographicConversions.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.imaging;
     
    2424 * Contains helper methods that perform photographic conversions.
    2525 *
    26  * @author Drew Noakes http://drewnoakes.com
     26 * @author Drew Noakes https://drewnoakes.com
    2727 */
    2828public final class PhotographicConversions
  • trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.imaging.jpeg;
    2222
    23 import com.drew.lang.ByteArrayReader;
    24 import com.drew.lang.annotations.NotNull;
    25 import com.drew.metadata.Metadata;
    26 import com.drew.metadata.exif.ExifReader;
    27 import com.drew.metadata.iptc.IptcReader;
    28 import com.drew.metadata.jpeg.JpegCommentReader;
    29 import com.drew.metadata.jpeg.JpegDirectory;
    30 import com.drew.metadata.jpeg.JpegReader;
    31 
    3223import java.io.File;
     24import java.io.FileInputStream;
    3325import java.io.IOException;
    3426import java.io.InputStream;
     27import java.util.Arrays;
     28import java.util.HashSet;
     29import java.util.Set;
     30
     31import com.drew.lang.StreamReader;
     32import com.drew.lang.annotations.NotNull;
     33import com.drew.lang.annotations.Nullable;
     34import com.drew.metadata.Metadata;
     35//import com.drew.metadata.adobe.AdobeJpegReader;
     36import com.drew.metadata.exif.ExifReader;
     37//import com.drew.metadata.icc.IccReader;
     38import com.drew.metadata.iptc.IptcReader;
     39//import com.drew.metadata.jfif.JfifReader;
     40import com.drew.metadata.jpeg.JpegCommentReader;
     41import com.drew.metadata.jpeg.JpegReader;
     42//import com.drew.metadata.photoshop.PhotoshopReader;
     43//import com.drew.metadata.xmp.XmpReader;
    3544
    3645/**
    37  * Obtains all available metadata from Jpeg formatted files.
     46 * Obtains all available metadata from JPEG formatted files.
    3847 *
    39  * @author Drew Noakes http://drewnoakes.com
     48 * @author Drew Noakes https://drewnoakes.com
    4049 */
    4150public class JpegMetadataReader
    4251{
    43     // TODO investigate supporting javax.imageio
    44 //    public static Metadata readMetadata(IIOMetadata metadata) throws JpegProcessingException {}
    45 //    public static Metadata readMetadata(ImageInputStream in) throws JpegProcessingException{}
    46 //    public static Metadata readMetadata(IIOImage image) throws JpegProcessingException{}
    47 //    public static Metadata readMetadata(ImageReader reader) throws JpegProcessingException{}
     52    public static final Iterable<JpegSegmentMetadataReader> ALL_READERS = Arrays.asList(
     53            new JpegReader(),
     54            new JpegCommentReader(),
     55            //new JfifReader(),
     56            new ExifReader(),
     57            //new XmpReader(),
     58            //new IccReader(),
     59            //new PhotoshopReader(),
     60            new IptcReader()//,
     61            //new AdobeJpegReader()
     62    );
    4863
    4964    @NotNull
    50     public static Metadata readMetadata(@NotNull InputStream inputStream) throws JpegProcessingException
     65    public static Metadata readMetadata(@NotNull InputStream inputStream, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException
    5166    {
    52         return readMetadata(inputStream, true);
     67        Metadata metadata = new Metadata();
     68        process(metadata, inputStream, readers);
     69        return metadata;
    5370    }
    5471
    5572    @NotNull
    56     public static Metadata readMetadata(@NotNull InputStream inputStream, final boolean waitForBytes) throws JpegProcessingException
     73    public static Metadata readMetadata(@NotNull InputStream inputStream) throws JpegProcessingException, IOException
    5774    {
    58         JpegSegmentReader segmentReader = new JpegSegmentReader(inputStream, waitForBytes);
    59         return extractMetadataFromJpegSegmentReader(segmentReader.getSegmentData());
     75        return readMetadata(inputStream, null);
     76    }
     77
     78    @NotNull
     79    public static Metadata readMetadata(@NotNull File file, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException
     80    {
     81        InputStream inputStream = null;
     82        try
     83        {
     84            inputStream = new FileInputStream(file);
     85            return readMetadata(inputStream, readers);
     86        } finally {
     87            if (inputStream != null)
     88                inputStream.close();
     89        }
    6090    }
    6191
     
    6393    public static Metadata readMetadata(@NotNull File file) throws JpegProcessingException, IOException
    6494    {
    65         JpegSegmentReader segmentReader = new JpegSegmentReader(file);
    66         return extractMetadataFromJpegSegmentReader(segmentReader.getSegmentData());
     95        return readMetadata(file, null);
    6796    }
    6897
    69     @NotNull
    70     public static Metadata extractMetadataFromJpegSegmentReader(@NotNull JpegSegmentData segmentReader)
     98    public static void process(@NotNull Metadata metadata, @NotNull InputStream inputStream) throws JpegProcessingException, IOException
    7199    {
    72         final Metadata metadata = new Metadata();
     100        process(metadata, inputStream, null);
     101    }
    73102
    74         // Loop through looking for all SOFn segments.  When we find one, we know what type of compression
    75         // was used for the JPEG, and we can process the JPEG metadata in the segment too.
    76         for (byte i = 0; i < 16; i++) {
    77             // There are no SOF4 or SOF12 segments, so don't bother
    78             if (i == 4 || i == 12)
    79                 continue;
    80             // Should never have more than one SOFn for a given 'n'.
    81             byte[] jpegSegment = segmentReader.getSegment((byte)(JpegSegmentReader.SEGMENT_SOF0 + i));
    82             if (jpegSegment == null)
    83                 continue;
    84             JpegDirectory directory = metadata.getOrCreateDirectory(JpegDirectory.class);
    85             directory.setInt(JpegDirectory.TAG_JPEG_COMPRESSION_TYPE, i);
    86             new JpegReader().extract(new ByteArrayReader(jpegSegment), metadata);
    87             break;
    88         }
     103    public static void process(@NotNull Metadata metadata, @NotNull InputStream inputStream, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException
     104    {
     105        if (readers == null)
     106            readers = ALL_READERS;
    89107
    90         // There should never be more than one COM segment.
    91         byte[] comSegment = segmentReader.getSegment(JpegSegmentReader.SEGMENT_COM);
    92         if (comSegment != null)
    93             new JpegCommentReader().extract(new ByteArrayReader(comSegment), metadata);
    94 
    95         // Loop through all APP1 segments, checking the leading bytes to identify the format of each.
    96         for (byte[] app1Segment : segmentReader.getSegments(JpegSegmentReader.SEGMENT_APP1)) {
    97             if (app1Segment.length > 3 && "EXIF".equalsIgnoreCase(new String(app1Segment, 0, 4)))
    98                 new ExifReader().extract(new ByteArrayReader(app1Segment), metadata);
    99 
    100             //if (app1Segment.length > 27 && "http://ns.adobe.com/xap/1.0/".equalsIgnoreCase(new String(app1Segment, 0, 28)))
    101             //    new XmpReader().extract(new ByteArrayReader(app1Segment), metadata);
    102         }
    103 
    104         // Loop through all APPD segments, checking the leading bytes to identify the format of each.
    105         for (byte[] appdSegment : segmentReader.getSegments(JpegSegmentReader.SEGMENT_APPD)) {
    106             if (appdSegment.length > 12 && "Photoshop 3.0".compareTo(new String(appdSegment, 0, 13))==0) {
    107                 //new PhotoshopReader().extract(new ByteArrayReader(appdSegment), metadata);
    108             } else {
    109                 // TODO might be able to check for a leading 0x1c02 for IPTC data...
    110                 new IptcReader().extract(new ByteArrayReader(appdSegment), metadata);
     108        Set<JpegSegmentType> segmentTypes = new HashSet<JpegSegmentType>();
     109        for (JpegSegmentMetadataReader reader : readers) {
     110            for (JpegSegmentType type : reader.getSegmentTypes()) {
     111                segmentTypes.add(type);
    111112            }
    112113        }
    113114
    114         return metadata;
     115        JpegSegmentData segmentData = JpegSegmentReader.readSegments(new StreamReader(inputStream), segmentTypes);
     116
     117        processJpegSegmentData(metadata, readers, segmentData);
     118    }
     119
     120    public static void processJpegSegmentData(Metadata metadata, Iterable<JpegSegmentMetadataReader> readers, JpegSegmentData segmentData)
     121    {
     122        // Pass the appropriate byte arrays to each reader.
     123        for (JpegSegmentMetadataReader reader : readers) {
     124            for (JpegSegmentType segmentType : reader.getSegmentTypes()) {
     125                for (byte[] segmentBytes : segmentData.getSegments(segmentType)) {
     126                    if (reader.canProcess(segmentBytes, segmentType)) {
     127                        reader.extract(segmentBytes, metadata, segmentType);
     128                    }
     129                }
     130            }
     131        }
    115132    }
    116133
     
    120137    }
    121138}
    122 
  • trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.imaging.jpeg;
     
    2525
    2626/**
    27  * An exception class thrown upon unexpected and fatal conditions while processing a Jpeg file.
     27 * An exception class thrown upon unexpected and fatal conditions while processing a JPEG file.
    2828 *
    29  * @author Drew Noakes http://drewnoakes.com
     29 * @author Drew Noakes https://drewnoakes.com
    3030 */
    3131public class JpegProcessingException extends ImageProcessingException
  • trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.imaging.jpeg;
     
    2424import com.drew.lang.annotations.Nullable;
    2525
    26 import java.io.*;
    27 import java.util.ArrayList;
    28 import java.util.HashMap;
    29 import java.util.List;
     26import java.util.*;
    3027
    3128/**
    32  * Holds a collection of Jpeg data segments.  This need not necessarily be all segments
    33  * within the Jpeg.  For example, it may be convenient to store only the non-image
    34  * segments when analysing (or serializing) metadata.
    35  * <p/>
    36  * Segments are keyed via their segment marker (a byte).  Where multiple segments use the
    37  * same segment marker, they will all be stored and available.
    38  *
    39  * @author Drew Noakes http://drewnoakes.com
     29 * Holds a collection of JPEG data segments.  This need not necessarily be all segments
     30 * within the JPEG. For example, it may be convenient to store only the non-image
     31 * segments when analysing metadata.
     32 * <p>
     33 * Segments are keyed via their {@link JpegSegmentType}. Where multiple segments use the
     34 * same segment type, they will all be stored and available.
     35 * <p>
     36 * Each segment type may contain multiple entries. Conceptually the model is:
     37 * <code>Map&lt;JpegSegmentType, Collection&lt;byte[]&gt;&gt;</code>. This class provides
     38 * convenience methods around that structure.
     39 *
     40 * @author Drew Noakes https://drewnoakes.com
    4041 */
    41 public class JpegSegmentData implements Serializable
     42public class JpegSegmentData
    4243{
    43     private static final long serialVersionUID = 7110175216435025451L;
    44    
    45     /** A map of byte[], keyed by the segment marker */
     44    // TODO key this on JpegSegmentType rather than Byte, and hopefully lose much of the use of 'byte' with this class
    4645    @NotNull
    4746    private final HashMap<Byte, List<byte[]>> _segmentDataMap = new HashMap<Byte, List<byte[]>>(10);
     
    4948    /**
    5049     * Adds segment bytes to the collection.
    51      * @param segmentMarker
    52      * @param segmentBytes
    53      */
    54     @SuppressWarnings({ "MismatchedQueryAndUpdateOfCollection" })
    55     public void addSegment(byte segmentMarker, @NotNull byte[] segmentBytes)
    56     {
    57         final List<byte[]> segmentList = getOrCreateSegmentList(segmentMarker);
    58         segmentList.add(segmentBytes);
    59     }
    60 
    61     /**
    62      * Gets the first Jpeg segment data for the specified marker.
    63      * @param segmentMarker the byte identifier for the desired segment
     50     *
     51     * @param segmentType  the type of the segment being added
     52     * @param segmentBytes the byte array holding data for the segment being added
     53     */
     54    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
     55    public void addSegment(byte segmentType, @NotNull byte[] segmentBytes)
     56    {
     57        getOrCreateSegmentList(segmentType).add(segmentBytes);
     58    }
     59
     60    /**
     61     * Gets the set of JPEG segment type identifiers.
     62     */
     63    public Iterable<JpegSegmentType> getSegmentTypes()
     64    {
     65        Set<JpegSegmentType> segmentTypes = new HashSet<JpegSegmentType>();
     66
     67        for (Byte segmentTypeByte : _segmentDataMap.keySet())
     68        {
     69            JpegSegmentType segmentType = JpegSegmentType.fromByte(segmentTypeByte);
     70            if (segmentType == null) {
     71                throw new IllegalStateException("Should not have a segmentTypeByte that is not in the enum: " + Integer.toHexString(segmentTypeByte));
     72            }
     73            segmentTypes.add(segmentType);
     74        }
     75
     76        return segmentTypes;
     77    }
     78
     79    /**
     80     * Gets the first JPEG segment data for the specified type.
     81     *
     82     * @param segmentType the JpegSegmentType for the desired segment
    6483     * @return a byte[] containing segment data or null if no data exists for that segment
    6584     */
    6685    @Nullable
    67     public byte[] getSegment(byte segmentMarker)
    68     {
    69         return getSegment(segmentMarker, 0);
    70     }
    71 
    72     /**
    73      * Gets segment data for a specific occurrence and marker.  Use this method when more than one occurrence
    74      * of segment data for a given marker exists.
    75      * @param segmentMarker identifies the required segment
    76      * @param occurrence the zero-based index of the occurrence
    77      * @return the segment data as a byte[], or null if no segment exists for the marker & occurrence
    78      */
    79     @Nullable
    80     public byte[] getSegment(byte segmentMarker, int occurrence)
    81     {
    82         final List<byte[]> segmentList = getSegmentList(segmentMarker);
    83 
    84         if (segmentList==null || segmentList.size()<=occurrence)
    85             return null;
    86         else
    87             return segmentList.get(occurrence);
    88     }
    89 
    90     /**
    91      * Returns all instances of a given Jpeg segment.  If no instances exist, an empty sequence is returned.
    92      *
    93      * @param segmentMarker a number which identifies the type of Jpeg segment being queried
    94      * @return zero or more byte arrays, each holding the data of a Jpeg segment
    95      */
    96     @NotNull
    97     public Iterable<byte[]> getSegments(byte segmentMarker)
    98     {
    99         final List<byte[]> segmentList = getSegmentList(segmentMarker);
    100         return segmentList==null ? new ArrayList<byte[]>() : segmentList;
    101     }
    102 
    103     @Nullable
    104     public List<byte[]> getSegmentList(byte segmentMarker)
    105     {
    106         return _segmentDataMap.get(Byte.valueOf(segmentMarker));
    107     }
    108 
    109     @NotNull
    110     private List<byte[]> getOrCreateSegmentList(byte segmentMarker)
     86    public byte[] getSegment(byte segmentType)
     87    {
     88        return getSegment(segmentType, 0);
     89    }
     90
     91    /**
     92     * Gets the first JPEG segment data for the specified type.
     93     *
     94     * @param segmentType the JpegSegmentType for the desired segment
     95     * @return a byte[] containing segment data or null if no data exists for that segment
     96     */
     97    @Nullable
     98    public byte[] getSegment(@NotNull JpegSegmentType segmentType)
     99    {
     100        return getSegment(segmentType.byteValue, 0);
     101    }
     102
     103    /**
     104     * Gets segment data for a specific occurrence and type.  Use this method when more than one occurrence
     105     * of segment data for a given type exists.
     106     *
     107     * @param segmentType identifies the required segment
     108     * @param occurrence  the zero-based index of the occurrence
     109     * @return the segment data as a byte[], or null if no segment exists for the type &amp; occurrence
     110     */
     111    @Nullable
     112    public byte[] getSegment(@NotNull JpegSegmentType segmentType, int occurrence)
     113    {
     114        return getSegment(segmentType.byteValue, occurrence);
     115    }
     116
     117    /**
     118     * Gets segment data for a specific occurrence and type.  Use this method when more than one occurrence
     119     * of segment data for a given type exists.
     120     *
     121     * @param segmentType identifies the required segment
     122     * @param occurrence  the zero-based index of the occurrence
     123     * @return the segment data as a byte[], or null if no segment exists for the type &amp; occurrence
     124     */
     125    @Nullable
     126    public byte[] getSegment(byte segmentType, int occurrence)
     127    {
     128        final List<byte[]> segmentList = getSegmentList(segmentType);
     129
     130        return segmentList != null && segmentList.size() > occurrence
     131                ? segmentList.get(occurrence)
     132                : null;
     133    }
     134
     135    /**
     136     * Returns all instances of a given JPEG segment.  If no instances exist, an empty sequence is returned.
     137     *
     138     * @param segmentType a number which identifies the type of JPEG segment being queried
     139     * @return zero or more byte arrays, each holding the data of a JPEG segment
     140     */
     141    @NotNull
     142    public Iterable<byte[]> getSegments(@NotNull JpegSegmentType segmentType)
     143    {
     144        return getSegments(segmentType.byteValue);
     145    }
     146
     147    /**
     148     * Returns all instances of a given JPEG segment.  If no instances exist, an empty sequence is returned.
     149     *
     150     * @param segmentType a number which identifies the type of JPEG segment being queried
     151     * @return zero or more byte arrays, each holding the data of a JPEG segment
     152     */
     153    @NotNull
     154    public Iterable<byte[]> getSegments(byte segmentType)
     155    {
     156        final List<byte[]> segmentList = getSegmentList(segmentType);
     157        return segmentList == null ? new ArrayList<byte[]>() : segmentList;
     158    }
     159
     160    @Nullable
     161    private List<byte[]> getSegmentList(byte segmentType)
     162    {
     163        return _segmentDataMap.get(segmentType);
     164    }
     165
     166    @NotNull
     167    private List<byte[]> getOrCreateSegmentList(byte segmentType)
    111168    {
    112169        List<byte[]> segmentList;
    113         if (_segmentDataMap.containsKey(segmentMarker)) {
    114             segmentList = _segmentDataMap.get(segmentMarker);
     170        if (_segmentDataMap.containsKey(segmentType)) {
     171            segmentList = _segmentDataMap.get(segmentType);
    115172        } else {
    116173            segmentList = new ArrayList<byte[]>();
    117             _segmentDataMap.put(segmentMarker, segmentList);
     174            _segmentDataMap.put(segmentType, segmentList);
    118175        }
    119176        return segmentList;
     
    121178
    122179    /**
    123      * Returns the count of segment data byte arrays stored for a given segment marker.
    124      * @param segmentMarker identifies the required segment
     180     * Returns the count of segment data byte arrays stored for a given segment type.
     181     *
     182     * @param segmentType identifies the required segment
    125183     * @return the segment count (zero if no segments exist).
    126184     */
    127     public int getSegmentCount(byte segmentMarker)
    128     {
    129         final List<byte[]> segmentList = getSegmentList(segmentMarker);
     185    public int getSegmentCount(@NotNull JpegSegmentType segmentType)
     186    {
     187        return getSegmentCount(segmentType.byteValue);
     188    }
     189
     190    /**
     191     * Returns the count of segment data byte arrays stored for a given segment type.
     192     *
     193     * @param segmentType identifies the required segment
     194     * @return the segment count (zero if no segments exist).
     195     */
     196    public int getSegmentCount(byte segmentType)
     197    {
     198        final List<byte[]> segmentList = getSegmentList(segmentType);
    130199        return segmentList == null ? 0 : segmentList.size();
    131200    }
     
    133202    /**
    134203     * Removes a specified instance of a segment's data from the collection.  Use this method when more than one
    135      * occurrence of segment data for a given marker exists.
    136      * @param segmentMarker identifies the required segment
    137      * @param occurrence the zero-based index of the segment occurrence to remove.
    138      */
    139     @SuppressWarnings({ "MismatchedQueryAndUpdateOfCollection" })
    140     public void removeSegmentOccurrence(byte segmentMarker, int occurrence)
    141     {
    142         final List<byte[]> segmentList = _segmentDataMap.get(Byte.valueOf(segmentMarker));
     204     * occurrence of segment data exists for a given type exists.
     205     *
     206     * @param segmentType identifies the required segment
     207     * @param occurrence  the zero-based index of the segment occurrence to remove.
     208     */
     209    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
     210    public void removeSegmentOccurrence(@NotNull JpegSegmentType segmentType, int occurrence)
     211    {
     212        removeSegmentOccurrence(segmentType.byteValue, occurrence);
     213    }
     214
     215    /**
     216     * Removes a specified instance of a segment's data from the collection.  Use this method when more than one
     217     * occurrence of segment data exists for a given type exists.
     218     *
     219     * @param segmentType identifies the required segment
     220     * @param occurrence  the zero-based index of the segment occurrence to remove.
     221     */
     222    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
     223    public void removeSegmentOccurrence(byte segmentType, int occurrence)
     224    {
     225        final List<byte[]> segmentList = _segmentDataMap.get(segmentType);
    143226        segmentList.remove(occurrence);
    144227    }
    145228
    146229    /**
    147      * Removes all segments from the collection having the specified marker.
    148      * @param segmentMarker identifies the required segment
    149      */
    150     public void removeSegment(byte segmentMarker)
    151     {
    152         _segmentDataMap.remove(Byte.valueOf(segmentMarker));
    153     }
    154 
    155     /**
    156      * Determines whether data is present for a given segment marker.
    157      * @param segmentMarker identifies the required segment
     230     * Removes all segments from the collection having the specified type.
     231     *
     232     * @param segmentType identifies the required segment
     233     */
     234    public void removeSegment(@NotNull JpegSegmentType segmentType)
     235    {
     236        removeSegment(segmentType.byteValue);
     237    }
     238
     239    /**
     240     * Removes all segments from the collection having the specified type.
     241     *
     242     * @param segmentType identifies the required segment
     243     */
     244    public void removeSegment(byte segmentType)
     245    {
     246        _segmentDataMap.remove(segmentType);
     247    }
     248
     249    /**
     250     * Determines whether data is present for a given segment type.
     251     *
     252     * @param segmentType identifies the required segment
    158253     * @return true if data exists, otherwise false
    159254     */
    160     public boolean containsSegment(byte segmentMarker)
    161     {
    162         return _segmentDataMap.containsKey(Byte.valueOf(segmentMarker));
    163     }
    164 
    165     /**
    166      * Serialises the contents of a JpegSegmentData to a file.
    167      * @param file to file to write from
    168      * @param segmentData the data to write
    169      * @throws IOException if problems occur while writing
    170      */
    171     public static void toFile(@NotNull File file, @NotNull JpegSegmentData segmentData) throws IOException
    172     {
    173         FileOutputStream fileOutputStream = null;
    174         try
    175         {
    176             fileOutputStream = new FileOutputStream(file);
    177             new ObjectOutputStream(fileOutputStream).writeObject(segmentData);
    178         }
    179         finally
    180         {
    181             if (fileOutputStream!=null)
    182                 fileOutputStream.close();
    183         }
    184     }
    185 
    186     /**
    187      * Deserialises the contents of a JpegSegmentData from a file.
    188      * @param file the file to read from
    189      * @return the JpegSegmentData as read
    190      * @throws IOException if problems occur while reading
    191      * @throws ClassNotFoundException if problems occur while deserialising
    192      */
    193     @NotNull
    194     public static JpegSegmentData fromFile(@NotNull File file) throws IOException, ClassNotFoundException
    195     {
    196         ObjectInputStream inputStream = null;
    197         try
    198         {
    199             inputStream = new ObjectInputStream(new FileInputStream(file));
    200             return (JpegSegmentData)inputStream.readObject();
    201         }
    202         finally
    203         {
    204             if (inputStream!=null)
    205                 inputStream.close();
    206         }
     255    public boolean containsSegment(@NotNull JpegSegmentType segmentType)
     256    {
     257        return containsSegment(segmentType.byteValue);
     258    }
     259
     260    /**
     261     * Determines whether data is present for a given segment type.
     262     *
     263     * @param segmentType identifies the required segment
     264     * @return true if data exists, otherwise false
     265     */
     266    public boolean containsSegment(byte segmentType)
     267    {
     268        return _segmentDataMap.containsKey(segmentType);
    207269    }
    208270}
  • trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.imaging.jpeg;
    2222
     23import com.drew.lang.SequentialReader;
     24import com.drew.lang.StreamReader;
    2325import com.drew.lang.annotations.NotNull;
    2426import com.drew.lang.annotations.Nullable;
    2527
    26 import java.io.*;
     28import java.io.File;
     29import java.io.FileInputStream;
     30import java.io.IOException;
     31import java.util.HashSet;
     32import java.util.Set;
    2733
    2834/**
    29  * Performs read functions of Jpeg files, returning specific file segments.
    30  * @author  Drew Noakes http://drewnoakes.com
     35 * Performs read functions of JPEG files, returning specific file segments.
     36 * <p>
     37 * JPEG files are composed of a sequence of consecutive JPEG 'segments'. Each is identified by one of a set of byte
     38 * values, modelled in the {@link JpegSegmentType} enumeration. Use <code>readSegments</code> to read out the some
     39 * or all segments into a {@link JpegSegmentData} object, from which the raw JPEG segment byte arrays may be accessed.
     40 *
     41 * @author Drew Noakes https://drewnoakes.com
    3142 */
    3243public class JpegSegmentReader
    3344{
    34     // TODO add a findAvailableSegments() method
    35     // TODO add more segment identifiers
    36     // TODO add a getSegmentDescription() method, returning for example 'App1 application data segment, commonly containing Exif data'
    37 
    38     @NotNull
    39     private final JpegSegmentData _segmentData;
    40 
    4145    /**
    4246     * Private, because this segment crashes my algorithm, and searching for it doesn't work (yet).
    4347     */
    44     private static final byte SEGMENT_SOS = (byte)0xDA;
     48    private static final byte SEGMENT_SOS = (byte) 0xDA;
    4549
    4650    /**
    4751     * Private, because one wouldn't search for it.
    4852     */
    49     private static final byte MARKER_EOI = (byte)0xD9;
    50 
    51     /** APP0 Jpeg segment identifier -- JFIF data (also JFXX apparently). */
    52     public static final byte SEGMENT_APP0 = (byte)0xE0;
    53     /** APP1 Jpeg segment identifier -- where Exif data is kept.  XMP data is also kept in here, though usually in a second instance. */
    54     public static final byte SEGMENT_APP1 = (byte)0xE1;
    55     /** APP2 Jpeg segment identifier. */
    56     public static final byte SEGMENT_APP2 = (byte)0xE2;
    57     /** APP3 Jpeg segment identifier. */
    58     public static final byte SEGMENT_APP3 = (byte)0xE3;
    59     /** APP4 Jpeg segment identifier. */
    60     public static final byte SEGMENT_APP4 = (byte)0xE4;
    61     /** APP5 Jpeg segment identifier. */
    62     public static final byte SEGMENT_APP5 = (byte)0xE5;
    63     /** APP6 Jpeg segment identifier. */
    64     public static final byte SEGMENT_APP6 = (byte)0xE6;
    65     /** APP7 Jpeg segment identifier. */
    66     public static final byte SEGMENT_APP7 = (byte)0xE7;
    67     /** APP8 Jpeg segment identifier. */
    68     public static final byte SEGMENT_APP8 = (byte)0xE8;
    69     /** APP9 Jpeg segment identifier. */
    70     public static final byte SEGMENT_APP9 = (byte)0xE9;
    71     /** APPA (App10) Jpeg segment identifier -- can hold Unicode comments. */
    72     public static final byte SEGMENT_APPA = (byte)0xEA;
    73     /** APPB (App11) Jpeg segment identifier. */
    74     public static final byte SEGMENT_APPB = (byte)0xEB;
    75     /** APPC (App12) Jpeg segment identifier. */
    76     public static final byte SEGMENT_APPC = (byte)0xEC;
    77     /** APPD (App13) Jpeg segment identifier -- IPTC data in here. */
    78     public static final byte SEGMENT_APPD = (byte)0xED;
    79     /** APPE (App14) Jpeg segment identifier. */
    80     public static final byte SEGMENT_APPE = (byte)0xEE;
    81     /** APPF (App15) Jpeg segment identifier. */
    82     public static final byte SEGMENT_APPF = (byte)0xEF;
    83     /** Start Of Image segment identifier. */
    84     public static final byte SEGMENT_SOI = (byte)0xD8;
    85     /** Define Quantization Table segment identifier. */
    86     public static final byte SEGMENT_DQT = (byte)0xDB;
    87     /** Define Huffman Table segment identifier. */
    88     public static final byte SEGMENT_DHT = (byte)0xC4;
    89     /** Start-of-Frame Zero segment identifier. */
    90     public static final byte SEGMENT_SOF0 = (byte)0xC0;
    91     /** Jpeg comment segment identifier. */
    92     public static final byte SEGMENT_COM = (byte)0xFE;
     53    private static final byte MARKER_EOI = (byte) 0xD9;
    9354
    9455    /**
    95      * Creates a JpegSegmentReader for a specific file.
    96      * @param file the Jpeg file to read segments from
     56     * Processes the provided JPEG data, and extracts the specified JPEG segments into a {@link JpegSegmentData} object.
     57     * <p>
     58     * Will not return SOS (start of scan) or EOI (end of image) segments.
     59     *
     60     * @param file a {@link File} from which the JPEG data will be read.
     61     * @param segmentTypes the set of JPEG segments types that are to be returned. If this argument is <code>null</code>
     62     *                     then all found segment types are returned.
    9763     */
    98     @SuppressWarnings({ "ConstantConditions" })
    99     public JpegSegmentReader(@NotNull File file) throws JpegProcessingException, IOException
     64    @NotNull
     65    public static JpegSegmentData readSegments(@NotNull File file, @Nullable Iterable<JpegSegmentType> segmentTypes) throws JpegProcessingException, IOException
    10066    {
    101         if (file==null)
    102             throw new NullPointerException();
    103 
    104         InputStream inputStream = null;
     67        FileInputStream stream = null;
    10568        try {
    106             inputStream = new FileInputStream(file);
    107             _segmentData = readSegments(new BufferedInputStream(inputStream), false);
     69            stream = new FileInputStream(file);
     70            return readSegments(new StreamReader(stream), segmentTypes);
    10871        } finally {
    109             if (inputStream != null)
    110                 inputStream.close();
     72            if (stream != null) {
     73                stream.close();
     74            }
    11175        }
    11276    }
    11377
    11478    /**
    115      * Creates a JpegSegmentReader for a byte array.
    116      * @param fileContents the byte array containing Jpeg data
     79     * Processes the provided JPEG data, and extracts the specified JPEG segments into a {@link JpegSegmentData} object.
     80     * <p>
     81     * Will not return SOS (start of scan) or EOI (end of image) segments.
     82     *
     83     * @param reader a {@link SequentialReader} from which the JPEG data will be read. It must be positioned at the
     84     *               beginning of the JPEG data stream.
     85     * @param segmentTypes the set of JPEG segments types that are to be returned. If this argument is <code>null</code>
     86     *                     then all found segment types are returned.
    11787     */
    118     @SuppressWarnings({ "ConstantConditions" })
    119     public JpegSegmentReader(@NotNull byte[] fileContents) throws JpegProcessingException
     88    @NotNull
     89    public static JpegSegmentData readSegments(@NotNull final SequentialReader reader, @Nullable Iterable<JpegSegmentType> segmentTypes) throws JpegProcessingException, IOException
    12090    {
    121         if (fileContents==null)
    122             throw new NullPointerException();
     91        // Must be big-endian
     92        assert (reader.isMotorolaByteOrder());
    12393
    124         BufferedInputStream stream = new BufferedInputStream(new ByteArrayInputStream(fileContents));
    125         _segmentData = readSegments(stream, false);
     94        // first two bytes should be JPEG magic number
     95        final int magicNumber = reader.getUInt16();
     96        if (magicNumber != 0xFFD8) {
     97            throw new JpegProcessingException("JPEG data is expected to begin with 0xFFD8 (ÿØ) not 0x" + Integer.toHexString(magicNumber));
     98        }
     99
     100        Set<Byte> segmentTypeBytes = null;
     101        if (segmentTypes != null) {
     102            segmentTypeBytes = new HashSet<Byte>();
     103            for (JpegSegmentType segmentType : segmentTypes) {
     104                segmentTypeBytes.add(segmentType.byteValue);
     105            }
     106        }
     107
     108        JpegSegmentData segmentData = new JpegSegmentData();
     109
     110        do {
     111            // Find the segment marker. Markers are zero or more 0xFF bytes, followed
     112            // by a 0xFF and then a byte not equal to 0x00 or 0xFF.
     113
     114            final short segmentIdentifier = reader.getUInt8();
     115
     116            // We must have at least one 0xFF byte
     117            if (segmentIdentifier != 0xFF)
     118                throw new JpegProcessingException("Expected JPEG segment start identifier 0xFF, not 0x" + Integer.toHexString(segmentIdentifier).toUpperCase());
     119
     120            // Read until we have a non-0xFF byte. This identifies the segment type.
     121            byte segmentType = reader.getInt8();
     122            while (segmentType == (byte)0xFF)
     123                segmentType = reader.getInt8();
     124
     125            if (segmentType == 0)
     126                throw new JpegProcessingException("Expected non-zero byte as part of JPEG marker identifier");
     127
     128            if (segmentType == SEGMENT_SOS) {
     129                // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would
     130                // have to search for the two bytes: 0xFF 0xD9 (EOI).
     131                // It comes last so simply return at this point
     132                return segmentData;
     133            }
     134
     135            if (segmentType == MARKER_EOI) {
     136                // the 'End-Of-Image' segment -- this should never be found in this fashion
     137                return segmentData;
     138            }
     139
     140            // next 2-bytes are <segment-size>: [high-byte] [low-byte]
     141            int segmentLength = reader.getUInt16();
     142
     143            // segment length includes size bytes, so subtract two
     144            segmentLength -= 2;
     145
     146            if (segmentLength < 0)
     147                throw new JpegProcessingException("JPEG segment size would be less than zero");
     148
     149            // Check whether we are interested in this segment
     150            if (segmentTypeBytes == null || segmentTypeBytes.contains(segmentType)) {
     151                byte[] segmentBytes = reader.getBytes(segmentLength);
     152                assert (segmentLength == segmentBytes.length);
     153                segmentData.addSegment(segmentType, segmentBytes);
     154            } else {
     155                // Some if the JPEG is truncated, just return what data we've already gathered
     156                if (!reader.trySkip(segmentLength)) {
     157                    return segmentData;
     158                }
     159            }
     160
     161        } while (true);
    126162    }
    127163
    128     /**
    129      * Creates a JpegSegmentReader for an InputStream.
    130      * @param inputStream the InputStream containing Jpeg data
    131      */
    132     @SuppressWarnings({ "ConstantConditions" })
    133     public JpegSegmentReader(@NotNull InputStream inputStream, boolean waitForBytes) throws JpegProcessingException
     164    private JpegSegmentReader() throws Exception
    134165    {
    135         if (inputStream==null)
    136             throw new NullPointerException();
    137 
    138         BufferedInputStream bufferedInputStream = inputStream instanceof BufferedInputStream
    139                 ? (BufferedInputStream)inputStream
    140                 : new BufferedInputStream(inputStream);
    141 
    142         _segmentData = readSegments(bufferedInputStream, waitForBytes);
    143     }
    144 
    145     /**
    146      * Reads the first instance of a given Jpeg segment, returning the contents as
    147      * a byte array.
    148      * @param segmentMarker the byte identifier for the desired segment
    149      * @return the byte array if found, else null
    150      */
    151     @Nullable
    152     public byte[] readSegment(byte segmentMarker)
    153     {
    154         return readSegment(segmentMarker, 0);
    155     }
    156 
    157     /**
    158      * Reads the Nth instance of a given Jpeg segment, returning the contents as a byte array.
    159      *
    160      * @param segmentMarker the byte identifier for the desired segment
    161      * @param occurrence the occurrence of the specified segment within the jpeg file
    162      * @return the byte array if found, else null
    163      */
    164     @Nullable
    165     public byte[] readSegment(byte segmentMarker, int occurrence)
    166     {
    167         return _segmentData.getSegment(segmentMarker, occurrence);
    168     }
    169 
    170     /**
    171      * Returns all instances of a given Jpeg segment.  If no instances exist, an empty sequence is returned.
    172      *
    173      * @param segmentMarker a number which identifies the type of Jpeg segment being queried
    174      * @return zero or more byte arrays, each holding the data of a Jpeg segment
    175      */
    176     @NotNull
    177     public Iterable<byte[]> readSegments(byte segmentMarker)
    178     {
    179         return _segmentData.getSegments(segmentMarker);
    180     }
    181 
    182     /**
    183      * Returns the number of segments having the specified JPEG segment marker.
    184      * @param segmentMarker the JPEG segment identifying marker.
    185      * @return the count of matching segments.
    186      */
    187     public final int getSegmentCount(byte segmentMarker)
    188     {
    189         return _segmentData.getSegmentCount(segmentMarker);
    190     }
    191 
    192     /**
    193      * Returns the JpegSegmentData object used by this reader.
    194      * @return the JpegSegmentData object.
    195      */
    196     @NotNull
    197     public final JpegSegmentData getSegmentData()
    198     {
    199         return _segmentData;
    200     }
    201 
    202     @NotNull
    203     private JpegSegmentData readSegments(@NotNull final BufferedInputStream jpegInputStream, boolean waitForBytes) throws JpegProcessingException
    204     {
    205         JpegSegmentData segmentData = new JpegSegmentData();
    206 
    207         try {
    208             int offset = 0;
    209             // first two bytes should be jpeg magic number
    210             byte[] headerBytes = new byte[2];
    211             if (jpegInputStream.read(headerBytes, 0, 2)!=2)
    212                 throw new JpegProcessingException("not a jpeg file");
    213             final boolean hasValidHeader = (headerBytes[0] & 0xFF) == 0xFF && (headerBytes[1] & 0xFF) == 0xD8;
    214             if (!hasValidHeader)
    215                 throw new JpegProcessingException("not a jpeg file");
    216 
    217             offset += 2;
    218             do {
    219                 // need four bytes from stream for segment header before continuing
    220                 if (!checkForBytesOnStream(jpegInputStream, 4, waitForBytes))
    221                     throw new JpegProcessingException("stream ended before segment header could be read");
    222 
    223                 // next byte is 0xFF
    224                 byte segmentIdentifier = (byte)(jpegInputStream.read() & 0xFF);
    225                 if ((segmentIdentifier & 0xFF) != 0xFF) {
    226                     throw new JpegProcessingException("expected jpeg segment start identifier 0xFF at offset " + offset + ", not 0x" + Integer.toHexString(segmentIdentifier & 0xFF));
    227                 }
    228                 offset++;
    229                 // next byte is <segment-marker>
    230                 byte thisSegmentMarker = (byte)(jpegInputStream.read() & 0xFF);
    231                 offset++;
    232                 // next 2-bytes are <segment-size>: [high-byte] [low-byte]
    233                 byte[] segmentLengthBytes = new byte[2];
    234                 if (jpegInputStream.read(segmentLengthBytes, 0, 2) != 2)
    235                     throw new JpegProcessingException("Jpeg data ended unexpectedly.");
    236                 offset += 2;
    237                 int segmentLength = ((segmentLengthBytes[0] << 8) & 0xFF00) | (segmentLengthBytes[1] & 0xFF);
    238                 // segment length includes size bytes, so subtract two
    239                 segmentLength -= 2;
    240                 if (!checkForBytesOnStream(jpegInputStream, segmentLength, waitForBytes))
    241                     throw new JpegProcessingException("segment size would extend beyond file stream length");
    242                 if (segmentLength < 0)
    243                     throw new JpegProcessingException("segment size would be less than zero");
    244                 byte[] segmentBytes = new byte[segmentLength];
    245                 if (jpegInputStream.read(segmentBytes, 0, segmentLength) != segmentLength)
    246                     throw new JpegProcessingException("Jpeg data ended unexpectedly.");
    247                 offset += segmentLength;
    248                 if ((thisSegmentMarker & 0xFF) == (SEGMENT_SOS & 0xFF)) {
    249                     // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would
    250                     // have to search for the two bytes: 0xFF 0xD9 (EOI).
    251                     // It comes last so simply return at this point
    252                     return segmentData;
    253                 } else if ((thisSegmentMarker & 0xFF) == (MARKER_EOI & 0xFF)) {
    254                     // the 'End-Of-Image' segment -- this should never be found in this fashion
    255                     return segmentData;
    256                 } else {
    257                     segmentData.addSegment(thisSegmentMarker, segmentBytes);
    258                 }
    259             } while (true);
    260         } catch (IOException ioe) {
    261             throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe);
    262         } finally {
    263             try {
    264                 if (jpegInputStream != null) {
    265                     jpegInputStream.close();
    266                 }
    267             } catch (IOException ioe) {
    268                 throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe);
    269             }
    270         }
    271     }
    272 
    273     private boolean checkForBytesOnStream(@NotNull BufferedInputStream stream, int bytesNeeded, boolean waitForBytes) throws IOException
    274     {
    275         // NOTE  waiting is essential for network streams where data can be delayed, but it is not necessary for byte[] or filesystems
    276 
    277         if (!waitForBytes)
    278             return bytesNeeded <= stream.available();
    279 
    280         int count = 40; // * 100ms = approx 4 seconds
    281         while (count > 0) {
    282             if (bytesNeeded <= stream.available())
    283                return true;
    284             try {
    285                 Thread.sleep(100);
    286             } catch (InterruptedException e) {
    287                 // continue
    288             }
    289             count--;
    290         }
    291         return false;
     166        throw new Exception("Not intended for instantiation.");
    292167    }
    293168}
  • trunk/src/com/drew/lang/BufferBoundsException.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
    2222package com.drew.lang;
    2323
    24 import com.drew.lang.annotations.NotNull;
    25 
    2624import java.io.IOException;
    2725
    2826/**
    29  * A checked replacement for IndexOutOfBoundsException.  Used by BufferReader.
    30  * 
    31  * @author Drew Noakes http://drewnoakes.com
     27 * A checked replacement for {@link IndexOutOfBoundsException}.  Used by {@link RandomAccessReader}.
     28 *
     29 * @author Drew Noakes https://drewnoakes.com
    3230 */
    33 public final class BufferBoundsException extends Exception
     31public final class BufferBoundsException extends IOException
    3432{
    3533    private static final long serialVersionUID = 2911102837808946396L;
    3634
    37     public BufferBoundsException(@NotNull byte[] buffer, int index, int bytesRequested)
     35    public BufferBoundsException(int index, int bytesRequested, long bufferLength)
    3836    {
    39         super(getMessage(buffer, index, bytesRequested));
     37        super(getMessage(index, bytesRequested, bufferLength));
    4038    }
    4139
     
    4543    }
    4644
    47     public BufferBoundsException(final String message, final IOException innerException)
    48     {
    49         super(message, innerException);
    50     }
    51 
    52     private static String getMessage(@NotNull byte[] buffer, int index, int bytesRequested)
     45    private static String getMessage(int index, int bytesRequested, long bufferLength)
    5346    {
    5447        if (index < 0)
    55             return String.format("Attempt to read from buffer using a negative index (%s)", index);
     48            return String.format("Attempt to read from buffer using a negative index (%d)", index);
    5649
    57         return String.format("Attempt to read %d byte%s from beyond end of buffer (requested index: %d, max index: %d)",
    58                 bytesRequested, bytesRequested==1?"":"s", index, buffer.length - 1);
     50        if (bytesRequested < 0)
     51            return String.format("Number of requested bytes cannot be negative (%d)", bytesRequested);
     52
     53        if ((long)index + (long)bytesRequested - 1L > (long)Integer.MAX_VALUE)
     54            return String.format("Number of requested bytes summed with starting index exceed maximum range of signed 32 bit integers (requested index: %d, requested count: %d)", index, bytesRequested);
     55
     56        return String.format("Attempt to read from beyond end of underlying data source (requested index: %d, requested count: %d, max index: %d)",
     57                index, bytesRequested, bufferLength - 1);
    5958    }
    6059}
  • trunk/src/com/drew/lang/ByteArrayReader.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
     
    2424import com.drew.lang.annotations.NotNull;
    2525
    26 import java.io.UnsupportedEncodingException;
     26import java.io.IOException;
    2727
    2828/**
    2929 * Provides methods to read specific values from a byte array, with a consistent, checked exception structure for
    3030 * issues.
    31  * <p/>
     31 * <p>
    3232 * By default, the reader operates with Motorola byte order (big endianness).  This can be changed by calling
    33  * {@see setMotorolaByteOrder(boolean)}.
    34  * 
    35  * @author Drew Noakes http://drewnoakes.com
     33 * <code>setMotorolaByteOrder(boolean)</code>.
     34 *
     35 * @author Drew Noakes https://drewnoakes.com
    3636 * */
    37 public class ByteArrayReader implements BufferReader
     37public class ByteArrayReader extends RandomAccessReader
    3838{
    3939    @NotNull
    4040    private final byte[] _buffer;
    41     private boolean _isMotorolaByteOrder = true;
    4241
    4342    @SuppressWarnings({ "ConstantConditions" })
     
    4746        if (buffer == null)
    4847            throw new NullPointerException();
    49        
     48
    5049        _buffer = buffer;
    5150    }
     
    5756    }
    5857
    59 
    6058    @Override
    61     public void setMotorolaByteOrder(boolean motorolaByteOrder)
     59    protected byte getByte(int index) throws IOException
    6260    {
    63         _isMotorolaByteOrder = motorolaByteOrder;
    64     }
    65 
    66     @Override
    67     public boolean isMotorolaByteOrder()
    68     {
    69         return _isMotorolaByteOrder;
    70     }
    71 
    72     @Override
    73     public short getUInt8(int index) throws BufferBoundsException
    74     {
    75         checkBounds(index, 1);
    76 
    77         return (short) (_buffer[index] & 255);
    78     }
    79 
    80     @Override
    81     public byte getInt8(int index) throws BufferBoundsException
    82     {
    83         checkBounds(index, 1);
    84 
    8561        return _buffer[index];
    8662    }
    8763
    8864    @Override
    89     public int getUInt16(int index) throws BufferBoundsException
     65    protected void validateIndex(int index, int bytesRequested) throws IOException
    9066    {
    91         checkBounds(index, 2);
    92 
    93         if (_isMotorolaByteOrder) {
    94             // Motorola - MSB first
    95             return (_buffer[index    ] << 8 & 0xFF00) |
    96                    (_buffer[index + 1]      & 0xFF);
    97         } else {
    98             // Intel ordering - LSB first
    99             return (_buffer[index + 1] << 8 & 0xFF00) |
    100                    (_buffer[index    ]      & 0xFF);
    101         }
     67        if (!isValidIndex(index, bytesRequested))
     68            throw new BufferBoundsException(index, bytesRequested, _buffer.length);
    10269    }
    10370
    10471    @Override
    105     public short getInt16(int index) throws BufferBoundsException
     72    protected boolean isValidIndex(int index, int bytesRequested) throws IOException
    10673    {
    107         checkBounds(index, 2);
    108 
    109         if (_isMotorolaByteOrder) {
    110             // Motorola - MSB first
    111             return (short) (((short)_buffer[index    ] << 8 & (short)0xFF00) |
    112                             ((short)_buffer[index + 1]      & (short)0xFF));
    113         } else {
    114             // Intel ordering - LSB first
    115             return (short) (((short)_buffer[index + 1] << 8 & (short)0xFF00) |
    116                             ((short)_buffer[index    ]      & (short)0xFF));
    117         }
     74        return bytesRequested >= 0
     75            && index >= 0
     76            && (long)index + (long)bytesRequested - 1L < (long)_buffer.length;
    11877    }
    11978
    12079    @Override
    121     public long getUInt32(int index) throws BufferBoundsException
     80    @NotNull
     81    public byte[] getBytes(int index, int count) throws IOException
    12282    {
    123         checkBounds(index, 4);
    124 
    125         if (_isMotorolaByteOrder) {
    126             // Motorola - MSB first (big endian)
    127             return (((long)_buffer[index    ]) << 24 & 0xFF000000L) |
    128                     (((long)_buffer[index + 1]) << 16 & 0xFF0000L) |
    129                     (((long)_buffer[index + 2]) << 8  & 0xFF00L) |
    130                     (((long)_buffer[index + 3])       & 0xFFL);
    131         } else {
    132             // Intel ordering - LSB first (little endian)
    133             return (((long)_buffer[index + 3]) << 24 & 0xFF000000L) |
    134                     (((long)_buffer[index + 2]) << 16 & 0xFF0000L) |
    135                     (((long)_buffer[index + 1]) << 8  & 0xFF00L) |
    136                     (((long)_buffer[index    ])       & 0xFFL);
    137         }
    138     }
    139 
    140     @Override
    141     public int getInt32(int index) throws BufferBoundsException
    142     {
    143         checkBounds(index, 4);
    144 
    145         if (_isMotorolaByteOrder) {
    146             // Motorola - MSB first (big endian)
    147             return (_buffer[index    ] << 24 & 0xFF000000) |
    148                    (_buffer[index + 1] << 16 & 0xFF0000) |
    149                    (_buffer[index + 2] << 8  & 0xFF00) |
    150                    (_buffer[index + 3]       & 0xFF);
    151         } else {
    152             // Intel ordering - LSB first (little endian)
    153             return (_buffer[index + 3] << 24 & 0xFF000000) |
    154                    (_buffer[index + 2] << 16 & 0xFF0000) |
    155                    (_buffer[index + 1] << 8  & 0xFF00) |
    156                    (_buffer[index    ]       & 0xFF);
    157         }
    158     }
    159 
    160     @Override
    161     public long getInt64(int index) throws BufferBoundsException
    162     {
    163         checkBounds(index, 8);
    164 
    165         if (_isMotorolaByteOrder) {
    166             // Motorola - MSB first
    167             return ((long)_buffer[index    ] << 56 & 0xFF00000000000000L) |
    168                    ((long)_buffer[index + 1] << 48 & 0xFF000000000000L) |
    169                    ((long)_buffer[index + 2] << 40 & 0xFF0000000000L) |
    170                    ((long)_buffer[index + 3] << 32 & 0xFF00000000L) |
    171                    ((long)_buffer[index + 4] << 24 & 0xFF000000L) |
    172                    ((long)_buffer[index + 5] << 16 & 0xFF0000L) |
    173                    ((long)_buffer[index + 6] << 8  & 0xFF00L) |
    174                    ((long)_buffer[index + 7]       & 0xFFL);
    175         } else {
    176             // Intel ordering - LSB first
    177             return ((long)_buffer[index + 7] << 56 & 0xFF00000000000000L) |
    178                    ((long)_buffer[index + 6] << 48 & 0xFF000000000000L) |
    179                    ((long)_buffer[index + 5] << 40 & 0xFF0000000000L) |
    180                    ((long)_buffer[index + 4] << 32 & 0xFF00000000L) |
    181                    ((long)_buffer[index + 3] << 24 & 0xFF000000L) |
    182                    ((long)_buffer[index + 2] << 16 & 0xFF0000L) |
    183                    ((long)_buffer[index + 1] << 8  & 0xFF00L) |
    184                    ((long)_buffer[index    ]       & 0xFFL);
    185         }
    186     }
    187 
    188     @Override
    189     public float getS15Fixed16(int index) throws BufferBoundsException
    190     {
    191         checkBounds(index, 4);
    192 
    193         if (_isMotorolaByteOrder) {
    194             float res = (_buffer[index    ] & 255) << 8 |
    195                         (_buffer[index + 1] & 255);
    196             int d =     (_buffer[index + 2] & 255) << 8 |
    197                         (_buffer[index + 3] & 255);
    198             return (float)(res + d/65536.0);
    199         } else {
    200             // this particular branch is untested
    201             float res = (_buffer[index + 3] & 255) << 8 |
    202                         (_buffer[index + 2] & 255);
    203             int d =     (_buffer[index + 1] & 255) << 8 |
    204                         (_buffer[index    ] & 255);
    205             return (float)(res + d/65536.0);
    206         }
    207     }
    208 
    209     @Override
    210     public float getFloat32(int index) throws BufferBoundsException
    211     {
    212         return Float.intBitsToFloat(getInt32(index));
    213     }
    214 
    215     @Override
    216     public double getDouble64(int index) throws BufferBoundsException
    217     {
    218         return Double.longBitsToDouble(getInt64(index));
    219     }
    220    
    221     @Override
    222     @NotNull
    223     public byte[] getBytes(int index, int count) throws BufferBoundsException
    224     {
    225         checkBounds(index, count);
     83        validateIndex(index, count);
    22684
    22785        byte[] bytes = new byte[count];
     
    22987        return bytes;
    23088    }
    231 
    232     @Override
    233     @NotNull
    234     public String getString(int index, int bytesRequested) throws BufferBoundsException
    235     {
    236         return new String(getBytes(index, bytesRequested));
    237     }
    238 
    239     @Override
    240     @NotNull
    241     public String getString(int index, int bytesRequested, String charset) throws BufferBoundsException
    242     {
    243         byte[] bytes = getBytes(index, bytesRequested);
    244         try {
    245             return new String(bytes, charset);
    246         } catch (UnsupportedEncodingException e) {
    247             return new String(bytes);
    248         }
    249     }
    250 
    251     @Override
    252     @NotNull
    253     public String getNullTerminatedString(int index, int maxLengthBytes) throws BufferBoundsException
    254     {
    255         // NOTE currently only really suited to single-byte character strings
    256 
    257         checkBounds(index, maxLengthBytes);
    258 
    259         // Check for null terminators
    260         int length = 0;
    261         while ((index + length) < _buffer.length && _buffer[index + length] != '\0' && length < maxLengthBytes)
    262             length++;
    263 
    264         byte[] bytes = getBytes(index, length);
    265         return new String(bytes);
    266     }
    267 
    268     private void checkBounds(final int index, final int bytesRequested) throws BufferBoundsException
    269     {
    270         if (bytesRequested < 0 || index < 0 || (long)index + (long)bytesRequested - 1L >= (long)_buffer.length)
    271             throw new BufferBoundsException(_buffer, index, bytesRequested);
    272     }
    27389}
  • trunk/src/com/drew/lang/CompoundException.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.lang;
     
    3232 * of these previous JDK versions.
    3333 *
    34  * @author Drew Noakes http://drewnoakes.com
     34 * @author Drew Noakes https://drewnoakes.com
    3535 */
    3636public class CompoundException extends Exception
     
    6363    }
    6464
     65    @Override
    6566    @NotNull
    6667    public String toString()
     
    7778    }
    7879
     80    @Override
    7981    public void printStackTrace(@NotNull PrintStream s)
    8082    {
     
    8688    }
    8789
     90    @Override
    8891    public void printStackTrace(@NotNull PrintWriter s)
    8992    {
     
    9598    }
    9699
     100    @Override
    97101    public void printStackTrace()
    98102    {
  • trunk/src/com/drew/lang/GeoLocation.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
     
    2525import com.drew.lang.annotations.Nullable;
    2626
     27import java.text.DecimalFormat;
     28
    2729/**
    2830 * Represents a latitude and longitude pair, giving a position on earth in spherical coordinates.
     31 * <p>
    2932 * Values of latitude and longitude are given in degrees.
     33 * <p>
    3034 * This type is immutable.
    3135 */
     
    7983    {
    8084        double[] dms = decimalToDegreesMinutesSeconds(decimal);
    81         return dms[0] + "° " + dms[1] + "' " + dms[2] + '"';
     85        DecimalFormat format = new DecimalFormat("0.##");
     86        return String.format("%s° %s' %s\"", format.format(dms[0]), format.format(dms[1]), format.format(dms[2]));
    8287    }
    8388
  • trunk/src/com/drew/lang/NullOutputStream.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.lang;
     
    2727 * An implementation of OutputSteam that ignores write requests by doing nothing.  This class may be useful in tests.
    2828 *
    29  * @author Drew Noakes http://drewnoakes.com
     29 * @author Drew Noakes https://drewnoakes.com
    3030 */
    3131public class NullOutputStream extends OutputStream
     
    3636    }
    3737
     38    @Override
    3839    public void write(int b) throws IOException
    3940    {
  • trunk/src/com/drew/lang/Rational.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
     
    2929/**
    3030 * Immutable class for holding a rational number without loss of precision.  Provides
    31  * a familiar representation via toString() in form <code>numerator/denominator</code>.
    32  *
    33  * @author Drew Noakes http://drewnoakes.com
     31 * a familiar representation via {@link Rational#toString} in form <code>numerator/denominator</code>.
     32 *
     33 * Note that any value with a numerator of zero will be treated as zero, even if the
     34 * denominator is also zero.
     35 *
     36 * @author Drew Noakes https://drewnoakes.com
    3437 */
    3538public class Rational extends java.lang.Number implements Serializable
     
    6164     *         to type <code>double</code>.
    6265     */
     66    @Override
    6367    public double doubleValue()
    6468    {
    65         return (double) _numerator / (double) _denominator;
     69        return _numerator == 0
     70            ? 0.0
     71            : (double) _numerator / (double) _denominator;
    6672    }
    6773
     
    7379     *         to type <code>float</code>.
    7480     */
     81    @Override
    7582    public float floatValue()
    7683    {
    77         return (float) _numerator / (float) _denominator;
     84        return _numerator == 0
     85            ? 0.0f
     86            : (float) _numerator / (float) _denominator;
    7887    }
    7988
     
    8190     * Returns the value of the specified number as a <code>byte</code>.
    8291     * This may involve rounding or truncation.  This implementation simply
    83      * casts the result of <code>doubleValue()</code> to <code>byte</code>.
     92     * casts the result of {@link Rational#doubleValue} to <code>byte</code>.
    8493     *
    8594     * @return the numeric value represented by this object after conversion
    8695     *         to type <code>byte</code>.
    8796     */
     97    @Override
    8898    public final byte byteValue()
    8999    {
     
    94104     * Returns the value of the specified number as an <code>int</code>.
    95105     * This may involve rounding or truncation.  This implementation simply
    96      * casts the result of <code>doubleValue()</code> to <code>int</code>.
     106     * casts the result of {@link Rational#doubleValue} to <code>int</code>.
    97107     *
    98108     * @return the numeric value represented by this object after conversion
    99109     *         to type <code>int</code>.
    100110     */
     111    @Override
    101112    public final int intValue()
    102113    {
     
    107118     * Returns the value of the specified number as a <code>long</code>.
    108119     * This may involve rounding or truncation.  This implementation simply
    109      * casts the result of <code>doubleValue()</code> to <code>long</code>.
     120     * casts the result of {@link Rational#doubleValue} to <code>long</code>.
    110121     *
    111122     * @return the numeric value represented by this object after conversion
    112123     *         to type <code>long</code>.
    113124     */
     125    @Override
    114126    public final long longValue()
    115127    {
     
    120132     * Returns the value of the specified number as a <code>short</code>.
    121133     * This may involve rounding or truncation.  This implementation simply
    122      * casts the result of <code>doubleValue()</code> to <code>short</code>.
     134     * casts the result of {@link Rational#doubleValue} to <code>short</code>.
    123135     *
    124136     * @return the numeric value represented by this object after conversion
    125137     *         to type <code>short</code>.
    126138     */
     139    @Override
    127140    public final short shortValue()
    128141    {
     
    154167    }
    155168
    156     /** Checks if this rational number is an Integer, either positive or negative. */
     169    /** Checks if this {@link Rational} number is an Integer, either positive or negative. */
    157170    public boolean isInteger()
    158171    {
     
    167180     * @return a string representation of the object.
    168181     */
     182    @Override
    169183    @NotNull
    170184    public String toString()
     
    173187    }
    174188
    175     /** Returns the simplest representation of this Rational's value possible. */
     189    /** Returns the simplest representation of this {@link Rational}'s value possible. */
    176190    @NotNull
    177191    public String toSimpleString(boolean allowDecimal)
     
    211225
    212226    /**
    213      * Compares two <code>Rational</code> instances, returning true if they are mathematically
     227     * Compares two {@link Rational} instances, returning true if they are mathematically
    214228     * equivalent.
    215229     *
    216      * @param obj the Rational to compare this instance to.
     230     * @param obj the {@link Rational} to compare this instance to.
    217231     * @return true if instances are mathematically equivalent, otherwise false.  Will also
    218      *         return false if <code>obj</code> is not an instance of <code>Rational</code>.
     232     *         return false if <code>obj</code> is not an instance of {@link Rational}.
    219233     */
    220234    @Override
     
    235249    /**
    236250     * <p>
    237      * Simplifies the Rational number.</p>
     251     * Simplifies the {@link Rational} number.</p>
    238252     * <p>
    239253     * Prime number series: 1, 2, 3, 5, 7, 9, 11, 13, 17</p>
     
    244258     * <p>
    245259     * However, generating the prime number series seems to be a hefty task.  Perhaps
    246      * it's simpler to check if both d & n are divisible by all numbers from 2 ->
     260     * it's simpler to check if both d &amp; n are divisible by all numbers from 2 {@literal ->}
    247261     * (Math.min(denominator, numerator) / 2).  In doing this, one can check for 2
    248262     * and 5 once, then ignore all even numbers, and all numbers ending in 0 or 5.
     
    250264     * <p>
    251265     * Therefore, the max number of pairs of modulus divisions required will be:</p>
    252      * <code><pre>
     266     * <pre><code>
    253267     *    4   Math.min(denominator, numerator) - 1
    254268     *   -- * ------------------------------------ + 2
    255269     *   10                    2
    256      * <p/>
     270     *
    257271     *   Math.min(denominator, numerator) - 1
    258272     * = ------------------------------------ + 2
    259273     *                  5
    260      * </pre></code>
     274     * </code></pre>
    261275     *
    262276     * @return a simplified instance, or if the Rational could not be simplified,
  • trunk/src/com/drew/lang/StringUtil.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
     
    2323
    2424import com.drew.lang.annotations.NotNull;
     25import com.drew.lang.annotations.Nullable;
    2526
     27import java.io.BufferedReader;
     28import java.io.IOException;
     29import java.io.InputStream;
     30import java.io.InputStreamReader;
    2631import java.util.Iterator;
    2732
    28 /** @author Drew Noakes http://drewnoakes.com */
     33/**
     34 * @author Drew Noakes https://drewnoakes.com
     35 */
    2936public class StringUtil
    3037{
     38    @NotNull
    3139    public static String join(@NotNull Iterable<? extends CharSequence> strings, @NotNull String delimiter)
    3240    {
     
    5058    }
    5159
     60    @NotNull
    5261    public static <T extends CharSequence> String join(@NotNull T[] strings, @NotNull String delimiter)
    5362    {
     
    6978        return buffer.toString();
    7079    }
     80
     81    @NotNull
     82    public static String fromStream(@NotNull InputStream stream) throws IOException
     83    {
     84        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
     85        StringBuilder sb = new StringBuilder();
     86        String line;
     87        while ((line = reader.readLine()) != null) {
     88            sb.append(line);
     89        }
     90        return sb.toString();
     91    }
     92
     93    public static int compare(@Nullable String s1, @Nullable String s2)
     94    {
     95        boolean null1 = s1 == null;
     96        boolean null2 = s2 == null;
     97
     98        if (null1 && null2) {
     99            return 0;
     100        } else if (null1) {
     101            return -1;
     102        } else if (null2) {
     103            return 1;
     104        } else {
     105            return s1.compareTo(s2);
     106        }
     107    }
     108
     109    @NotNull
     110    public static String urlEncode(@NotNull String name)
     111    {
     112        // Sufficient for now, it seems
     113        return name.replace(" ", "%20");
     114    }
    71115}
  • trunk/src/com/drew/lang/annotations/NotNull.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
     
    2323
    2424/**
    25  * @author Drew Noakes http://drewnoakes.com
     25 * @author Drew Noakes https://drewnoakes.com
    2626 */
    2727public @interface NotNull
  • trunk/src/com/drew/lang/annotations/Nullable.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
     
    2323
    2424/**
    25  * @author Drew Noakes http://drewnoakes.com
     25 * @author Drew Noakes https://drewnoakes.com
    2626 */
    2727public @interface Nullable
  • trunk/src/com/drew/lang/annotations/SuppressWarnings.java

    r6127 r8132  
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
  • trunk/src/com/drew/metadata/Age.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
     
    2727/**
    2828 * Represents an age in years, months, days, hours, minutes and seconds.
    29  * <p/>
     29 * <p>
    3030 * Used by certain Panasonic cameras which have face recognition features.
    3131 *
    32  * @author Drew Noakes http://drewnoakes.com
     32 * @author Drew Noakes https://drewnoakes.com
    3333 */
    3434public class Age
    3535{
    36     private int _years;
    37     private int _months;
    38     private int _days;
    39     private int _hours;
    40     private int _minutes;
    41     private int _seconds;
     36    private final int _years;
     37    private final int _months;
     38    private final int _days;
     39    private final int _hours;
     40    private final int _minutes;
     41    private final int _seconds;
    4242
    4343    /**
    4444     * Parses an age object from the string format used by Panasonic cameras:
    4545     * <code>0031:07:15 00:00:00</code>
     46     *
    4647     * @param s The String in format <code>0031:07:15 00:00:00</code>.
    4748     * @return The parsed Age object, or null if the value could not be parsed
  • trunk/src/com/drew/metadata/DefaultTagDescriptor.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata;
     
    2828 * and gives descriptions using the default string representation of the value.
    2929 *
    30  * @author Drew Noakes http://drewnoakes.com
     30 * @author Drew Noakes https://drewnoakes.com
    3131 */
    3232public class DefaultTagDescriptor extends TagDescriptor<Directory>
  • trunk/src/com/drew/metadata/Directory.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata;
     
    3737 * data types.
    3838 *
    39  * @author Drew Noakes http://drewnoakes.com
     39 * @author Drew Noakes https://drewnoakes.com
    4040 */
    4141public abstract class Directory
    4242{
    43     // TODO get Array methods need to return cloned data, to maintain this directory's integrity
    44 
    4543    /** Map of values hashed by type identifiers. */
    4644    @NotNull
     
    104102    public Collection<Tag> getTags()
    105103    {
    106         return _definedTagList;
     104        return Collections.unmodifiableCollection(_definedTagList);
    107105    }
    108106
     
    158156    public Iterable<String> getErrors()
    159157    {
    160         return _errorList;
     158        return Collections.unmodifiableCollection(_errorList);
    161159    }
    162160
     
    414412            return null;
    415413
    416         if (o instanceof String) {
     414        if (o instanceof Number) {
     415            return ((Number)o).intValue();
     416        } else if (o instanceof String) {
    417417            try {
    418418                return Integer.parseInt((String)o);
     
    428428                return (int)val;
    429429            }
    430         } else if (o instanceof Number) {
    431             return ((Number)o).intValue();
    432430        } else if (o instanceof Rational[]) {
    433431            Rational[] rationals = (Rational[])o;
     
    498496        if (o == null)
    499497            return null;
     498        if (o instanceof int[])
     499            return (int[])o;
    500500        if (o instanceof Rational[]) {
    501501            Rational[] rationals = (Rational[])o;
     
    506506            return ints;
    507507        }
    508         if (o instanceof int[])
    509             return (int[])o;
     508        if (o instanceof short[]) {
     509            short[] shorts = (short[])o;
     510            int[] ints = new int[shorts.length];
     511            for (int i = 0; i < shorts.length; i++) {
     512                ints[i] = shorts[i];
     513            }
     514            return ints;
     515        }
    510516        if (o instanceof byte[]) {
    511517            byte[] bytes = (byte[])o;
    512518            int[] ints = new int[bytes.length];
    513519            for (int i = 0; i < bytes.length; i++) {
    514                 byte b = bytes[i];
    515                 ints[i] = b;
     520                ints[i] = bytes[i];
    516521            }
    517522            return ints;
     
    527532        if (o instanceof Integer)
    528533            return new int[] { (Integer)o };
    529        
     534
    530535        return null;
    531536    }
     
    560565            }
    561566            return bytes;
     567        } else if (o instanceof short[]) {
     568            short[] shorts = (short[])o;
     569            byte[] bytes = new byte[shorts.length];
     570            for (int i = 0; i < shorts.length; i++) {
     571                bytes[i] = (byte)shorts[i];
     572            }
     573            return bytes;
    562574        } else if (o instanceof CharSequence) {
    563575            CharSequence str = (CharSequence)o;
     
    703715    /**
    704716     * Returns the specified tag's value as a java.util.Date.  If the value is unset or cannot be converted, <code>null</code> is returned.
    705      * <p/>
     717     * <p>
    706718     * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in
    707719     * the current {@link TimeZone}.  If the {@link TimeZone} is known, call the overload that accepts one as an argument.
     
    712724        return getDate(tagType, null);
    713725    }
    714    
     726
    715727    /**
    716728     * Returns the specified tag's value as a java.util.Date.  If the value is unset or cannot be converted, <code>null</code> is returned.
    717      * <p/>
     729     * <p>
    718730     * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in
    719731     * the {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null).  Note that this parameter
     
    818830            boolean isLongArray = componentType.getName().equals("long");
    819831            boolean isByteArray = componentType.getName().equals("byte");
     832            boolean isShortArray = componentType.getName().equals("short");
    820833            StringBuilder string = new StringBuilder();
    821834            for (int i = 0; i < arrayLength; i++) {
     
    826839                else if (isIntArray)
    827840                    string.append(Array.getInt(o, i));
     841                else if (isShortArray)
     842                    string.append(Array.getShort(o, i));
    828843                else if (isLongArray)
    829844                    string.append(Array.getLong(o, i));
     
    896911
    897912    /**
     913     * Gets whether the specified tag is known by the directory and has a name.
     914     *
     915     * @param tagType the tag type identifier
     916     * @return whether this directory has a name for the specified tag
     917     */
     918    public boolean hasTagName(int tagType)
     919    {
     920        return getTagNameMap().containsKey(tagType);
     921    }
     922
     923    /**
    898924     * Provides a description of a tag's value using the descriptor set by
    899925     * <code>setDescriptor(Descriptor)</code>.
     
    908934        return _descriptor.getDescription(tagType);
    909935    }
     936
     937    @Override
     938    public String toString()
     939    {
     940        return String.format("%s Directory (%d %s)",
     941            getName(),
     942            _tagMap.size(),
     943            _tagMap.size() == 1
     944                ? "tag"
     945                : "tags");
     946    }
    910947}
  • trunk/src/com/drew/metadata/Face.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata;
     
    2626/**
    2727 * Class to hold information about a detected or recognized face in a photo.
    28  * <p/>
     28 * <p>
    2929 * When a face is <em>detected</em>, the camera believes that a face is present at a given location in
    3030 * the image, but is not sure whose face it is.  When a face is <em>recognised</em>, then the face is
     
    116116    }
    117117
     118    @Override
    118119    @NotNull
    119120    public String toString()
  • trunk/src/com/drew/metadata/Metadata.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata;
     
    2424import com.drew.lang.annotations.Nullable;
    2525
    26 import java.util.ArrayList;
    27 import java.util.Collection;
    28 import java.util.HashMap;
    29 import java.util.Map;
     26import java.util.*;
    3027
    3128/**
    32  * A top-level object to hold the various types of metadata (Exif/IPTC/etc) related to one entity (such as a file
    33  * or stream).
    34  * <p/>
    35  * Metadata objects may contain zero or more directories.  Each directory may contain zero or more tags with
    36  * corresponding values.
     29 * A top-level object that holds the metadata values extracted from an image.
     30 * <p>
     31 * Metadata objects may contain zero or more {@link Directory} objects.  Each directory may contain zero or more tags
     32 * with corresponding values.
    3733 *
    38  * @author Drew Noakes http://drewnoakes.com
     34 * @author Drew Noakes https://drewnoakes.com
    3935 */
    4036public final class Metadata
     
    4238    @NotNull
    4339    private final Map<Class<? extends Directory>,Directory> _directoryByClass = new HashMap<Class<? extends Directory>, Directory>();
    44    
     40
    4541    /**
    4642     * List of Directory objects set against this object.  Keeping a list handy makes
     
    5854    public Iterable<Directory> getDirectories()
    5955    {
    60         return _directoryList;
     56        return Collections.unmodifiableCollection(_directoryList);
    6157    }
    6258
     
    7268
    7369    /**
    74      * Returns a <code>Directory</code> of specified type.  If this <code>Metadata</code> object already contains
     70     * Returns a {@link Directory} of specified type.  If this {@link Metadata} object already contains
    7571     * such a directory, it is returned.  Otherwise a new instance of this directory will be created and stored within
    76      * this Metadata object.
     72     * this {@link Metadata} object.
    7773     *
    7874     * @param type the type of the Directory implementation required.
     
    104100
    105101    /**
    106      * If this <code>Metadata</code> object contains a <code>Directory</code> of the specified type, it is returned.
     102     * If this {@link Metadata} object contains a {@link Directory} of the specified type, it is returned.
    107103     * Otherwise <code>null</code> is returned.
    108104     *
    109105     * @param type the Directory type
    110106     * @param <T> the Directory type
    111      * @return a Directory of type T if it exists in this Metadata object, otherwise <code>null</code>.
     107     * @return a Directory of type T if it exists in this {@link Metadata} object, otherwise <code>null</code>.
    112108     */
    113109    @Nullable
     
    123119    /**
    124120     * Indicates whether a given directory type has been created in this metadata
    125      * repository.  Directories are created by calling <code>getOrCreateDirectory(Class)</code>.
     121     * repository.  Directories are created by calling {@link Metadata#getOrCreateDirectory(Class)}.
    126122     *
    127      * @param type the Directory type
    128      * @return true if the metadata directory has been created
     123     * @param type the {@link Directory} type
     124     * @return true if the {@link Directory} has been created
    129125     */
    130126    public boolean containsDirectory(Class<? extends Directory> type)
     
    135131    /**
    136132     * Indicates whether any errors were reported during the reading of metadata values.
    137      * This value will be true if Directory.hasErrors() is true for one of the contained Directory objects.
     133     * This value will be true if Directory.hasErrors() is true for one of the contained {@link Directory} objects.
    138134     *
    139135     * @return whether one of the contained directories has an error
     
    147143        return false;
    148144    }
     145
     146    @Override
     147    public String toString()
     148    {
     149        return String.format("Metadata (%d %s)",
     150            _directoryList.size(),
     151            _directoryList.size() == 1
     152                ? "directory"
     153                : "directories");
     154    }
    149155}
  • trunk/src/com/drew/metadata/MetadataException.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata;
     
    2727 * Base class for all metadata specific exceptions.
    2828 *
    29  * @author Drew Noakes http://drewnoakes.com
     29 * @author Drew Noakes https://drewnoakes.com
    3030 */
    3131public class MetadataException extends CompoundException
  • trunk/src/com/drew/metadata/MetadataReader.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata;
    2222
    23 import com.drew.lang.BufferReader;
     23import com.drew.lang.RandomAccessReader;
    2424import com.drew.lang.annotations.NotNull;
    2525
    2626/**
    27  * Interface through which all classes responsible for decoding a particular type of metadata may be called.
    28  * Note that the data source is not specified on this interface.  Instead it is suggested that implementations
    29  * take their data within a constructor.  Constructors might be overloaded to allow for different sources, such as
    30  * files, streams and byte arrays.  As such, instances of implementations of this interface would be single-use and
    31  * not thread-safe.
     27 * Defines an object capable of processing a particular type of metadata from a {@link RandomAccessReader}.
     28 * <p>
     29 * Instances of this interface must be thread-safe and reusable.
    3230 *
    33  * @author Drew Noakes http://drewnoakes.com
     31 * @author Drew Noakes https://drewnoakes.com
    3432 */
    3533public interface MetadataReader
    3634{
    3735    /**
    38      * Extract metadata from the source and merge it into an existing Metadata object.
     36     * Extracts metadata from <code>reader</code> and merges it into the specified {@link Metadata} object.
    3937     *
    40      * @param reader   The reader from which the metadata should be extracted.
    41      * @param metadata The Metadata object into which extracted values should be merged.
     38     * @param reader   The {@link RandomAccessReader} from which the metadata should be extracted.
     39     * @param metadata The {@link Metadata} object into which extracted values should be merged.
    4240     */
    43     public void extract(@NotNull final BufferReader reader, @NotNull final Metadata metadata);
     41    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata);
    4442}
  • trunk/src/com/drew/metadata/Tag.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata;
     
    2525
    2626/**
    27  * Models a particular tag within a directory and provides methods for obtaining its value.  Note that a Tag instance is
    28  * specific to a particular metadata extraction and cannot be reused.
     27 * Models a particular tag within a {@link com.drew.metadata.Directory} and provides methods for obtaining its value.
     28 * Immutable.
    2929 *
    30  * @author Drew Noakes http://drewnoakes.com
     30 * @author Drew Noakes https://drewnoakes.com
    3131 */
    3232public class Tag
     
    7979
    8080    /**
     81     * Get whether this tag has a name.
     82     *
     83     * If <code>true</code>, it may be accessed via {@link #getTagName}.
     84     * If <code>false</code>, {@link #getTagName} will return a string resembling <code>"Unknown tag (0x1234)"</code>.
     85     *
     86     * @return whether this tag has a name
     87     */
     88    @NotNull
     89    public boolean hasTagName()
     90    {
     91        return _directory.hasTagName(_tagType);
     92    }
     93
     94    /**
    8195     * Get the name of the tag, such as <code>Aperture</code>, or
    8296     * <code>InteropVersion</code>.
     
    91105
    92106    /**
    93      * Get the name of the directory in which the tag exists, such as
     107     * Get the name of the {@link com.drew.metadata.Directory} in which the tag exists, such as
    94108     * <code>Exif</code>, <code>GPS</code> or <code>Interoperability</code>.
    95109     *
    96      * @return name of the directory in which this tag exists
     110     * @return name of the {@link com.drew.metadata.Directory} in which this tag exists
    97111     */
    98112    @NotNull
     
    107121     * @return the tag's type and value
    108122     */
     123    @Override
    109124    @NotNull
    110125    public String toString()
    111126    {
    112127        String description = getDescription();
    113         if (description==null)
     128        if (description == null)
    114129            description = _directory.getString(getTagType()) + " (unable to formulate description)";
    115130        return "[" + _directory.getName() + "] " + getTagName() + " - " + description;
  • trunk/src/com/drew/metadata/TagDescriptor.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata;
    2222
     23import com.drew.lang.Rational;
     24import com.drew.lang.StringUtil;
    2325import com.drew.lang.annotations.NotNull;
    2426import com.drew.lang.annotations.Nullable;
    2527
     28import java.io.UnsupportedEncodingException;
    2629import java.lang.reflect.Array;
     30import java.util.ArrayList;
     31import java.util.Date;
     32import java.util.List;
    2733
    2834/**
    29  * Abstract base class for all tag descriptor classes.  Implementations are responsible for
     35 * Base class for all tag descriptor classes.  Implementations are responsible for
    3036 * providing the human-readable string representation of tag values stored in a directory.
    3137 * The directory is provided to the tag descriptor via its constructor.
    3238 *
    33  * @author Drew Noakes http://drewnoakes.com
     39 * @author Drew Noakes https://drewnoakes.com
    3440 */
    35 public abstract class TagDescriptor<T extends Directory>
     41public class TagDescriptor<T extends Directory>
    3642{
    3743    @NotNull
     
    4450
    4551    /**
    46      * Returns a descriptive value of the the specified tag for this image.
     52     * Returns a descriptive value of the specified tag for this image.
    4753     * Where possible, known values will be substituted here in place of the raw
    4854     * tokens actually kept in the metadata segment.  If no substitution is
    4955     * available, the value provided by <code>getString(tagType)</code> will be returned.
    50      * 
     56     *
    5157     * @param tagType the tag to find a description for
    5258     * @return a description of the image's value for the specified tag, or
     
    5864        Object object = _directory.getObject(tagType);
    5965
    60         if (object==null)
     66        if (object == null)
    6167            return null;
    6268
     
    6672            if (length > 16) {
    6773                final String componentTypeName = object.getClass().getComponentType().getName();
    68                 return String.format("[%d %s%s]", length, componentTypeName, length==1 ? "" : "s");
     74                return String.format("[%d %s%s]", length, componentTypeName, length == 1 ? "" : "s");
    6975            }
    7076        }
     
    7783     * Takes a series of 4 bytes from the specified offset, and converts these to a
    7884     * well-known version number, where possible.
    79      * <p/>
     85     * <p>
    8086     * Two different formats are processed:
    8187     * <ul>
    82      *     <li>[30 32 31 30] -&gt; 2.10</li>
    83      *     <li>[0 1 0 0] -&gt; 1.00</li>
     88     * <li>[30 32 31 30] -&gt; 2.10</li>
     89     * <li>[0 1 0 0] -&gt; 1.00</li>
    8490     * </ul>
    85      * @param components the four version values
    86      * @param majorDigits the number of components to be
     91     *
     92     * @param components  the four version values
     93     * @param majorDigits the number of components to be
    8794     * @return the version as a string of form "2.10" or null if the argument cannot be converted
    8895     */
     
    9097    public static String convertBytesToVersionString(@Nullable int[] components, final int majorDigits)
    9198    {
    92         if (components==null)
     99        if (components == null)
    93100            return null;
    94101        StringBuilder version = new StringBuilder();
     
    99106            if (c < '0')
    100107                c += '0';
    101             if (i == 0 && c=='0')
     108            if (i == 0 && c == '0')
    102109                continue;
    103110            version.append(c);
     
    105112        return version.toString();
    106113    }
     114
     115    @Nullable
     116    protected String getVersionBytesDescription(final int tagType, int majorDigits)
     117    {
     118        int[] values = _directory.getIntArray(tagType);
     119        return values == null ? null : convertBytesToVersionString(values, majorDigits);
     120    }
     121
     122    @Nullable
     123    protected String getIndexedDescription(final int tagType, @NotNull String... descriptions)
     124    {
     125        return getIndexedDescription(tagType, 0, descriptions);
     126    }
     127
     128    @Nullable
     129    protected String getIndexedDescription(final int tagType, final int baseIndex, @NotNull String... descriptions)
     130    {
     131        final Integer index = _directory.getInteger(tagType);
     132        if (index == null)
     133            return null;
     134        final int arrayIndex = index - baseIndex;
     135        if (arrayIndex >= 0 && arrayIndex < descriptions.length) {
     136            String description = descriptions[arrayIndex];
     137            if (description != null)
     138                return description;
     139        }
     140        return "Unknown (" + index + ")";
     141    }
     142
     143    @Nullable
     144    protected String getByteLengthDescription(final int tagType)
     145    {
     146        byte[] bytes = _directory.getByteArray(tagType);
     147        if (bytes == null)
     148            return null;
     149        return String.format("(%d byte%s)", bytes.length, bytes.length == 1 ? "" : "s");
     150    }
     151
     152    @Nullable
     153    protected String getSimpleRational(final int tagType)
     154    {
     155        Rational value = _directory.getRational(tagType);
     156        if (value == null)
     157            return null;
     158        return value.toSimpleString(true);
     159    }
     160
     161    @Nullable
     162    protected String getDecimalRational(final int tagType, final int decimalPlaces)
     163    {
     164        Rational value = _directory.getRational(tagType);
     165        if (value == null)
     166            return null;
     167        return String.format("%." + decimalPlaces + "f", value.doubleValue());
     168    }
     169
     170    @Nullable
     171    protected String getFormattedInt(final int tagType, @NotNull final String format)
     172    {
     173        Integer value = _directory.getInteger(tagType);
     174        if (value == null)
     175            return null;
     176        return String.format(format, value);
     177    }
     178
     179    @Nullable
     180    protected String getFormattedFloat(final int tagType, @NotNull final String format)
     181    {
     182        Float value = _directory.getFloatObject(tagType);
     183        if (value == null)
     184            return null;
     185        return String.format(format, value);
     186    }
     187
     188    @Nullable
     189    protected String getFormattedString(final int tagType, @NotNull final String format)
     190    {
     191        String value = _directory.getString(tagType);
     192        if (value == null)
     193            return null;
     194        return String.format(format, value);
     195    }
     196
     197    @Nullable
     198    protected String getEpochTimeDescription(final int tagType)
     199    {
     200        // TODO have observed a byte[8] here which is likely some kind of date (ticks as long?)
     201        Long value = _directory.getLongObject(tagType);
     202        if (value==null)
     203            return null;
     204        return new Date(value).toString();
     205    }
     206
     207    /**
     208     * LSB first. Labels may be null, a String, or a String[2] with (low label,high label) values.
     209     */
     210    @Nullable
     211    protected String getBitFlagDescription(final int tagType, @NotNull final Object... labels)
     212    {
     213        Integer value = _directory.getInteger(tagType);
     214
     215        if (value == null)
     216            return null;
     217
     218        List<String> parts = new ArrayList<String>();
     219
     220        int bitIndex = 0;
     221        while (labels.length > bitIndex) {
     222            Object labelObj = labels[bitIndex];
     223            if (labelObj != null) {
     224                boolean isBitSet = (value & 1) == 1;
     225                if (labelObj instanceof String[]) {
     226                    String[] labelPair = (String[])labelObj;
     227                    assert(labelPair.length == 2);
     228                    parts.add(labelPair[isBitSet ? 1 : 0]);
     229                } else if (isBitSet && labelObj instanceof String) {
     230                    parts.add((String)labelObj);
     231                }
     232            }
     233            value >>= 1;
     234            bitIndex++;
     235        }
     236
     237        return StringUtil.join(parts, ", ");
     238    }
     239
     240    @Nullable
     241    protected String get7BitStringFromBytes(final int tagType)
     242    {
     243        final byte[] bytes = _directory.getByteArray(tagType);
     244
     245        if (bytes == null)
     246            return null;
     247
     248        int length = bytes.length;
     249        for (int index = 0; index < bytes.length; index++) {
     250            int i = bytes[index] & 0xFF;
     251            if (i == 0 || i > 0x7F) {
     252                length = index;
     253                break;
     254            }
     255        }
     256
     257        return new String(bytes, 0, length);
     258    }
     259
     260    @Nullable
     261    protected String getAsciiStringFromBytes(int tag)
     262    {
     263        byte[] values = _directory.getByteArray(tag);
     264
     265        if (values == null)
     266            return null;
     267
     268        try {
     269            return new String(values, "ASCII").trim();
     270        } catch (UnsupportedEncodingException e) {
     271            return null;
     272        }
     273    }
    107274}
  • trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
     
    2929import java.io.UnsupportedEncodingException;
    3030
     31import static com.drew.metadata.exif.ExifIFD0Directory.*;
     32
    3133/**
    32  * Provides human-readable string representations of tag values stored in a <code>ExifIFD0Directory</code>.
    33  *
    34  * @author Drew Noakes http://drewnoakes.com
     34 * Provides human-readable string representations of tag values stored in a {@link ExifIFD0Directory}.
     35 *
     36 * @author Drew Noakes https://drewnoakes.com
    3537 */
    3638public class ExifIFD0Descriptor extends TagDescriptor<ExifIFD0Directory>
     
    5456
    5557    /**
    56      * Returns a descriptive value of the the specified tag for this image.
     58     * Returns a descriptive value of the specified tag for this image.
    5759     * Where possible, known values will be substituted here in place of the raw
    5860     * tokens actually kept in the Exif segment.  If no substitution is
     
    6264     *         <code>null</code> if the tag hasn't been defined.
    6365     */
     66    @Override
    6467    @Nullable
    6568    public String getDescription(int tagType)
    6669    {
    6770        switch (tagType) {
    68             case ExifIFD0Directory.TAG_RESOLUTION_UNIT:
     71            case TAG_RESOLUTION_UNIT:
    6972                return getResolutionDescription();
    70             case ExifIFD0Directory.TAG_YCBCR_POSITIONING:
     73            case TAG_YCBCR_POSITIONING:
    7174                return getYCbCrPositioningDescription();
    72             case ExifIFD0Directory.TAG_X_RESOLUTION:
     75            case TAG_X_RESOLUTION:
    7376                return getXResolutionDescription();
    74             case ExifIFD0Directory.TAG_Y_RESOLUTION:
     77            case TAG_Y_RESOLUTION:
    7578                return getYResolutionDescription();
    76             case ExifIFD0Directory.TAG_REFERENCE_BLACK_WHITE:
     79            case TAG_REFERENCE_BLACK_WHITE:
    7780                return getReferenceBlackWhiteDescription();
    78             case ExifIFD0Directory.TAG_ORIENTATION:
     81            case TAG_ORIENTATION:
    7982                return getOrientationDescription();
    8083
    81             case ExifIFD0Directory.TAG_WIN_AUTHOR:
     84            case TAG_WIN_AUTHOR:
    8285               return getWindowsAuthorDescription();
    83             case ExifIFD0Directory.TAG_WIN_COMMENT:
     86            case TAG_WIN_COMMENT:
    8487               return getWindowsCommentDescription();
    85             case ExifIFD0Directory.TAG_WIN_KEYWORDS:
     88            case TAG_WIN_KEYWORDS:
    8689               return getWindowsKeywordsDescription();
    87             case ExifIFD0Directory.TAG_WIN_SUBJECT:
     90            case TAG_WIN_SUBJECT:
    8891               return getWindowsSubjectDescription();
    89             case ExifIFD0Directory.TAG_WIN_TITLE:
     92            case TAG_WIN_TITLE:
    9093               return getWindowsTitleDescription();
    9194
     
    98101    public String getReferenceBlackWhiteDescription()
    99102    {
    100         int[] ints = _directory.getIntArray(ExifIFD0Directory.TAG_REFERENCE_BLACK_WHITE);
    101         if (ints==null)
     103        int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE);
     104        if (ints==null || ints.length < 6)
    102105            return null;
    103106        int blackR = ints[0];
     
    107110        int blackB = ints[4];
    108111        int whiteB = ints[5];
    109         return "[" + blackR + "," + blackG + "," + blackB + "] " +
    110                "[" + whiteR + "," + whiteG + "," + whiteB + "]";
     112        return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB);
    111113    }
    112114
     
    114116    public String getYResolutionDescription()
    115117    {
    116         Rational value = _directory.getRational(ExifIFD0Directory.TAG_Y_RESOLUTION);
     118        Rational value = _directory.getRational(TAG_Y_RESOLUTION);
    117119        if (value==null)
    118120            return null;
    119121        final String unit = getResolutionDescription();
    120         return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
    121                 " dots per " +
    122                 (unit==null ? "unit" : unit.toLowerCase());
     122        return String.format("%s dots per %s",
     123            value.toSimpleString(_allowDecimalRepresentationOfRationals),
     124            unit == null ? "unit" : unit.toLowerCase());
    123125    }
    124126
     
    126128    public String getXResolutionDescription()
    127129    {
    128         Rational value = _directory.getRational(ExifIFD0Directory.TAG_X_RESOLUTION);
     130        Rational value = _directory.getRational(TAG_X_RESOLUTION);
    129131        if (value==null)
    130132            return null;
    131133        final String unit = getResolutionDescription();
    132         return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
    133                 " dots per " +
    134                 (unit==null ? "unit" : unit.toLowerCase());
     134        return String.format("%s dots per %s",
     135            value.toSimpleString(_allowDecimalRepresentationOfRationals),
     136            unit == null ? "unit" : unit.toLowerCase());
    135137    }
    136138
     
    138140    public String getYCbCrPositioningDescription()
    139141    {
    140         Integer value = _directory.getInteger(ExifIFD0Directory.TAG_YCBCR_POSITIONING);
    141         if (value==null)
    142             return null;
    143         switch (value) {
    144             case 1: return "Center of pixel array";
    145             case 2: return "Datum point";
    146             default:
    147                 return String.valueOf(value);
     142        return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point");
     143    }
     144
     145    @Nullable
     146    public String getOrientationDescription()
     147    {
     148        return getIndexedDescription(TAG_ORIENTATION, 1,
     149            "Top, left side (Horizontal / normal)",
     150            "Top, right side (Mirror horizontal)",
     151            "Bottom, right side (Rotate 180)",
     152            "Bottom, left side (Mirror vertical)",
     153            "Left side, top (Mirror horizontal and rotate 270 CW)",
     154            "Right side, top (Rotate 90 CW)",
     155            "Right side, bottom (Mirror horizontal and rotate 90 CW)",
     156            "Left side, bottom (Rotate 270 CW)");
     157    }
     158
     159    @Nullable
     160    public String getResolutionDescription()
     161    {
     162        // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
     163        return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm");
     164    }
     165
     166    /** The Windows specific tags uses plain Unicode. */
     167    @Nullable
     168    private String getUnicodeDescription(int tag)
     169    {
     170        byte[] bytes = _directory.getByteArray(tag);
     171        if (bytes == null)
     172            return null;
     173        try {
     174            // Decode the unicode string and trim the unicode zero "\0" from the end.
     175            return new String(bytes, "UTF-16LE").trim();
     176        } catch (UnsupportedEncodingException ex) {
     177            return null;
    148178        }
    149179    }
    150180
    151181    @Nullable
    152     public String getOrientationDescription()
    153     {
    154         Integer value = _directory.getInteger(ExifIFD0Directory.TAG_ORIENTATION);
    155         if (value==null)
    156             return null;
    157         switch (value) {
    158             case 1: return "Top, left side (Horizontal / normal)";
    159             case 2: return "Top, right side (Mirror horizontal)";
    160             case 3: return "Bottom, right side (Rotate 180)";
    161             case 4: return "Bottom, left side (Mirror vertical)";
    162             case 5: return "Left side, top (Mirror horizontal and rotate 270 CW)";
    163             case 6: return "Right side, top (Rotate 90 CW)";
    164             case 7: return "Right side, bottom (Mirror horizontal and rotate 90 CW)";
    165             case 8: return "Left side, bottom (Rotate 270 CW)";
    166             default:
    167                 return String.valueOf(value);
    168         }
    169     }
    170 
    171     @Nullable
    172     public String getResolutionDescription()
    173     {
    174         // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
    175         Integer value = _directory.getInteger(ExifIFD0Directory.TAG_RESOLUTION_UNIT);
    176         if (value==null)
    177             return null;
    178         switch (value) {
    179             case 1: return "(No unit)";
    180             case 2: return "Inch";
    181             case 3: return "cm";
    182             default:
    183                 return "";
    184         }
    185     }
    186 
    187     /** The Windows specific tags uses plain Unicode. */
    188     @Nullable
    189     private String getUnicodeDescription(int tag)
    190     {
    191          byte[] commentBytes = _directory.getByteArray(tag);
    192         if (commentBytes==null)
    193             return null;
    194          try {
    195              // Decode the unicode string and trim the unicode zero "\0" from the end.
    196             return new String(commentBytes, "UTF-16LE").trim();
    197          }
    198          catch (UnsupportedEncodingException ex) {
    199             return null;
    200          }
    201     }
    202 
    203     @Nullable
    204182    public String getWindowsAuthorDescription()
    205183    {
    206        return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_AUTHOR);
     184       return getUnicodeDescription(TAG_WIN_AUTHOR);
    207185    }
    208186
     
    210188    public String getWindowsCommentDescription()
    211189    {
    212        return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_COMMENT);
     190       return getUnicodeDescription(TAG_WIN_COMMENT);
    213191    }
    214192
     
    216194    public String getWindowsKeywordsDescription()
    217195    {
    218        return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_KEYWORDS);
     196       return getUnicodeDescription(TAG_WIN_KEYWORDS);
    219197    }
    220198
     
    222200    public String getWindowsTitleDescription()
    223201    {
    224        return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_TITLE);
     202       return getUnicodeDescription(TAG_WIN_TITLE);
    225203    }
    226204
     
    228206    public String getWindowsSubjectDescription()
    229207    {
    230        return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_SUBJECT);
     208       return getUnicodeDescription(TAG_WIN_SUBJECT);
    231209    }
    232210}
  • trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
     
    3030 * Describes Exif tags from the IFD0 directory.
    3131 *
    32  * @author Drew Noakes http://drewnoakes.com
     32 * @author Drew Noakes https://drewnoakes.com
    3333 */
    3434public class ExifIFD0Directory extends Directory
     
    4646    public static final int TAG_WHITE_POINT = 0x013E;
    4747    public static final int TAG_PRIMARY_CHROMATICITIES = 0x013F;
     48
    4849    public static final int TAG_YCBCR_COEFFICIENTS = 0x0211;
    4950    public static final int TAG_YCBCR_POSITIONING = 0x0213;
    5051    public static final int TAG_REFERENCE_BLACK_WHITE = 0x0214;
     52
     53
     54    /** This tag is a pointer to the Exif SubIFD. */
     55    public static final int TAG_EXIF_SUB_IFD_OFFSET = 0x8769;
     56
     57    /** This tag is a pointer to the Exif GPS IFD. */
     58    public static final int TAG_GPS_INFO_OFFSET = 0x8825;
     59
    5160    public static final int TAG_COPYRIGHT = 0x8298;
     61
     62    /** Non-standard, but in use. */
     63    public static final int TAG_TIME_ZONE_OFFSET = 0x882a;
    5264
    5365    /** The image title, as used by Windows XP. */
     
    8294        _tagNameMap.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning");
    8395        _tagNameMap.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White");
     96
    8497        _tagNameMap.put(TAG_COPYRIGHT, "Copyright");
     98
     99        _tagNameMap.put(TAG_TIME_ZONE_OFFSET, "Time Zone Offset");
    85100
    86101        _tagNameMap.put(TAG_WIN_AUTHOR, "Windows XP Author");
     
    96111    }
    97112
     113    @Override
    98114    @NotNull
    99115    public String getName()
     
    102118    }
    103119
     120    @Override
    104121    @NotNull
    105122    protected HashMap<Integer, String> getTagNameMap()
  • trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata.exif;
     
    2525import com.drew.metadata.TagDescriptor;
    2626
     27import static com.drew.metadata.exif.ExifInteropDirectory.*;
     28
    2729/**
    28  * Provides human-readable string representations of tag values stored in a <code>ExifInteropDirectory</code>.
     30 * Provides human-readable string representations of tag values stored in a {@link ExifInteropDirectory}.
    2931 *
    30  * @author Drew Noakes http://drewnoakes.com
     32 * @author Drew Noakes https://drewnoakes.com
    3133 */
    3234public class ExifInteropDescriptor extends TagDescriptor<ExifInteropDirectory>
     
    3739    }
    3840
     41    @Override
    3942    @Nullable
    4043    public String getDescription(int tagType)
    4144    {
    4245        switch (tagType) {
    43             case ExifInteropDirectory.TAG_INTEROP_INDEX:
     46            case TAG_INTEROP_INDEX:
    4447                return getInteropIndexDescription();
    45             case ExifInteropDirectory.TAG_INTEROP_VERSION:
     48            case TAG_INTEROP_VERSION:
    4649                return getInteropVersionDescription();
    4750            default:
     
    5356    public String getInteropVersionDescription()
    5457    {
    55         int[] ints = _directory.getIntArray(ExifInteropDirectory.TAG_INTEROP_VERSION);
    56         return convertBytesToVersionString(ints, 2);
     58        return getVersionBytesDescription(TAG_INTEROP_VERSION, 2);
    5759    }
    5860
     
    6062    public String getInteropIndexDescription()
    6163    {
    62         String value = _directory.getString(ExifInteropDirectory.TAG_INTEROP_INDEX);
     64        String value = _directory.getString(TAG_INTEROP_INDEX);
    6365
    64         if (value==null)
     66        if (value == null)
    6567            return null;
    6668
  • trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata.exif;
     
    2929 * Describes Exif interoperability tags.
    3030 *
    31  * @author Drew Noakes http://drewnoakes.com
     31 * @author Drew Noakes https://drewnoakes.com
    3232 */
    3333public class ExifInteropDirectory extends Directory
     
    5656    }
    5757
     58    @Override
    5859    @NotNull
    5960    public String getName()
     
    6263    }
    6364
     65    @Override
    6466    @NotNull
    6567    protected HashMap<Integer, String> getTagNameMap()
  • trunk/src/com/drew/metadata/exif/ExifReader.java

    r6209 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata.exif;
    2222
    23 import java.util.HashSet;
    24 import java.util.Set;
     23import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
     24import com.drew.imaging.jpeg.JpegSegmentType;
     25import com.drew.imaging.tiff.TiffProcessingException;
     26import com.drew.imaging.tiff.TiffReader;
     27import com.drew.lang.ByteArrayReader;
     28import com.drew.lang.annotations.NotNull;
     29import com.drew.metadata.Metadata;
    2530
    26 import com.drew.lang.BufferBoundsException;
    27 import com.drew.lang.BufferReader;
    28 import com.drew.lang.Rational;
    29 import com.drew.lang.annotations.NotNull;
    30 import com.drew.metadata.Directory;
    31 import com.drew.metadata.Metadata;
    32 import com.drew.metadata.MetadataReader;
     31import java.io.IOException;
     32import java.util.Arrays;
    3333
    3434/**
    3535 * Decodes Exif binary data, populating a {@link Metadata} object with tag values in {@link ExifSubIFDDirectory},
    36  * {@link ExifThumbnailDirectory}, {@link ExifInteropDirectory}, {@link GpsDirectory} and one of the many camera makernote directories.
     36 * {@link ExifThumbnailDirectory}, {@link ExifInteropDirectory}, {@link GpsDirectory} and one of the many camera
     37 * makernote directories.
    3738 *
    38  * @author Drew Noakes http://drewnoakes.com
     39 * @author Drew Noakes https://drewnoakes.com
    3940 */
    40 public class ExifReader implements MetadataReader
     41public class ExifReader implements JpegSegmentMetadataReader
    4142{
    42     // TODO extract a reusable TiffReader from this class with hooks for special tag handling and subdir following
    43    
    44     /** The number of bytes used per format descriptor. */
     43    /**
     44     * The offset at which the TIFF data actually starts. This may be necessary when, for example, processing
     45     * JPEG Exif data from APP0 which has a 6-byte preamble before starting the TIFF data.
     46     */
     47    private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0";
     48
     49    private boolean _storeThumbnailBytes = true;
     50
     51    public boolean isStoreThumbnailBytes()
     52    {
     53        return _storeThumbnailBytes;
     54    }
     55
     56    public void setStoreThumbnailBytes(boolean storeThumbnailBytes)
     57    {
     58        _storeThumbnailBytes = storeThumbnailBytes;
     59    }
     60
    4561    @NotNull
    46     private static final int[] BYTES_PER_FORMAT = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 };
     62    public Iterable<JpegSegmentType> getSegmentTypes()
     63    {
     64        return Arrays.asList(JpegSegmentType.APP1);
     65    }
    4766
    48     /** The number of formats known. */
    49     private static final int MAX_FORMAT_CODE = 12;
     67    public boolean canProcess(@NotNull final byte[] segmentBytes, @NotNull final JpegSegmentType segmentType)
     68    {
     69        return segmentBytes.length >= JPEG_EXIF_SEGMENT_PREAMBLE.length() && new String(segmentBytes, 0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equalsIgnoreCase(JPEG_EXIF_SEGMENT_PREAMBLE);
     70    }
    5071
    51     // Format types
    52     // TODO use an enum for these?
    53     /** An 8-bit unsigned integer. */
    54     private static final int FMT_BYTE = 1;
    55     /** A fixed-length character string. */
    56     private static final int FMT_STRING = 2;
    57     /** An unsigned 16-bit integer. */
    58     private static final int FMT_USHORT = 3;
    59     /** An unsigned 32-bit integer. */
    60     private static final int FMT_ULONG = 4;
    61     private static final int FMT_URATIONAL = 5;
    62     /** An 8-bit signed integer. */
    63     private static final int FMT_SBYTE = 6;
    64     private static final int FMT_UNDEFINED = 7;
    65     /** A signed 16-bit integer. */
    66     private static final int FMT_SSHORT = 8;
    67     /** A signed 32-bit integer. */
    68     private static final int FMT_SLONG = 9;
    69     private static final int FMT_SRATIONAL = 10;
    70     /** A 32-bit floating point number. */
    71     private static final int FMT_SINGLE = 11;
    72     /** A 64-bit floating point number. */
    73     private static final int FMT_DOUBLE = 12;
     72    public void extract(@NotNull final byte[] segmentBytes, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType)
     73    {
     74        if (segmentBytes == null)
     75            throw new NullPointerException("segmentBytes cannot be null");
     76        if (metadata == null)
     77            throw new NullPointerException("metadata cannot be null");
     78        if (segmentType == null)
     79            throw new NullPointerException("segmentType cannot be null");
    7480
    75     /** This tag is a pointer to the Exif SubIFD. */
    76     public static final int TAG_EXIF_SUB_IFD_OFFSET = 0x8769;
    77     /** This tag is a pointer to the Exif Interop IFD. */
    78     public static final int TAG_INTEROP_OFFSET = 0xA005;
    79     /** This tag is a pointer to the Exif GPS IFD. */
    80     public static final int TAG_GPS_INFO_OFFSET = 0x8825;
    81     /** This tag is a pointer to the Exif Makernote IFD. */
    82     public static final int TAG_MAKER_NOTE_OFFSET = 0x927C;
     81        try {
     82            ByteArrayReader reader = new ByteArrayReader(segmentBytes);
    8383
    84     public static final int TIFF_HEADER_START_OFFSET = 6;
    85 
    86     /**
    87      * Performs the Exif data extraction, adding found values to the specified
    88      * instance of <code>Metadata</code>.
    89      *
    90      * @param reader   The buffer reader from which Exif data should be read.
    91      * @param metadata The Metadata object into which extracted values should be merged.
    92      */
    93     public void extract(@NotNull final BufferReader reader, @NotNull Metadata metadata)
    94     {
    95         final ExifSubIFDDirectory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class);
    96 
    97         // check for the header length
    98         if (reader.getLength() <= 14) {
    99             directory.addError("Exif data segment must contain at least 14 bytes");
    100             return;
    101         }
    102 
    103         // check for the header preamble
    104         try {
    105             if (!reader.getString(0, 6).equals("Exif\0\0")) {
    106                 directory.addError("Exif data segment doesn't begin with 'Exif'");
     84            //
     85            // Check for the header preamble
     86            //
     87            try {
     88                if (!reader.getString(0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equals(JPEG_EXIF_SEGMENT_PREAMBLE)) {
     89                    // TODO what do to with this error state?
     90                    System.err.println("Invalid JPEG Exif segment preamble");
     91                    return;
     92                }
     93            } catch (IOException e) {
     94                // TODO what do to with this error state?
     95                e.printStackTrace(System.err);
    10796                return;
    10897            }
    10998
    110             extractIFD(metadata, metadata.getOrCreateDirectory(ExifIFD0Directory.class), TIFF_HEADER_START_OFFSET, reader);
    111         } catch (BufferBoundsException e) {
    112             directory.addError("Exif data segment ended prematurely");
     99            //
     100            // Read the TIFF-formatted Exif data
     101            //
     102            new TiffReader().processTiff(
     103                reader,
     104                new ExifTiffHandler(metadata, _storeThumbnailBytes),
     105                JPEG_EXIF_SEGMENT_PREAMBLE.length()
     106            );
     107
     108        } catch (TiffProcessingException e) {
     109            // TODO what do to with this error state?
     110            e.printStackTrace(System.err);
     111        } catch (IOException e) {
     112            // TODO what do to with this error state?
     113            e.printStackTrace(System.err);
    113114        }
    114115    }
    115 
    116     /**
    117      * Performs the Exif data extraction on a TIFF/RAW, adding found values to the specified
    118      * instance of <code>Metadata</code>.
    119      *
    120      * @param reader   The BufferReader from which TIFF data should be read.
    121      * @param metadata The Metadata object into which extracted values should be merged.
    122      */
    123     public void extractTiff(@NotNull BufferReader reader, @NotNull Metadata metadata)
    124     {
    125         final ExifIFD0Directory directory = metadata.getOrCreateDirectory(ExifIFD0Directory.class);
    126 
    127         try {
    128             extractIFD(metadata, directory, 0, reader);
    129         } catch (BufferBoundsException e) {
    130             directory.addError("Exif data segment ended prematurely");
    131         }
    132     }
    133 
    134     private void extractIFD(@NotNull Metadata metadata, @NotNull final ExifIFD0Directory directory, int tiffHeaderOffset, @NotNull BufferReader reader) throws BufferBoundsException
    135     {
    136         // this should be either "MM" or "II"
    137         String byteOrderIdentifier = reader.getString(tiffHeaderOffset, 2);
    138 
    139         if ("MM".equals(byteOrderIdentifier)) {
    140             reader.setMotorolaByteOrder(true);
    141         } else if ("II".equals(byteOrderIdentifier)) {
    142             reader.setMotorolaByteOrder(false);
    143         } else {
    144             directory.addError("Unclear distinction between Motorola/Intel byte ordering: " + byteOrderIdentifier);
    145             return;
    146         }
    147 
    148         // Check the next two values for correctness.
    149         final int tiffMarker = reader.getUInt16(2 + tiffHeaderOffset);
    150 
    151         final int standardTiffMarker = 0x002A;
    152         final int olympusRawTiffMarker = 0x4F52; // for ORF files
    153         final int panasonicRawTiffMarker = 0x0055; // for RW2 files
    154 
    155         if (tiffMarker != standardTiffMarker && tiffMarker != olympusRawTiffMarker && tiffMarker != panasonicRawTiffMarker) {
    156             directory.addError("Unexpected TIFF marker after byte order identifier: 0x" + Integer.toHexString(tiffMarker));
    157             return;
    158         }
    159 
    160         int firstDirectoryOffset = reader.getInt32(4 + tiffHeaderOffset) + tiffHeaderOffset;
    161 
    162         // David Ekholm sent a digital camera image that has this problem
    163         if (firstDirectoryOffset >= reader.getLength() - 1) {
    164             directory.addError("First exif directory offset is beyond end of Exif data segment");
    165             // First directory normally starts 14 bytes in -- try it here and catch another error in the worst case
    166             firstDirectoryOffset = 14;
    167         }
    168 
    169         Set<Integer> processedDirectoryOffsets = new HashSet<Integer>();
    170 
    171         processDirectory(directory, processedDirectoryOffsets, firstDirectoryOffset, tiffHeaderOffset, metadata, reader);
    172 
    173         // after the extraction process, if we have the correct tags, we may be able to store thumbnail information
    174         ExifThumbnailDirectory thumbnailDirectory = metadata.getDirectory(ExifThumbnailDirectory.class);
    175         if (thumbnailDirectory!=null && thumbnailDirectory.containsTag(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)) {
    176             Integer offset = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);
    177             Integer length = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);
    178             if (offset != null && length != null) {
    179                 try {
    180                     byte[] thumbnailData = reader.getBytes(tiffHeaderOffset + offset, length);
    181                     thumbnailDirectory.setThumbnailData(thumbnailData);
    182                 } catch (BufferBoundsException ex) {
    183                     directory.addError("Invalid thumbnail data specification: " + ex.getMessage());
    184                 }
    185             }
    186         }
    187     }
    188 
    189     /**
    190      * Process one of the nested Tiff IFD directories.
    191      * <p/>
    192      * Header
    193      * 2 bytes: number of tags
    194      * <p/>
    195      * Then for each tag
    196      * 2 bytes: tag type
    197      * 2 bytes: format code
    198      * 4 bytes: component count
    199      */
    200     private void processDirectory(@NotNull Directory directory, @NotNull Set<Integer> processedDirectoryOffsets, int dirStartOffset, int tiffHeaderOffset, @NotNull final Metadata metadata, @NotNull final BufferReader reader) throws BufferBoundsException
    201     {
    202         // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist
    203         if (processedDirectoryOffsets.contains(Integer.valueOf(dirStartOffset)))
    204             return;
    205 
    206         // remember that we've visited this directory so that we don't visit it again later
    207         processedDirectoryOffsets.add(dirStartOffset);
    208 
    209         if (dirStartOffset >= reader.getLength() || dirStartOffset < 0) {
    210             directory.addError("Ignored directory marked to start outside data segment");
    211             return;
    212         }
    213 
    214         // First two bytes in the IFD are the number of tags in this directory
    215         int dirTagCount = reader.getUInt16(dirStartOffset);
    216 
    217         int dirLength = (2 + (12 * dirTagCount) + 4);
    218         if (dirLength + dirStartOffset > reader.getLength()) {
    219             directory.addError("Illegally sized directory");
    220             return;
    221         }
    222 
    223         // Handle each tag in this directory
    224         for (int tagNumber = 0; tagNumber < dirTagCount; tagNumber++) {
    225             final int tagOffset = calculateTagOffset(dirStartOffset, tagNumber);
    226 
    227             // 2 bytes for the tag type
    228             final int tagType = reader.getUInt16(tagOffset);
    229 
    230             // 2 bytes for the format code
    231             final int formatCode = reader.getUInt16(tagOffset + 2);
    232             if (formatCode < 1 || formatCode > MAX_FORMAT_CODE) {
    233                 // This error suggests that we are processing at an incorrect index and will generate
    234                 // rubbish until we go out of bounds (which may be a while).  Exit now.
    235                 directory.addError("Invalid TIFF tag format code: " + formatCode);
    236                 continue; // JOSM patch to fix #9030
    237             }
    238 
    239             // 4 bytes dictate the number of components in this tag's data
    240             final int componentCount = reader.getInt32(tagOffset + 4);
    241             if (componentCount < 0) {
    242                 directory.addError("Negative TIFF tag component count");
    243                 continue;
    244             }
    245             // each component may have more than one byte... calculate the total number of bytes
    246             final int byteCount = componentCount * BYTES_PER_FORMAT[formatCode];
    247             final int tagValueOffset;
    248             if (byteCount > 4) {
    249                 // If it's bigger than 4 bytes, the dir entry contains an offset.
    250                 // dirEntryOffset must be passed, as some makernote implementations (e.g. FujiFilm) incorrectly use an
    251                 // offset relative to the start of the makernote itself, not the TIFF segment.
    252                 final int offsetVal = reader.getInt32(tagOffset + 8);
    253                 if (offsetVal + byteCount > reader.getLength()) {
    254                     // Bogus pointer offset and / or byteCount value
    255                     directory.addError("Illegal TIFF tag pointer offset");
    256                     continue;
    257                 }
    258                 tagValueOffset = tiffHeaderOffset + offsetVal;
    259             } else {
    260                 // 4 bytes or less and value is in the dir entry itself
    261                 tagValueOffset = tagOffset + 8;
    262             }
    263 
    264             if (tagValueOffset < 0 || tagValueOffset > reader.getLength()) {
    265                 directory.addError("Illegal TIFF tag pointer offset");
    266                 continue;
    267             }
    268 
    269             // Check that this tag isn't going to allocate outside the bounds of the data array.
    270             // This addresses an uncommon OutOfMemoryError.
    271             if (byteCount < 0 || tagValueOffset + byteCount > reader.getLength()) {
    272                 directory.addError("Illegal number of bytes: " + byteCount);
    273                 continue;
    274             }
    275 
    276             switch (tagType) {
    277                 case TAG_EXIF_SUB_IFD_OFFSET: {
    278                     final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
    279                     processDirectory(metadata.getOrCreateDirectory(ExifSubIFDDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
    280                     continue;
    281                 }
    282                 case TAG_INTEROP_OFFSET: {
    283                     final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
    284                     processDirectory(metadata.getOrCreateDirectory(ExifInteropDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
    285                     continue;
    286                 }
    287                 case TAG_GPS_INFO_OFFSET: {
    288                     final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
    289                     processDirectory(metadata.getOrCreateDirectory(GpsDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
    290                     continue;
    291                 }
    292                 case TAG_MAKER_NOTE_OFFSET: {
    293                     processMakerNote(tagValueOffset, processedDirectoryOffsets, tiffHeaderOffset, metadata, reader);
    294                     continue;
    295                 }
    296                 default: {
    297                     processTag(directory, tagType, tagValueOffset, componentCount, formatCode, reader);
    298                     break;
    299                 }
    300             }
    301         }
    302 
    303         // at the end of each IFD is an optional link to the next IFD
    304         final int finalTagOffset = calculateTagOffset(dirStartOffset, dirTagCount);
    305         int nextDirectoryOffset = reader.getInt32(finalTagOffset);
    306         if (nextDirectoryOffset != 0) {
    307             nextDirectoryOffset += tiffHeaderOffset;
    308             if (nextDirectoryOffset >= reader.getLength()) {
    309                 // Last 4 bytes of IFD reference another IFD with an address that is out of bounds
    310                 // Note this could have been caused by jhead 1.3 cropping too much
    311                 return;
    312             } else if (nextDirectoryOffset < dirStartOffset) {
    313                 // Last 4 bytes of IFD reference another IFD with an address that is before the start of this directory
    314                 return;
    315             }
    316             // TODO in Exif, the only known 'follower' IFD is the thumbnail one, however this may not be the case
    317             final ExifThumbnailDirectory nextDirectory = metadata.getOrCreateDirectory(ExifThumbnailDirectory.class);
    318             processDirectory(nextDirectory, processedDirectoryOffsets, nextDirectoryOffset, tiffHeaderOffset, metadata, reader);
    319         }
    320     }
    321 
    322     private void processMakerNote(int subdirOffset, @NotNull Set<Integer> processedDirectoryOffsets, int tiffHeaderOffset, @NotNull final Metadata metadata, @NotNull BufferReader reader) throws BufferBoundsException
    323     {
    324         // Determine the camera model and makernote format
    325         Directory ifd0Directory = metadata.getDirectory(ExifIFD0Directory.class);
    326 
    327         if (ifd0Directory==null)
    328             return;
    329 
    330         String cameraModel = ifd0Directory.getString(ExifIFD0Directory.TAG_MAKE);
    331 
    332         //final String firstTwoChars = reader.getString(subdirOffset, 2);
    333         final String firstThreeChars = reader.getString(subdirOffset, 3);
    334         final String firstFourChars = reader.getString(subdirOffset, 4);
    335         final String firstFiveChars = reader.getString(subdirOffset, 5);
    336         final String firstSixChars = reader.getString(subdirOffset, 6);
    337         final String firstSevenChars = reader.getString(subdirOffset, 7);
    338         final String firstEightChars = reader.getString(subdirOffset, 8);
    339         final String firstTwelveChars = reader.getString(subdirOffset, 12);
    340 
    341         if ("OLYMP".equals(firstFiveChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars)) {
    342             // Olympus Makernote
    343             // Epson and Agfa use Olympus maker note standard: http://www.ozhiker.com/electronics/pjmt/jpeg_info/
    344             processDirectory(metadata.getOrCreateDirectory(OlympusMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 8, tiffHeaderOffset, metadata, reader);
    345         } else if (cameraModel != null && cameraModel.trim().toUpperCase().startsWith("NIKON")) {
    346             if ("Nikon".equals(firstFiveChars)) {
    347                 /* There are two scenarios here:
    348                  * Type 1:                  **
    349                  * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........
    350                  * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................
    351                  * Type 3:                  **
    352                  * :0000: 4E 69 6B 6F 6E 00 02 00-00 00 4D 4D 00 2A 00 00 Nikon....MM.*...
    353                  * :0010: 00 08 00 1E 00 01 00 07-00 00 00 04 30 32 30 30 ............0200
    354                  */
    355                 switch (reader.getUInt8(subdirOffset + 6)) {
    356                     case 1:
    357                         processDirectory(metadata.getOrCreateDirectory(NikonType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 8, tiffHeaderOffset, metadata, reader);
    358                         break;
    359                     case 2:
    360                         processDirectory(metadata.getOrCreateDirectory(NikonType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 18, subdirOffset + 10, metadata, reader);
    361                         break;
    362                     default:
    363                         ifd0Directory.addError("Unsupported Nikon makernote data ignored.");
    364                         break;
    365                 }
    366             } else {
    367                 // The IFD begins with the first MakerNote byte (no ASCII name).  This occurs with CoolPix 775, E990 and D1 models.
    368                 processDirectory(metadata.getOrCreateDirectory(NikonType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
    369             }
    370         } else if ("SONY CAM".equals(firstEightChars) || "SONY DSC".equals(firstEightChars)) {
    371             processDirectory(metadata.getOrCreateDirectory(SonyType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 12, tiffHeaderOffset, metadata, reader);
    372         } else if ("SIGMA\u0000\u0000\u0000".equals(firstEightChars) || "FOVEON\u0000\u0000".equals(firstEightChars)) {
    373             processDirectory(metadata.getOrCreateDirectory(SigmaMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 10, tiffHeaderOffset, metadata, reader);
    374         } else if ("SEMC MS\u0000\u0000\u0000\u0000\u0000".equals(firstTwelveChars)) {
    375             // force MM for this directory
    376             boolean isMotorola = reader.isMotorolaByteOrder();
    377             reader.setMotorolaByteOrder(true);
    378             // skip 12 byte header + 2 for "MM" + 6
    379             processDirectory(metadata.getOrCreateDirectory(SonyType6MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 20, tiffHeaderOffset, metadata, reader);
    380             reader.setMotorolaByteOrder(isMotorola);
    381         } else if ("KDK".equals(firstThreeChars)) {
    382             processDirectory(metadata.getOrCreateDirectory(KodakMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 20, tiffHeaderOffset, metadata, reader);
    383         } else if ("Canon".equalsIgnoreCase(cameraModel)) {
    384             processDirectory(metadata.getOrCreateDirectory(CanonMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
    385         } else if (cameraModel != null && cameraModel.toUpperCase().startsWith("CASIO")) {
    386             if ("QVC\u0000\u0000\u0000".equals(firstSixChars))
    387                 processDirectory(metadata.getOrCreateDirectory(CasioType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 6, tiffHeaderOffset, metadata, reader);
    388             else
    389                 processDirectory(metadata.getOrCreateDirectory(CasioType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
    390         } else if ("FUJIFILM".equals(firstEightChars) || "Fujifilm".equalsIgnoreCase(cameraModel)) {
    391             boolean byteOrderBefore = reader.isMotorolaByteOrder();
    392             // bug in fujifilm makernote ifd means we temporarily use Intel byte ordering
    393             reader.setMotorolaByteOrder(false);
    394             // the 4 bytes after "FUJIFILM" in the makernote point to the start of the makernote
    395             // IFD, though the offset is relative to the start of the makernote, not the TIFF
    396             // header (like everywhere else)
    397             int ifdStart = subdirOffset + reader.getInt32(subdirOffset + 8);
    398             processDirectory(metadata.getOrCreateDirectory(FujifilmMakernoteDirectory.class), processedDirectoryOffsets, ifdStart, tiffHeaderOffset, metadata, reader);
    399             reader.setMotorolaByteOrder(byteOrderBefore);
    400         } else if (cameraModel != null && cameraModel.toUpperCase().startsWith("MINOLTA")) {
    401             // Cases seen with the model starting with MINOLTA in capitals seem to have a valid Olympus makernote
    402             // area that commences immediately.
    403             processDirectory(metadata.getOrCreateDirectory(OlympusMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
    404         } else if ("KYOCERA".equals(firstSevenChars)) {
    405             // http://www.ozhiker.com/electronics/pjmt/jpeg_info/kyocera_mn.html
    406             processDirectory(metadata.getOrCreateDirectory(KyoceraMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 22, tiffHeaderOffset, metadata, reader);
    407         } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(subdirOffset, 12))) {
    408             // NON-Standard TIFF IFD Data using Panasonic Tags. There is no Next-IFD pointer after the IFD
    409             // Offsets are relative to the start of the TIFF header at the beginning of the EXIF segment
    410             // more information here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html
    411             processDirectory(metadata.getOrCreateDirectory(PanasonicMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 12, tiffHeaderOffset, metadata, reader);
    412         } else if ("AOC\u0000".equals(firstFourChars)) {
    413             // NON-Standard TIFF IFD Data using Casio Type 2 Tags
    414             // IFD has no Next-IFD pointer at end of IFD, and
    415             // Offsets are relative to the start of the current IFD tag, not the TIFF header
    416             // Observed for:
    417             // - Pentax ist D
    418             processDirectory(metadata.getOrCreateDirectory(CasioType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 6, subdirOffset, metadata, reader);
    419         } else if (cameraModel != null && (cameraModel.toUpperCase().startsWith("PENTAX") || cameraModel.toUpperCase().startsWith("ASAHI"))) {
    420             // NON-Standard TIFF IFD Data using Pentax Tags
    421             // IFD has no Next-IFD pointer at end of IFD, and
    422             // Offsets are relative to the start of the current IFD tag, not the TIFF header
    423             // Observed for:
    424             // - PENTAX Optio 330
    425             // - PENTAX Optio 430
    426             processDirectory(metadata.getOrCreateDirectory(PentaxMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, subdirOffset, metadata, reader);
    427 //        } else if ("KC".equals(firstTwoChars) || "MINOL".equals(firstFiveChars) || "MLY".equals(firstThreeChars) || "+M+M+M+M".equals(firstEightChars)) {
    428 //            // This Konica data is not understood.  Header identified in accordance with information at this site:
    429 //            // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html
    430 //            // TODO add support for minolta/konica cameras
    431 //            exifDirectory.addError("Unsupported Konica/Minolta data ignored.");
    432         } else {
    433             // TODO how to store makernote data when it's not from a supported camera model?
    434             // this is difficult as the starting offset is not known.  we could look for it...
    435         }
    436     }
    437 
    438     private void processTag(@NotNull Directory directory, int tagType, int tagValueOffset, int componentCount, int formatCode, @NotNull final BufferReader reader) throws BufferBoundsException
    439     {
    440         // Directory simply stores raw values
    441         // The display side uses a Descriptor class per directory to turn the raw values into 'pretty' descriptions
    442         switch (formatCode) {
    443             case FMT_UNDEFINED:
    444                 // this includes exif user comments
    445                 directory.setByteArray(tagType, reader.getBytes(tagValueOffset, componentCount));
    446                 break;
    447             case FMT_STRING:
    448                 String string = reader.getNullTerminatedString(tagValueOffset, componentCount);
    449                 directory.setString(tagType, string);
    450 /*
    451                 // special handling for certain known tags, proposed by Yuri Binev but left out for now,
    452                 // as it gives the false impression that the image was captured in the same timezone
    453                 // in which the string is parsed
    454                 if (tagType==ExifSubIFDDirectory.TAG_DATETIME ||
    455                     tagType==ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL ||
    456                     tagType==ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED) {
    457                     String[] datePatterns = {
    458                         "yyyy:MM:dd HH:mm:ss",
    459                         "yyyy:MM:dd HH:mm",
    460                         "yyyy-MM-dd HH:mm:ss",
    461                         "yyyy-MM-dd HH:mm"};
    462                     for (String datePattern : datePatterns) {
    463                         try {
    464                             DateFormat parser = new SimpleDateFormat(datePattern);
    465                             Date date = parser.parse(string);
    466                             directory.setDate(tagType, date);
    467                             break;
    468                         } catch (ParseException ex) {
    469                             // simply try the next pattern
    470                         }
    471                     }
    472                 }
    473 */
    474                 break;
    475             case FMT_SRATIONAL:
    476                 if (componentCount == 1) {
    477                     directory.setRational(tagType, new Rational(reader.getInt32(tagValueOffset), reader.getInt32(tagValueOffset + 4)));
    478                 } else if (componentCount > 1) {
    479                     Rational[] rationals = new Rational[componentCount];
    480                     for (int i = 0; i < componentCount; i++)
    481                         rationals[i] = new Rational(reader.getInt32(tagValueOffset + (8 * i)), reader.getInt32(tagValueOffset + 4 + (8 * i)));
    482                     directory.setRationalArray(tagType, rationals);
    483                 }
    484                 break;
    485             case FMT_URATIONAL:
    486                 if (componentCount == 1) {
    487                     directory.setRational(tagType, new Rational(reader.getUInt32(tagValueOffset), reader.getUInt32(tagValueOffset + 4)));
    488                 } else if (componentCount > 1) {
    489                     Rational[] rationals = new Rational[componentCount];
    490                     for (int i = 0; i < componentCount; i++)
    491                         rationals[i] = new Rational(reader.getUInt32(tagValueOffset + (8 * i)), reader.getUInt32(tagValueOffset + 4 + (8 * i)));
    492                     directory.setRationalArray(tagType, rationals);
    493                 }
    494                 break;
    495             case FMT_SINGLE:
    496                 if (componentCount == 1) {
    497                     directory.setFloat(tagType, reader.getFloat32(tagValueOffset));
    498                 } else {
    499                     float[] floats = new float[componentCount];
    500                     for (int i = 0; i < componentCount; i++)
    501                         floats[i] = reader.getFloat32(tagValueOffset + (i * 4));
    502                     directory.setFloatArray(tagType, floats);
    503                 }
    504                 break;
    505             case FMT_DOUBLE:
    506                 if (componentCount == 1) {
    507                     directory.setDouble(tagType, reader.getDouble64(tagValueOffset));
    508                 } else {
    509                     double[] doubles = new double[componentCount];
    510                     for (int i = 0; i < componentCount; i++)
    511                         doubles[i] = reader.getDouble64(tagValueOffset + (i * 4));
    512                     directory.setDoubleArray(tagType, doubles);
    513                 }
    514                 break;
    515 
    516             //
    517             // Note that all integral types are stored as int32 internally (the largest supported by TIFF)
    518             //
    519 
    520             case FMT_SBYTE:
    521                 if (componentCount == 1) {
    522                     directory.setInt(tagType, reader.getInt8(tagValueOffset));
    523                 } else {
    524                     int[] bytes = new int[componentCount];
    525                     for (int i = 0; i < componentCount; i++)
    526                         bytes[i] = reader.getInt8(tagValueOffset + i);
    527                     directory.setIntArray(tagType, bytes);
    528                 }
    529                 break;
    530             case FMT_BYTE:
    531                 if (componentCount == 1) {
    532                     directory.setInt(tagType, reader.getUInt8(tagValueOffset));
    533                 } else {
    534                     int[] bytes = new int[componentCount];
    535                     for (int i = 0; i < componentCount; i++)
    536                         bytes[i] = reader.getUInt8(tagValueOffset + i);
    537                     directory.setIntArray(tagType, bytes);
    538                 }
    539                 break;
    540             case FMT_USHORT:
    541                 if (componentCount == 1) {
    542                     int i = reader.getUInt16(tagValueOffset);
    543                     directory.setInt(tagType, i);
    544                 } else {
    545                     int[] ints = new int[componentCount];
    546                     for (int i = 0; i < componentCount; i++)
    547                         ints[i] = reader.getUInt16(tagValueOffset + (i * 2));
    548                     directory.setIntArray(tagType, ints);
    549                 }
    550                 break;
    551             case FMT_SSHORT:
    552                 if (componentCount == 1) {
    553                     int i = reader.getInt16(tagValueOffset);
    554                     directory.setInt(tagType, i);
    555                 } else {
    556                     int[] ints = new int[componentCount];
    557                     for (int i = 0; i < componentCount; i++)
    558                         ints[i] = reader.getInt16(tagValueOffset + (i * 2));
    559                     directory.setIntArray(tagType, ints);
    560                 }
    561                 break;
    562             case FMT_SLONG:
    563             case FMT_ULONG:
    564                 // NOTE 'long' in this case means 32 bit, not 64
    565                 if (componentCount == 1) {
    566                     int i = reader.getInt32(tagValueOffset);
    567                     directory.setInt(tagType, i);
    568                 } else {
    569                     int[] ints = new int[componentCount];
    570                     for (int i = 0; i < componentCount; i++)
    571                         ints[i] = reader.getInt32(tagValueOffset + (i * 4));
    572                     directory.setIntArray(tagType, ints);
    573                 }
    574                 break;
    575             default:
    576                 directory.addError("Unknown format code " + formatCode + " for tag " + tagType);
    577         }
    578     }
    579 
    580     /**
    581      * Determine the offset at which a given InteropArray entry begins within the specified IFD.
    582      *
    583      * @param dirStartOffset the offset at which the IFD starts
    584      * @param entryNumber    the zero-based entry number
    585      */
    586     private int calculateTagOffset(int dirStartOffset, int entryNumber)
    587     {
    588         // add 2 bytes for the tag count
    589         // each entry is 12 bytes, so we skip 12 * the number seen so far
    590         return dirStartOffset + 2 + (12 * entryNumber);
    591     }
    592116}
  • trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata.exif;
     
    3232import java.util.Map;
    3333
     34import static com.drew.metadata.exif.ExifSubIFDDirectory.*;
     35
    3436/**
    35  * Provides human-readable string representations of tag values stored in a <code>ExifSubIFDDirectory</code>.
     37 * Provides human-readable string representations of tag values stored in a {@link ExifSubIFDDirectory}.
    3638 *
    37  * @author Drew Noakes http://drewnoakes.com
     39 * @author Drew Noakes https://drewnoakes.com
    3840 */
    3941public class ExifSubIFDDescriptor extends TagDescriptor<ExifSubIFDDirectory>
     
    6062
    6163    /**
    62      * Returns a descriptive value of the the specified tag for this image.
     64     * Returns a descriptive value of the specified tag for this image.
    6365     * Where possible, known values will be substituted here in place of the raw
    6466     * tokens actually kept in the Exif segment.  If no substitution is
    6567     * available, the value provided by getString(int) will be returned.
     68     *
    6669     * @param tagType the tag to find a description for
    6770     * @return a description of the image's value for the specified tag, or
    6871     *         <code>null</code> if the tag hasn't been defined.
    6972     */
     73    @Override
    7074    @Nullable
    7175    public String getDescription(int tagType)
    7276    {
    7377        switch (tagType) {
    74             case ExifSubIFDDirectory.TAG_NEW_SUBFILE_TYPE:
     78            case TAG_NEW_SUBFILE_TYPE:
    7579                return getNewSubfileTypeDescription();
    76             case ExifSubIFDDirectory.TAG_SUBFILE_TYPE:
     80            case TAG_SUBFILE_TYPE:
    7781                return getSubfileTypeDescription();
    78             case ExifSubIFDDirectory.TAG_THRESHOLDING:
     82            case TAG_THRESHOLDING:
    7983                return getThresholdingDescription();
    80             case ExifSubIFDDirectory.TAG_FILL_ORDER:
     84            case TAG_FILL_ORDER:
    8185                return getFillOrderDescription();
    82             case ExifSubIFDDirectory.TAG_EXPOSURE_TIME:
     86            case TAG_EXPOSURE_TIME:
    8387                return getExposureTimeDescription();
    84             case ExifSubIFDDirectory.TAG_SHUTTER_SPEED:
     88            case TAG_SHUTTER_SPEED:
    8589                return getShutterSpeedDescription();
    86             case ExifSubIFDDirectory.TAG_FNUMBER:
     90            case TAG_FNUMBER:
    8791                return getFNumberDescription();
    88             case ExifSubIFDDirectory.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL:
     92            case TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL:
    8993                return getCompressedAverageBitsPerPixelDescription();
    90             case ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE:
     94            case TAG_SUBJECT_DISTANCE:
    9195                return getSubjectDistanceDescription();
    92             case ExifSubIFDDirectory.TAG_METERING_MODE:
     96            case TAG_METERING_MODE:
    9397                return getMeteringModeDescription();
    94             case ExifSubIFDDirectory.TAG_WHITE_BALANCE:
     98            case TAG_WHITE_BALANCE:
    9599                return getWhiteBalanceDescription();
    96             case ExifSubIFDDirectory.TAG_FLASH:
     100            case TAG_FLASH:
    97101                return getFlashDescription();
    98             case ExifSubIFDDirectory.TAG_FOCAL_LENGTH:
     102            case TAG_FOCAL_LENGTH:
    99103                return getFocalLengthDescription();
    100             case ExifSubIFDDirectory.TAG_COLOR_SPACE:
     104            case TAG_COLOR_SPACE:
    101105                return getColorSpaceDescription();
    102             case ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH:
     106            case TAG_EXIF_IMAGE_WIDTH:
    103107                return getExifImageWidthDescription();
    104             case ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT:
     108            case TAG_EXIF_IMAGE_HEIGHT:
    105109                return getExifImageHeightDescription();
    106             case ExifSubIFDDirectory.TAG_FOCAL_PLANE_UNIT:
     110            case TAG_FOCAL_PLANE_RESOLUTION_UNIT:
    107111                return getFocalPlaneResolutionUnitDescription();
    108             case ExifSubIFDDirectory.TAG_FOCAL_PLANE_X_RES:
     112            case TAG_FOCAL_PLANE_X_RESOLUTION:
    109113                return getFocalPlaneXResolutionDescription();
    110             case ExifSubIFDDirectory.TAG_FOCAL_PLANE_Y_RES:
     114            case TAG_FOCAL_PLANE_Y_RESOLUTION:
    111115                return getFocalPlaneYResolutionDescription();
    112             case ExifSubIFDDirectory.TAG_BITS_PER_SAMPLE:
     116            case TAG_BITS_PER_SAMPLE:
    113117                return getBitsPerSampleDescription();
    114             case ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION:
     118            case TAG_PHOTOMETRIC_INTERPRETATION:
    115119                return getPhotometricInterpretationDescription();
    116             case ExifSubIFDDirectory.TAG_ROWS_PER_STRIP:
     120            case TAG_ROWS_PER_STRIP:
    117121                return getRowsPerStripDescription();
    118             case ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS:
     122            case TAG_STRIP_BYTE_COUNTS:
    119123                return getStripByteCountsDescription();
    120             case ExifSubIFDDirectory.TAG_SAMPLES_PER_PIXEL:
     124            case TAG_SAMPLES_PER_PIXEL:
    121125                return getSamplesPerPixelDescription();
    122             case ExifSubIFDDirectory.TAG_PLANAR_CONFIGURATION:
     126            case TAG_PLANAR_CONFIGURATION:
    123127                return getPlanarConfigurationDescription();
    124             case ExifSubIFDDirectory.TAG_YCBCR_SUBSAMPLING:
     128            case TAG_YCBCR_SUBSAMPLING:
    125129                return getYCbCrSubsamplingDescription();
    126             case ExifSubIFDDirectory.TAG_EXPOSURE_PROGRAM:
     130            case TAG_EXPOSURE_PROGRAM:
    127131                return getExposureProgramDescription();
    128             case ExifSubIFDDirectory.TAG_APERTURE:
     132            case TAG_APERTURE:
    129133                return getApertureValueDescription();
    130             case ExifSubIFDDirectory.TAG_MAX_APERTURE:
     134            case TAG_MAX_APERTURE:
    131135                return getMaxApertureValueDescription();
    132             case ExifSubIFDDirectory.TAG_SENSING_METHOD:
     136            case TAG_SENSING_METHOD:
    133137                return getSensingMethodDescription();
    134             case ExifSubIFDDirectory.TAG_EXPOSURE_BIAS:
     138            case TAG_EXPOSURE_BIAS:
    135139                return getExposureBiasDescription();
    136             case ExifSubIFDDirectory.TAG_FILE_SOURCE:
     140            case TAG_FILE_SOURCE:
    137141                return getFileSourceDescription();
    138             case ExifSubIFDDirectory.TAG_SCENE_TYPE:
     142            case TAG_SCENE_TYPE:
    139143                return getSceneTypeDescription();
    140             case ExifSubIFDDirectory.TAG_COMPONENTS_CONFIGURATION:
     144            case TAG_COMPONENTS_CONFIGURATION:
    141145                return getComponentConfigurationDescription();
    142             case ExifSubIFDDirectory.TAG_EXIF_VERSION:
     146            case TAG_EXIF_VERSION:
    143147                return getExifVersionDescription();
    144             case ExifSubIFDDirectory.TAG_FLASHPIX_VERSION:
     148            case TAG_FLASHPIX_VERSION:
    145149                return getFlashPixVersionDescription();
    146             case ExifSubIFDDirectory.TAG_ISO_EQUIVALENT:
     150            case TAG_ISO_EQUIVALENT:
    147151                return getIsoEquivalentDescription();
    148             case ExifSubIFDDirectory.TAG_USER_COMMENT:
     152            case TAG_USER_COMMENT:
    149153                return getUserCommentDescription();
    150             case ExifSubIFDDirectory.TAG_CUSTOM_RENDERED:
     154            case TAG_CUSTOM_RENDERED:
    151155                return getCustomRenderedDescription();
    152             case ExifSubIFDDirectory.TAG_EXPOSURE_MODE:
     156            case TAG_EXPOSURE_MODE:
    153157                return getExposureModeDescription();
    154             case ExifSubIFDDirectory.TAG_WHITE_BALANCE_MODE:
     158            case TAG_WHITE_BALANCE_MODE:
    155159                return getWhiteBalanceModeDescription();
    156             case ExifSubIFDDirectory.TAG_DIGITAL_ZOOM_RATIO:
     160            case TAG_DIGITAL_ZOOM_RATIO:
    157161                return getDigitalZoomRatioDescription();
    158             case ExifSubIFDDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH:
     162            case TAG_35MM_FILM_EQUIV_FOCAL_LENGTH:
    159163                return get35mmFilmEquivFocalLengthDescription();
    160             case ExifSubIFDDirectory.TAG_SCENE_CAPTURE_TYPE:
     164            case TAG_SCENE_CAPTURE_TYPE:
    161165                return getSceneCaptureTypeDescription();
    162             case ExifSubIFDDirectory.TAG_GAIN_CONTROL:
     166            case TAG_GAIN_CONTROL:
    163167                return getGainControlDescription();
    164             case ExifSubIFDDirectory.TAG_CONTRAST:
     168            case TAG_CONTRAST:
    165169                return getContrastDescription();
    166             case ExifSubIFDDirectory.TAG_SATURATION:
     170            case TAG_SATURATION:
    167171                return getSaturationDescription();
    168             case ExifSubIFDDirectory.TAG_SHARPNESS:
     172            case TAG_SHARPNESS:
    169173                return getSharpnessDescription();
    170             case ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE_RANGE:
     174            case TAG_SUBJECT_DISTANCE_RANGE:
    171175                return getSubjectDistanceRangeDescription();
    172176            default:
     
    178182    public String getNewSubfileTypeDescription()
    179183    {
    180         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_NEW_SUBFILE_TYPE);
    181         if (value==null)
    182             return null;
    183         switch (value) {
    184             case 1: return "Full-resolution image";
    185             case 2: return "Reduced-resolution image";
    186             case 3: return "Single page of multi-page reduced-resolution image";
    187             case 4: return "Transparency mask";
    188             case 5: return "Transparency mask of reduced-resolution image";
    189             case 6: return "Transparency mask of multi-page image";
    190             case 7: return "Transparency mask of reduced-resolution multi-page image";
    191             default:
    192                 return "Unknown (" + value + ")";
    193         }
     184        return getIndexedDescription(TAG_NEW_SUBFILE_TYPE, 1,
     185            "Full-resolution image",
     186            "Reduced-resolution image",
     187            "Single page of multi-page reduced-resolution image",
     188            "Transparency mask",
     189            "Transparency mask of reduced-resolution image",
     190            "Transparency mask of multi-page image",
     191            "Transparency mask of reduced-resolution multi-page image"
     192        );
    194193    }
    195194
     
    197196    public String getSubfileTypeDescription()
    198197    {
    199         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SUBFILE_TYPE);
    200         if (value==null)
    201             return null;
    202         switch (value) {
    203             case 1: return "Full-resolution image";
    204             case 2: return "Reduced-resolution image";
    205             case 3: return "Single page of multi-page image";
    206             default:
    207                 return "Unknown (" + value + ")";
    208         }
     198        return getIndexedDescription(TAG_SUBFILE_TYPE, 1,
     199            "Full-resolution image",
     200            "Reduced-resolution image",
     201            "Single page of multi-page image"
     202        );
    209203    }
    210204
     
    212206    public String getThresholdingDescription()
    213207    {
    214         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_THRESHOLDING);
    215         if (value==null)
    216             return null;
    217         switch (value) {
    218             case 1: return "No dithering or halftoning";
    219             case 2: return "Ordered dither or halftone";
    220             case 3: return "Randomized dither";
    221             default:
    222                 return "Unknown (" + value + ")";
    223         }
     208        return getIndexedDescription(TAG_THRESHOLDING, 1,
     209            "No dithering or halftoning",
     210            "Ordered dither or halftone",
     211            "Randomized dither"
     212        );
    224213    }
    225214
     
    227216    public String getFillOrderDescription()
    228217    {
    229         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FILL_ORDER);
    230         if (value==null)
    231             return null;
    232         switch (value) {
    233             case 1: return "Normal";
    234             case 2: return "Reversed";
    235             default:
    236                 return "Unknown (" + value + ")";
    237         }
     218        return getIndexedDescription(TAG_FILL_ORDER, 1,
     219            "Normal",
     220            "Reversed"
     221        );
    238222    }
    239223
     
    241225    public String getSubjectDistanceRangeDescription()
    242226    {
    243         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE_RANGE);
    244         if (value==null)
    245             return null;
    246         switch (value) {
    247             case 0: return "Unknown";
    248             case 1: return "Macro";
    249             case 2: return "Close view";
    250             case 3: return "Distant view";
    251             default:
    252                 return "Unknown (" + value + ")";
    253         }
     227        return getIndexedDescription(TAG_SUBJECT_DISTANCE_RANGE,
     228            "Unknown",
     229            "Macro",
     230            "Close view",
     231            "Distant view"
     232        );
    254233    }
    255234
     
    257236    public String getSharpnessDescription()
    258237    {
    259         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SHARPNESS);
    260         if (value==null)
    261             return null;
    262         switch (value) {
    263             case 0: return "None";
    264             case 1: return "Low";
    265             case 2: return "Hard";
    266             default:
    267                 return "Unknown (" + value + ")";
    268         }
     238        return getIndexedDescription(TAG_SHARPNESS,
     239            "None",
     240            "Low",
     241            "Hard"
     242        );
    269243    }
    270244
     
    272246    public String getSaturationDescription()
    273247    {
    274         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SATURATION);
    275         if (value==null)
    276             return null;
    277         switch (value) {
    278             case 0: return "None";
    279             case 1: return "Low saturation";
    280             case 2: return "High saturation";
    281             default:
    282                 return "Unknown (" + value + ")";
    283         }
     248        return getIndexedDescription(TAG_SATURATION,
     249            "None",
     250            "Low saturation",
     251            "High saturation"
     252        );
    284253    }
    285254
     
    287256    public String getContrastDescription()
    288257    {
    289         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_CONTRAST);
    290         if (value==null)
    291             return null;
    292         switch (value) {
    293             case 0: return "None";
    294             case 1: return "Soft";
    295             case 2: return "Hard";
    296             default:
    297                 return "Unknown (" + value + ")";
    298         }
     258        return getIndexedDescription(TAG_CONTRAST,
     259            "None",
     260            "Soft",
     261            "Hard"
     262        );
    299263    }
    300264
     
    302266    public String getGainControlDescription()
    303267    {
    304         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_GAIN_CONTROL);
    305         if (value==null)
    306             return null;
    307         switch (value) {
    308             case 0: return "None";
    309             case 1: return "Low gain up";
    310             case 2: return "Low gain down";
    311             case 3: return "High gain up";
    312             case 4: return "High gain down";
    313             default:
    314                 return "Unknown (" + value + ")";
    315         }
     268        return getIndexedDescription(TAG_GAIN_CONTROL,
     269            "None",
     270            "Low gain up",
     271            "Low gain down",
     272            "High gain up",
     273            "High gain down"
     274        );
    316275    }
    317276
     
    319278    public String getSceneCaptureTypeDescription()
    320279    {
    321         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SCENE_CAPTURE_TYPE);
    322         if (value==null)
    323             return null;
    324         switch (value) {
    325             case 0: return "Standard";
    326             case 1: return "Landscape";
    327             case 2: return "Portrait";
    328             case 3: return "Night scene";
    329             default:
    330                 return "Unknown (" + value + ")";
    331         }
     280        return getIndexedDescription(TAG_SCENE_CAPTURE_TYPE,
     281            "Standard",
     282            "Landscape",
     283            "Portrait",
     284            "Night scene"
     285        );
    332286    }
    333287
     
    335289    public String get35mmFilmEquivFocalLengthDescription()
    336290    {
    337         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH);
    338         if (value==null)
    339             return null;
    340         if (value==0)
    341             return "Unknown";
    342         else
    343             return SimpleDecimalFormatter.format(value) + "mm";
     291        Integer value = _directory.getInteger(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH);
     292        return value == null
     293            ? null
     294            : value == 0
     295            ? "Unknown"
     296            : SimpleDecimalFormatter.format(value) + "mm";
    344297    }
    345298
     
    347300    public String getDigitalZoomRatioDescription()
    348301    {
    349         Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_DIGITAL_ZOOM_RATIO);
    350         if (value==null)
    351             return null;
    352         if (value.getNumerator()==0)
    353             return "Digital zoom not used.";
    354         return SimpleDecimalFormatter.format(value.doubleValue());
     302        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM_RATIO);
     303        return value == null
     304            ? null
     305            : value.getNumerator() == 0
     306            ? "Digital zoom not used."
     307            : SimpleDecimalFormatter.format(value.doubleValue());
    355308    }
    356309
     
    358311    public String getWhiteBalanceModeDescription()
    359312    {
    360         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_WHITE_BALANCE_MODE);
    361         if (value==null)
    362             return null;
    363         switch (value) {
    364             case 0: return "Auto white balance";
    365             case 1: return "Manual white balance";
    366             default:
    367                 return "Unknown (" + value + ")";
    368         }
     313        return getIndexedDescription(TAG_WHITE_BALANCE_MODE,
     314            "Auto white balance",
     315            "Manual white balance"
     316        );
    369317    }
    370318
     
    372320    public String getExposureModeDescription()
    373321    {
    374         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXPOSURE_MODE);
    375         if (value==null)
    376             return null;
    377         switch (value) {
    378             case 0: return "Auto exposure";
    379             case 1: return "Manual exposure";
    380             case 2: return "Auto bracket";
    381             default:
    382                 return "Unknown (" + value + ")";
    383         }
     322        return getIndexedDescription(TAG_EXPOSURE_MODE,
     323            "Auto exposure",
     324            "Manual exposure",
     325            "Auto bracket"
     326        );
    384327    }
    385328
     
    387330    public String getCustomRenderedDescription()
    388331    {
    389         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_CUSTOM_RENDERED);
    390         if (value==null)
    391             return null;
    392         switch (value) {
    393             case 0: return "Normal process";
    394             case 1: return "Custom process";
    395             default:
    396                 return "Unknown (" + value + ")";
    397         }
     332        return getIndexedDescription(TAG_CUSTOM_RENDERED,
     333            "Normal process",
     334            "Custom process"
     335        );
    398336    }
    399337
     
    401339    public String getUserCommentDescription()
    402340    {
    403         byte[] commentBytes = _directory.getByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT);
    404         if (commentBytes==null)
     341        byte[] commentBytes = _directory.getByteArray(TAG_USER_COMMENT);
     342        if (commentBytes == null)
    405343            return null;
    406344        if (commentBytes.length == 0)
     
    408346
    409347        final Map<String, String> encodingMap = new HashMap<String, String>();
    410         encodingMap.put("ASCII",    System.getProperty("file.encoding")); // Someone suggested "ISO-8859-1".
    411         encodingMap.put("UNICODE",  "UTF-16LE");
    412         encodingMap.put("JIS",      "Shift-JIS"); // We assume this charset for now.  Another suggestion is "JIS".
     348        encodingMap.put("ASCII", System.getProperty("file.encoding")); // Someone suggested "ISO-8859-1".
     349        encodingMap.put("UNICODE", "UTF-16LE");
     350        encodingMap.put("JIS", "Shift-JIS"); // We assume this charset for now.  Another suggestion is "JIS".
    413351
    414352        try {
     
    442380    {
    443381        // Have seen an exception here from files produced by ACDSEE that stored an int[] here with two values
    444         Integer isoEquiv = _directory.getInteger(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT);
    445         if (isoEquiv==null)
    446             return null;
     382        Integer isoEquiv = _directory.getInteger(TAG_ISO_EQUIVALENT);
    447383        // There used to be a check here that multiplied ISO values < 50 by 200.
    448384        // Issue 36 shows a smart-phone image from a Samsung Galaxy S2 with ISO-40.
    449         return Integer.toString(isoEquiv);
     385        return isoEquiv != null
     386            ? Integer.toString(isoEquiv)
     387            : null;
    450388    }
    451389
     
    453391    public String getExifVersionDescription()
    454392    {
    455         int[] ints = _directory.getIntArray(ExifSubIFDDirectory.TAG_EXIF_VERSION);
    456         if (ints==null)
    457             return null;
    458         return ExifSubIFDDescriptor.convertBytesToVersionString(ints, 2);
     393        return getVersionBytesDescription(TAG_EXIF_VERSION, 2);
    459394    }
    460395
     
    462397    public String getFlashPixVersionDescription()
    463398    {
    464         int[] ints = _directory.getIntArray(ExifSubIFDDirectory.TAG_FLASHPIX_VERSION);
    465         if (ints==null)
    466             return null;
    467         return ExifSubIFDDescriptor.convertBytesToVersionString(ints, 2);
     399        return getVersionBytesDescription(TAG_FLASHPIX_VERSION, 2);
    468400    }
    469401
     
    471403    public String getSceneTypeDescription()
    472404    {
    473         Integer sceneType = _directory.getInteger(ExifSubIFDDirectory.TAG_SCENE_TYPE);
    474         if (sceneType==null)
    475             return null;
    476         return sceneType == 1
    477                 ? "Directly photographed image"
    478                 : "Unknown (" + sceneType + ")";
     405        return getIndexedDescription(TAG_SCENE_TYPE,
     406            1,
     407            "Directly photographed image"
     408        );
    479409    }
    480410
     
    482412    public String getFileSourceDescription()
    483413    {
    484         Integer fileSource = _directory.getInteger(ExifSubIFDDirectory.TAG_FILE_SOURCE);
    485         if (fileSource==null)
    486             return null;
    487         return fileSource == 3
    488                 ? "Digital Still Camera (DSC)"
    489                 : "Unknown (" + fileSource + ")";
     414        return getIndexedDescription(TAG_FILE_SOURCE,
     415            1,
     416            "Film Scanner",
     417            "Reflection Print Scanner",
     418            "Digital Still Camera (DSC)"
     419        );
    490420    }
    491421
     
    493423    public String getExposureBiasDescription()
    494424    {
    495         Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_EXPOSURE_BIAS);
    496         if (value==null)
     425        Rational value = _directory.getRational(TAG_EXPOSURE_BIAS);
     426        if (value == null)
    497427            return null;
    498428        return value.toSimpleString(true) + " EV";
     
    502432    public String getMaxApertureValueDescription()
    503433    {
    504         Double aperture = _directory.getDoubleObject(ExifSubIFDDirectory.TAG_MAX_APERTURE);
    505         if (aperture==null)
     434        Double aperture = _directory.getDoubleObject(TAG_MAX_APERTURE);
     435        if (aperture == null)
    506436            return null;
    507437        double fStop = PhotographicConversions.apertureToFStop(aperture);
     
    512442    public String getApertureValueDescription()
    513443    {
    514         Double aperture = _directory.getDoubleObject(ExifSubIFDDirectory.TAG_APERTURE);
    515         if (aperture==null)
     444        Double aperture = _directory.getDoubleObject(TAG_APERTURE);
     445        if (aperture == null)
    516446            return null;
    517447        double fStop = PhotographicConversions.apertureToFStop(aperture);
     
    522452    public String getExposureProgramDescription()
    523453    {
    524         // '1' means manual control, '2' program normal, '3' aperture priority,
    525         // '4' shutter priority, '5' program creative (slow program),
    526         // '6' program action(high-speed program), '7' portrait mode, '8' landscape mode.
    527         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXPOSURE_PROGRAM);
    528         if (value==null)
    529             return null;
    530         switch (value) {
    531             case 1: return "Manual control";
    532             case 2: return "Program normal";
    533             case 3: return "Aperture priority";
    534             case 4: return "Shutter priority";
    535             case 5: return "Program creative (slow program)";
    536             case 6: return "Program action (high-speed program)";
    537             case 7: return "Portrait mode";
    538             case 8: return "Landscape mode";
    539             default:
    540                 return "Unknown program (" + value + ")";
    541         }
     454        return getIndexedDescription(TAG_EXPOSURE_PROGRAM,
     455            1,
     456            "Manual control",
     457            "Program normal",
     458            "Aperture priority",
     459            "Shutter priority",
     460            "Program creative (slow program)",
     461            "Program action (high-speed program)",
     462            "Portrait mode",
     463            "Landscape mode"
     464        );
    542465    }
    543466
     
    545468    public String getYCbCrSubsamplingDescription()
    546469    {
    547         int[] positions = _directory.getIntArray(ExifSubIFDDirectory.TAG_YCBCR_SUBSAMPLING);
    548         if (positions==null)
     470        int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING);
     471        if (positions == null)
    549472            return null;
    550473        if (positions[0] == 2 && positions[1] == 1) {
     
    564487        // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr
    565488        // plane format.
    566         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_PLANAR_CONFIGURATION);
    567         if (value==null)
    568             return null;
    569         switch (value) {
    570             case 1: return "Chunky (contiguous for each subsampling pixel)";
    571             case 2: return "Separate (Y-plane/Cb-plane/Cr-plane format)";
    572             default:
    573                 return "Unknown configuration";
    574         }
     489        return getIndexedDescription(TAG_PLANAR_CONFIGURATION,
     490            1,
     491            "Chunky (contiguous for each subsampling pixel)",
     492            "Separate (Y-plane/Cb-plane/Cr-plane format)"
     493        );
    575494    }
    576495
     
    578497    public String getSamplesPerPixelDescription()
    579498    {
    580         String value = _directory.getString(ExifSubIFDDirectory.TAG_SAMPLES_PER_PIXEL);
    581         return value==null ? null : value + " samples/pixel";
     499        String value = _directory.getString(TAG_SAMPLES_PER_PIXEL);
     500        return value == null ? null : value + " samples/pixel";
    582501    }
    583502
     
    585504    public String getRowsPerStripDescription()
    586505    {
    587         final String value = _directory.getString(ExifSubIFDDirectory.TAG_ROWS_PER_STRIP);
    588         return value==null ? null : value + " rows/strip";
     506        final String value = _directory.getString(TAG_ROWS_PER_STRIP);
     507        return value == null ? null : value + " rows/strip";
    589508    }
    590509
     
    592511    public String getStripByteCountsDescription()
    593512    {
    594         final String value = _directory.getString(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS);
    595         return value==null ? null : value + " bytes";
     513        final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS);
     514        return value == null ? null : value + " bytes";
    596515    }
    597516
     
    600519    {
    601520        // Shows the color space of the image data components
    602         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION);
    603         if (value==null)
     521        Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION);
     522        if (value == null)
    604523            return null;
    605524        switch (value) {
     
    626545    public String getBitsPerSampleDescription()
    627546    {
    628         String value = _directory.getString(ExifSubIFDDirectory.TAG_BITS_PER_SAMPLE);
    629         return value==null ? null : value + " bits/component/pixel";
     547        String value = _directory.getString(TAG_BITS_PER_SAMPLE);
     548        return value == null ? null : value + " bits/component/pixel";
    630549    }
    631550
     
    633552    public String getFocalPlaneXResolutionDescription()
    634553    {
    635         Rational rational = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_PLANE_X_RES);
    636         if (rational==null)
     554        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_X_RESOLUTION);
     555        if (rational == null)
    637556            return null;
    638557        final String unit = getFocalPlaneResolutionUnitDescription();
    639558        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
    640             + (unit==null ? "" : " " + unit.toLowerCase());
     559            + (unit == null ? "" : " " + unit.toLowerCase());
    641560    }
    642561
     
    644563    public String getFocalPlaneYResolutionDescription()
    645564    {
    646         Rational rational = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_PLANE_Y_RES);
    647         if (rational==null)
     565        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_Y_RESOLUTION);
     566        if (rational == null)
    648567            return null;
    649568        final String unit = getFocalPlaneResolutionUnitDescription();
    650569        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
    651             + (unit==null ? "" : " " + unit.toLowerCase());
     570            + (unit == null ? "" : " " + unit.toLowerCase());
    652571    }
    653572
     
    655574    public String getFocalPlaneResolutionUnitDescription()
    656575    {
    657         // Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit,
    658         // '2' inch, '3' centimeter.
    659         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FOCAL_PLANE_UNIT);
    660         if (value==null)
    661             return null;
    662         switch (value) {
    663             case 1: return "(No unit)";
    664             case 2: return "Inches";
    665             case 3: return "cm";
    666             default:
    667                 return "";
    668         }
     576        // Unit of FocalPlaneXResolution/FocalPlaneYResolution.
     577        // '1' means no-unit, '2' inch, '3' centimeter.
     578        return getIndexedDescription(TAG_FOCAL_PLANE_RESOLUTION_UNIT,
     579            1,
     580            "(No unit)",
     581            "Inches",
     582            "cm"
     583        );
    669584    }
    670585
     
    672587    public String getExifImageWidthDescription()
    673588    {
    674         final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH);
    675         if (value==null)
    676             return null;
    677         return value + " pixels";
     589        final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_WIDTH);
     590        return value == null ? null : value + " pixels";
    678591    }
    679592
     
    681594    public String getExifImageHeightDescription()
    682595    {
    683         final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT);
    684         if (value==null)
    685             return null;
    686         return value + " pixels";
     596        final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_HEIGHT);
     597        return value == null ? null : value + " pixels";
    687598    }
    688599
     
    690601    public String getColorSpaceDescription()
    691602    {
    692         final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_COLOR_SPACE);
    693         if (value==null)
     603        final Integer value = _directory.getInteger(TAG_COLOR_SPACE);
     604        if (value == null)
    694605            return null;
    695606        if (value == 1)
     
    697608        if (value == 65535)
    698609            return "Undefined";
    699         return "Unknown";
     610        return "Unknown (" + value + ")";
    700611    }
    701612
     
    703614    public String getFocalLengthDescription()
    704615    {
    705         Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_LENGTH);
    706         if (value==null)
     616        Rational value = _directory.getRational(TAG_FOCAL_LENGTH);
     617        if (value == null)
    707618            return null;
    708619        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
     
    714625    {
    715626        /*
    716          * This is a bitmask.
     627         * This is a bit mask.
    717628         * 0 = flash fired
    718629         * 1 = return detected
     
    724635         */
    725636
    726         final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FLASH);
    727 
    728         if (value==null)
     637        final Integer value = _directory.getInteger(TAG_FLASH);
     638
     639        if (value == null)
    729640            return null;
    730641
    731642        StringBuilder sb = new StringBuilder();
    732643
    733         if ((value & 0x1)!=0)
     644        if ((value & 0x1) != 0)
    734645            sb.append("Flash fired");
    735646        else
     
    737648
    738649        // check if we're able to detect a return, before we mention it
    739         if ((value & 0x4)!=0)
    740         {
    741             if ((value & 0x2)!=0)
     650        if ((value & 0x4) != 0) {
     651            if ((value & 0x2) != 0)
    742652                sb.append(", return detected");
    743653            else
     
    745655        }
    746656
    747         if ((value & 0x10)!=0)
     657        if ((value & 0x10) != 0)
    748658            sb.append(", auto");
    749659
    750         if ((value & 0x40)!=0)
     660        if ((value & 0x40) != 0)
    751661            sb.append(", red-eye reduction");
    752662
     
    760670        // '17' standard light A, '18' standard light B, '19' standard light C, '20' D55,
    761671        // '21' D65, '22' D75, '255' other.
    762         final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_WHITE_BALANCE);
    763         if (value==null)
     672        final Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
     673        if (value == null)
    764674            return null;
    765675        switch (value) {
     
    786696        // '0' means unknown, '1' average, '2' center weighted average, '3' spot
    787697        // '4' multi-spot, '5' multi-segment, '6' partial, '255' other
    788         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_METERING_MODE);
    789         if (value==null)
     698        Integer value = _directory.getInteger(TAG_METERING_MODE);
     699        if (value == null)
    790700            return null;
    791701        switch (value) {
     
    806716    public String getSubjectDistanceDescription()
    807717    {
    808         Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE);
    809         if (value==null)
     718        Rational value = _directory.getRational(TAG_SUBJECT_DISTANCE);
     719        if (value == null)
    810720            return null;
    811721        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
     
    816726    public String getCompressedAverageBitsPerPixelDescription()
    817727    {
    818         Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL);
    819         if (value==null)
     728        Rational value = _directory.getRational(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL);
     729        if (value == null)
    820730            return null;
    821731        String ratio = value.toSimpleString(_allowDecimalRepresentationOfRationals);
    822         if (value.isInteger() && value.intValue() == 1) {
    823             return ratio + " bit/pixel";
    824         } else {
    825             return ratio + " bits/pixel";
    826         }
     732        return value.isInteger() && value.intValue() == 1
     733            ? ratio + " bit/pixel"
     734            : ratio + " bits/pixel";
    827735    }
    828736
     
    830738    public String getExposureTimeDescription()
    831739    {
    832         String value = _directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME);
    833         return value==null ? null : value + " sec";
     740        String value = _directory.getString(TAG_EXPOSURE_TIME);
     741        return value == null ? null : value + " sec";
    834742    }
    835743
     
    847755        // description (spotted bug using a Canon EOS 300D)
    848756        // thanks also to Gli Blr for spotting this bug
    849         Float apexValue = _directory.getFloatObject(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
    850         if (apexValue==null)
    851             return null;
    852         if (apexValue<=1) {
    853             float apexPower = (float)(1/(Math.exp(apexValue*Math.log(2))));
     757        Float apexValue = _directory.getFloatObject(TAG_SHUTTER_SPEED);
     758        if (apexValue == null)
     759            return null;
     760        if (apexValue <= 1) {
     761            float apexPower = (float)(1 / (Math.exp(apexValue * Math.log(2))));
    854762            long apexPower10 = Math.round((double)apexPower * 10.0);
    855             float fApexPower = (float) apexPower10 / 10.0f;
     763            float fApexPower = (float)apexPower10 / 10.0f;
    856764            return fApexPower + " sec";
    857765        } else {
    858             int apexPower = (int)((Math.exp(apexValue*Math.log(2))));
     766            int apexPower = (int)((Math.exp(apexValue * Math.log(2))));
    859767            return "1/" + apexPower + " sec";
    860768        }
     
    879787        return sb.toString();
    880788*/
    881 
    882789    }
    883790
     
    885792    public String getFNumberDescription()
    886793    {
    887         Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_FNUMBER);
    888         if (value==null)
     794        Rational value = _directory.getRational(TAG_FNUMBER);
     795        if (value == null)
    889796            return null;
    890797        return "F" + SimpleDecimalFormatter.format(value.doubleValue());
     
    897804        // '4' Three-chip color area sensor, '5' Color sequential area sensor
    898805        // '7' Trilinear sensor '8' Color sequential linear sensor,  'Other' reserved
    899         Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SENSING_METHOD);
    900         if (value==null)
    901             return null;
    902         switch (value) {
    903             case 1: return "(Not defined)";
    904             case 2: return "One-chip color area sensor";
    905             case 3: return "Two-chip color area sensor";
    906             case 4: return "Three-chip color area sensor";
    907             case 5: return "Color sequential area sensor";
    908             case 7: return "Trilinear sensor";
    909             case 8: return "Color sequential linear sensor";
    910             default:
    911                 return "";
    912         }
     806        return getIndexedDescription(TAG_SENSING_METHOD,
     807            1,
     808            "(Not defined)",
     809            "One-chip color area sensor",
     810            "Two-chip color area sensor",
     811            "Three-chip color area sensor",
     812            "Color sequential area sensor",
     813            null,
     814            "Trilinear sensor",
     815            "Color sequential linear sensor"
     816        );
    913817    }
    914818
     
    916820    public String getComponentConfigurationDescription()
    917821    {
    918         int[] components = _directory.getIntArray(ExifSubIFDDirectory.TAG_COMPONENTS_CONFIGURATION);
    919         if (components==null)
     822        int[] components = _directory.getIntArray(TAG_COMPONENTS_CONFIGURATION);
     823        if (components == null)
    920824            return null;
    921825        String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"};
  • trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121package com.drew.metadata.exif;
     
    2929 * Describes Exif tags from the SubIFD directory.
    3030 *
    31  * @author Drew Noakes http://drewnoakes.com
     31 * @author Drew Noakes https://drewnoakes.com
    3232 */
    3333public class ExifSubIFDDirectory extends Directory
     
    133133    /**
    134134     * Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524.
    135      * <p/>
     135     * <p>
    136136     * OECF is the relationship between the camera optical input and the image values.
    137      * <p/>
     137     * <p>
    138138     * The values are:
    139139     * <ul>
     
    260260     */
    261261    public static final int TAG_FOCAL_LENGTH = 0x920A;
     262
     263    /**
     264     * This tag holds the Exif Makernote. Makernotes are free to be in any format, though they are often IFDs.
     265     * To determine the format, we consider the starting bytes of the makernote itself and sometimes the
     266     * camera model and make.
     267     * <p>
     268     * The component count for this tag includes all of the bytes needed for the makernote.
     269     */
     270    public static final int TAG_MAKERNOTE = 0x927C;
     271
    262272    public static final int TAG_USER_COMMENT = 0x9286;
     273
    263274    public static final int TAG_SUBSECOND_TIME = 0x9290;
    264275    public static final int TAG_SUBSECOND_TIME_ORIGINAL = 0x9291;
    265276    public static final int TAG_SUBSECOND_TIME_DIGITIZED = 0x9292;
     277
    266278    public static final int TAG_FLASHPIX_VERSION = 0xA000;
    267279    /**
     
    274286    public static final int TAG_EXIF_IMAGE_HEIGHT = 0xA003;
    275287    public static final int TAG_RELATED_SOUND_FILE = 0xA004;
    276     public static final int TAG_FOCAL_PLANE_X_RES = 0xA20E;
    277     public static final int TAG_FOCAL_PLANE_Y_RES = 0xA20F;
     288
     289    /** This tag is a pointer to the Exif Interop IFD. */
     290    public static final int TAG_INTEROP_OFFSET = 0xA005;
     291
     292    public static final int TAG_FOCAL_PLANE_X_RESOLUTION = 0xA20E;
     293    public static final int TAG_FOCAL_PLANE_Y_RESOLUTION = 0xA20F;
    278294    /**
    279295     * Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit,
     
    285301     * been changed to use value '2' but it doesn't match to actual value also.
    286302     */
    287     public static final int TAG_FOCAL_PLANE_UNIT = 0xA210;
     303    public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT = 0xA210;
    288304    public static final int TAG_EXPOSURE_INDEX = 0xA215;
    289305    public static final int TAG_SENSING_METHOD = 0xA217;
     
    512528        _tagNameMap.put(0x0200, "JPEG Proc");
    513529        _tagNameMap.put(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL, "Compressed Bits Per Pixel");
    514         _tagNameMap.put(0x927C, "Maker Note");
    515         _tagNameMap.put(0xA005, "Interoperability Offset");
     530        _tagNameMap.put(TAG_MAKERNOTE, "Makernote");
     531        _tagNameMap.put(TAG_INTEROP_OFFSET, "Interoperability Offset");
    516532
    517533        _tagNameMap.put(TAG_NEW_SUBFILE_TYPE, "New Subfile Type");
     
    586602        _tagNameMap.put(TAG_SPATIAL_FREQ_RESPONSE_2, "Spatial Frequency Response");
    587603        // 0x920E in TIFF/EP
    588         _tagNameMap.put(TAG_FOCAL_PLANE_X_RES, "Focal Plane X Resolution");
     604        _tagNameMap.put(TAG_FOCAL_PLANE_X_RESOLUTION, "Focal Plane X Resolution");
    589605        // 0x920F in TIFF/EP
    590         _tagNameMap.put(TAG_FOCAL_PLANE_Y_RES, "Focal Plane Y Resolution");
     606        _tagNameMap.put(TAG_FOCAL_PLANE_Y_RESOLUTION, "Focal Plane Y Resolution");
    591607        // 0x9210 in TIFF/EP
    592         _tagNameMap.put(TAG_FOCAL_PLANE_UNIT, "Focal Plane Resolution Unit");
     608        _tagNameMap.put(TAG_FOCAL_PLANE_RESOLUTION_UNIT, "Focal Plane Resolution Unit");
    593609        // 0x9214 in TIFF/EP
    594610        _tagNameMap.put(TAG_SUBJECT_LOCATION_2, "Subject Location");
     
    614630        _tagNameMap.put(TAG_SUBJECT_DISTANCE_RANGE, "Subject Distance Range");
    615631        _tagNameMap.put(TAG_IMAGE_UNIQUE_ID, "Unique Image ID");
    616        
     632
    617633        _tagNameMap.put(TAG_CAMERA_OWNER_NAME, "Camera Owner Name");
    618634        _tagNameMap.put(TAG_BODY_SERIAL_NUMBER, "Body Serial Number");
     
    634650    }
    635651
     652    @Override
    636653    @NotNull
    637654    public String getName()
     
    640657    }
    641658
     659    @Override
    642660    @NotNull
    643661    protected HashMap<Integer, String> getTagNameMap()
  • trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java

    r6127 r8132  
    11/*
    2  * Copyright 2002-2012 Drew Noakes
     2 * Copyright 2002-2015 Drew Noakes
    33 *
    44 *    Licensed under the Apache License, Version 2.0 (the "License");
     
    1616 * More information about this project is available at:
    1717 *
    18  *    http://drewnoakes.com/code/exif/
    19  *    http://code.google.com/p/metadata-extractor/
     18 *    https://drewnoakes.com/code/exif/
     19 *    https://github.com/drewnoakes/metadata-extractor
    2020 */
    2121
     
    2727import com.drew.metadata.TagDescriptor;
    2828
     29import static com.drew.metadata.exif.ExifThumbnailDirectory.*;
     30
    2931/**
    30  * Provides human-readable string representations of tag values stored in a <code>ExifThumbnailDirectory</code>.
    31  *
    32  * @author Drew Noakes http://drewnoakes.com
     32 * Provides human-readable string representations of tag values stored in a {@link ExifThumbnailDirectory}.
     33 *