001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import java.io.File;
005import java.io.IOException;
006import java.text.ParseException;
007import java.util.Date;
008
009import org.openstreetmap.josm.Main;
010import org.openstreetmap.josm.data.coor.LatLon;
011import org.openstreetmap.josm.tools.date.PrimaryDateParser;
012
013import com.drew.imaging.jpeg.JpegMetadataReader;
014import com.drew.imaging.jpeg.JpegProcessingException;
015import com.drew.lang.Rational;
016import com.drew.metadata.Directory;
017import com.drew.metadata.Metadata;
018import com.drew.metadata.MetadataException;
019import com.drew.metadata.Tag;
020import com.drew.metadata.exif.ExifIFD0Directory;
021import com.drew.metadata.exif.ExifSubIFDDirectory;
022import com.drew.metadata.exif.GpsDirectory;
023import java.awt.geom.AffineTransform;
024
025/**
026 * Read out EXIF information from a JPEG file
027 * @author Imi
028 * @since 99
029 */
030public final class ExifReader {
031
032    private ExifReader() {
033        // Hide default constructor for utils classes
034    }
035
036    /**
037     * Returns the date/time from the given JPEG file.
038     * @param filename The JPEG file to read
039     * @return The date/time read in the EXIF section, or {@code null} if not found
040     * @throws ParseException if {@link PrimaryDateParser#parse} fails to parse date/time
041     */
042    public static Date readTime(File filename) throws ParseException {
043        try {
044            Metadata metadata = JpegMetadataReader.readMetadata(filename);
045            String dateStr = null;
046            OUTER:
047            for (Directory dirIt : metadata.getDirectories()) {
048                for (Tag tag : dirIt.getTags()) {
049                    if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */) {
050                        dateStr = tag.getDescription();
051                        break OUTER; // prefer this tag
052                    }
053                    if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */ ||
054                        tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) {
055                        dateStr = tag.getDescription();
056                    }
057                }
058            }
059            if (dateStr != null) {
060                dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228
061                return new PrimaryDateParser().parse(dateStr);
062            }
063        } catch (ParseException e) {
064            throw e;
065        } catch (Exception e) {
066            Main.error(e);
067        }
068        return null;
069    }
070
071    /**
072     * Returns the image orientation of the given JPEG file.
073     * @param filename The JPEG file to read
074     * @return The image orientation as an {@code int}. Default value is 1. Possible values are listed in EXIF spec as follows:<br><ol>
075     * <li>The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.</li>
076     * <li>The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.</li>
077     * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.</li>
078     * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.</li>
079     * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.</li>
080     * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.</li>
081     * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.</li>
082     * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.</li></ol>
083     * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a>
084     * @see <a href="http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto">http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto</a>
085     */
086    public static Integer readOrientation(File filename) {
087        try {
088            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
089            final Directory dir = metadata.getDirectory(ExifIFD0Directory.class);
090            return dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
091        } catch (JpegProcessingException | MetadataException | IOException e) {
092            Main.error(e);
093        }
094        return null;
095    }
096
097    /**
098     * Returns the geolocation of the given JPEG file.
099     * @param filename The JPEG file to read
100     * @return The lat/lon read in the EXIF section, or {@code null} if not found
101     * @since 6209
102     */
103    public static LatLon readLatLon(File filename) {
104        try {
105            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
106            final GpsDirectory dirGps = metadata.getDirectory(GpsDirectory.class);
107            return readLatLon(dirGps);
108        } catch (JpegProcessingException e) {
109            Main.error(e);
110        } catch (IOException e) {
111            Main.error(e);
112        } catch (MetadataException e) {
113            Main.error(e);
114        }
115        return null;
116    }
117
118    /**
119     * Returns the geolocation of the given EXIF GPS directory.
120     * @param dirGps The EXIF GPS directory
121     * @return The lat/lon read in the EXIF section, or {@code null} if {@code dirGps} is null
122     * @throws MetadataException
123     * @since 6209
124     */
125    public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException {
126        if (dirGps != null) {
127            double lat = readAxis(dirGps, GpsDirectory.TAG_GPS_LATITUDE, GpsDirectory.TAG_GPS_LATITUDE_REF, 'S');
128            double lon = readAxis(dirGps, GpsDirectory.TAG_GPS_LONGITUDE, GpsDirectory.TAG_GPS_LONGITUDE_REF, 'W');
129            return new LatLon(lat, lon);
130        }
131        return null;
132    }
133
134    /**
135     * Returns the direction of the given JPEG file.
136     * @param filename The JPEG file to read
137     * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99), or {@code null} if missing or if {@code dirGps} is null
138     * @since 6209
139     */
140    public static Double readDirection(File filename) {
141        try {
142            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
143            final GpsDirectory dirGps = metadata.getDirectory(GpsDirectory.class);
144            return readDirection(dirGps);
145        } catch (JpegProcessingException e) {
146            Main.error(e);
147        } catch (IOException e) {
148            Main.error(e);
149        }
150        return null;
151    }
152
153    /**
154     * Returns the direction of the given EXIF GPS directory.
155     * @param dirGps The EXIF GPS directory
156     * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99), or {@code null} if missing or if {@code dirGps} is null
157     * @since 6209
158     */
159    public static Double readDirection(GpsDirectory dirGps) {
160        if (dirGps != null) {
161            Rational direction = dirGps.getRational(GpsDirectory.TAG_GPS_IMG_DIRECTION);
162            if (direction != null) {
163                return direction.doubleValue();
164            }
165        }
166        return null;
167    }
168
169    private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException  {
170        double value;
171        Rational[] components = dirGps.getRationalArray(gpsTag);
172        if (components != null) {
173            double deg = components[0].doubleValue();
174            double min = components[1].doubleValue();
175            double sec = components[2].doubleValue();
176
177            if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
178                throw new IllegalArgumentException("deg, min and sec are NaN");
179
180            value = (Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600)));
181
182            if (dirGps.getString(gpsTagRef).charAt(0) == cRef) {
183                value = -value;
184            }
185        } else {
186            // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220)
187            value = dirGps.getDouble(gpsTag);
188        }
189        return value;
190    }
191
192    /**
193     * Returns a Transform that fixes the image orientation.
194     *
195     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
196     * as 1.
197     * @param orientation the exif-orientation of the image
198     * @param width the original width of the image
199     * @param height the original height of the image
200     * @return a transform that rotates the image, so it is upright
201     */
202    public static AffineTransform getRestoreOrientationTransform(final int orientation, final int width, final int height) {
203        final int q;
204        final double ax, ay;
205        switch (orientation) {
206        case 8:
207            q = -1;
208            ax = width / 2;
209            ay = width / 2;
210            break;
211        case 3:
212            q = 2;
213            ax = width / 2;
214            ay = height / 2;
215            break;
216        case 6:
217            q = 1;
218            ax = height / 2;
219            ay = height / 2;
220            break;
221        default:
222            q = 0;
223            ax = 0;
224            ay = 0;
225        }
226        return AffineTransform.getQuadrantRotateInstance(q, ax, ay);
227    }
228
229    /**
230     * Check, if the given orientation switches width and height of the image.
231     * E.g. 90 degree rotation
232     *
233     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
234     * as 1.
235     * @param orientation the exif-orientation of the image
236     * @return true, if it switches width and height
237     */
238    public static boolean orientationSwitchesDimensions(int orientation) {
239        return orientation == 6 || orientation == 8;
240    }
241
242    /**
243     * Check, if the given orientation requires any correction to the image.
244     *
245     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
246     * as 1.
247     * @param orientation the exif-orientation of the image
248     * @return true, unless the orientation value is 1 or unsupported.
249     */
250    public static boolean orientationNeedsCorrection(int orientation) {
251        return orientation == 3 || orientation == 6 || orientation == 8;
252    }
253}