001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import java.awt.geom.AffineTransform;
005import java.io.File;
006import java.io.IOException;
007import java.util.Date;
008import java.util.concurrent.TimeUnit;
009
010import org.openstreetmap.josm.data.SystemOfMeasurement;
011import org.openstreetmap.josm.data.coor.LatLon;
012import org.openstreetmap.josm.tools.date.DateUtils;
013
014import com.drew.imaging.jpeg.JpegMetadataReader;
015import com.drew.imaging.jpeg.JpegProcessingException;
016import com.drew.lang.Rational;
017import com.drew.metadata.Directory;
018import com.drew.metadata.Metadata;
019import com.drew.metadata.MetadataException;
020import com.drew.metadata.Tag;
021import com.drew.metadata.exif.ExifDirectoryBase;
022import com.drew.metadata.exif.ExifIFD0Directory;
023import com.drew.metadata.exif.ExifSubIFDDirectory;
024import com.drew.metadata.exif.GpsDirectory;
025
026/**
027 * Read out EXIF information from a JPEG file
028 * @author Imi
029 * @since 99
030 */
031public final class ExifReader {
032
033    private ExifReader() {
034        // Hide default constructor for utils classes
035    }
036
037    /**
038     * Returns the date/time from the given JPEG file.
039     * @param filename The JPEG file to read
040     * @return The date/time read in the EXIF section, or {@code null} if not found
041     */
042    public static Date readTime(File filename) {
043        try {
044            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
045            return readTime(metadata);
046        } catch (JpegProcessingException | IOException e) {
047            Logging.error(e);
048        }
049        return null;
050    }
051
052    /**
053     * Returns the date/time from the given JPEG file.
054     * @param metadata The EXIF metadata
055     * @return The date/time read in the EXIF section, or {@code null} if not found
056     * @since 11745
057     */
058    public static Date readTime(Metadata metadata) {
059        try {
060            String dateTimeOrig = null;
061            String dateTime = null;
062            String dateTimeDig = null;
063            String subSecOrig = null;
064            String subSec = null;
065            String subSecDig = null;
066            // The date fields are preferred in this order: DATETIME_ORIGINAL
067            // (0x9003), DATETIME (0x0132), DATETIME_DIGITIZED (0x9004).  Some
068            // cameras store the fields in the wrong directory, so all
069            // directories are searched.  Assume that the order of the fields
070            // in the directories is random.
071            for (Directory dirIt : metadata.getDirectories()) {
072                if (!(dirIt instanceof ExifDirectoryBase)) {
073                    continue;
074                }
075                for (Tag tag : dirIt.getTags()) {
076                    if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */ &&
077                            !tag.getDescription().matches("\\[[0-9]+ .+\\]")) {
078                        dateTimeOrig = tag.getDescription();
079                    } else if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */) {
080                        dateTime = tag.getDescription();
081                    } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) {
082                        dateTimeDig = tag.getDescription();
083                    } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_ORIGINAL /* 0x9291 */) {
084                        subSecOrig = tag.getDescription();
085                    } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME /* 0x9290 */) {
086                        subSec = tag.getDescription();
087                    } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_DIGITIZED /* 0x9292 */) {
088                        subSecDig = tag.getDescription();
089                    }
090                }
091            }
092            String dateStr = null;
093            String subSeconds = null;
094            if (dateTimeOrig != null) {
095                // prefer TAG_DATETIME_ORIGINAL
096                dateStr = dateTimeOrig;
097                subSeconds = subSecOrig;
098            } else if (dateTime != null) {
099                // TAG_DATETIME is second choice, see #14209
100                dateStr = dateTime;
101                subSeconds = subSec;
102            } else if (dateTimeDig != null) {
103                dateStr = dateTimeDig;
104                subSeconds = subSecDig;
105            }
106            if (dateStr != null) {
107                dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228
108                final Date date = DateUtils.fromString(dateStr);
109                if (subSeconds != null) {
110                    try {
111                        date.setTime(date.getTime() + (long) (TimeUnit.SECONDS.toMillis(1) * Double.parseDouble("0." + subSeconds)));
112                    } catch (NumberFormatException e) {
113                        Logging.warn("Failed parsing sub seconds from [{0}]", subSeconds);
114                        Logging.warn(e);
115                    }
116                }
117                return date;
118            }
119        } catch (UncheckedParseException e) {
120            Logging.error(e);
121        }
122        return null;
123    }
124
125    /**
126     * Returns the image orientation of the given JPEG file.
127     * @param filename The JPEG file to read
128     * @return The image orientation as an {@code int}. Default value is 1. Possible values are listed in EXIF spec as follows:<br><ol>
129     * <li>The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.</li>
130     * <li>The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.</li>
131     * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.</li>
132     * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.</li>
133     * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.</li>
134     * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.</li>
135     * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.</li>
136     * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.</li></ol>
137     * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a>
138     * @see <a href="http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto">
139     * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto</a>
140     */
141    public static Integer readOrientation(File filename) {
142        try {
143            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
144            final Directory dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
145            return dir == null ? null : dir.getInteger(ExifIFD0Directory.TAG_ORIENTATION);
146        } catch (JpegProcessingException | IOException e) {
147            Logging.error(e);
148        }
149        return null;
150    }
151
152    /**
153     * Returns the geolocation of the given JPEG file.
154     * @param filename The JPEG file to read
155     * @return The lat/lon read in the EXIF section, or {@code null} if not found
156     * @since 6209
157     */
158    public static LatLon readLatLon(File filename) {
159        try {
160            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
161            final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
162            return readLatLon(dirGps);
163        } catch (JpegProcessingException | IOException | MetadataException e) {
164            Logging.error(e);
165        }
166        return null;
167    }
168
169    /**
170     * Returns the geolocation of the given EXIF GPS directory.
171     * @param dirGps The EXIF GPS directory
172     * @return The lat/lon read in the EXIF section, or {@code null} if {@code dirGps} is null
173     * @throws MetadataException if invalid metadata is given
174     * @since 6209
175     */
176    public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException {
177        if (dirGps != null) {
178            double lat = readAxis(dirGps, GpsDirectory.TAG_LATITUDE, GpsDirectory.TAG_LATITUDE_REF, 'S');
179            double lon = readAxis(dirGps, GpsDirectory.TAG_LONGITUDE, GpsDirectory.TAG_LONGITUDE_REF, 'W');
180            return new LatLon(lat, lon);
181        }
182        return null;
183    }
184
185    /**
186     * Returns the direction of the given JPEG file.
187     * @param filename The JPEG file to read
188     * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99),
189     * or {@code null} if not found
190     * @since 6209
191     */
192    public static Double readDirection(File filename) {
193        try {
194            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
195            final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
196            return readDirection(dirGps);
197        } catch (JpegProcessingException | IOException e) {
198            Logging.error(e);
199        }
200        return null;
201    }
202
203    /**
204     * Returns the direction of the given EXIF GPS directory.
205     * @param dirGps The EXIF GPS directory
206     * @return The direction of the image when it was captured (in degrees between 0.0 and 359.99),
207     * or {@code null} if missing or if {@code dirGps} is null
208     * @since 6209
209     */
210    public static Double readDirection(GpsDirectory dirGps) {
211        if (dirGps != null) {
212            Rational direction = dirGps.getRational(GpsDirectory.TAG_IMG_DIRECTION);
213            if (direction != null) {
214                return direction.doubleValue();
215            }
216        }
217        return null;
218    }
219
220    private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException {
221        double value;
222        Rational[] components = dirGps.getRationalArray(gpsTag);
223        if (components != null) {
224            double deg = components[0].doubleValue();
225            double min = components[1].doubleValue();
226            double sec = components[2].doubleValue();
227
228            if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
229                throw new IllegalArgumentException("deg, min and sec are NaN");
230
231            value = Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600));
232
233            String s = dirGps.getString(gpsTagRef);
234            if (s != null && s.charAt(0) == cRef) {
235                value = -value;
236            }
237        } else {
238            // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220)
239            value = dirGps.getDouble(gpsTag);
240        }
241        return value;
242    }
243
244    /**
245     * Returns the speed of the given JPEG file.
246     * @param filename The JPEG file to read
247     * @return The speed of the camera when the image was captured (in km/h),
248     *         or {@code null} if not found
249     * @since 11745
250     */
251    public static Double readSpeed(File filename) {
252        try {
253            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
254            final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
255            return readSpeed(dirGps);
256        } catch (JpegProcessingException | IOException e) {
257            Logging.error(e);
258        }
259        return null;
260    }
261
262    /**
263     * Returns the speed of the given EXIF GPS directory.
264     * @param dirGps The EXIF GPS directory
265     * @return The speed of the camera when the image was captured (in km/h),
266     *         or {@code null} if missing or if {@code dirGps} is null
267     * @since 11745
268     */
269    public static Double readSpeed(GpsDirectory dirGps) {
270        if (dirGps != null) {
271            Double speed = dirGps.getDoubleObject(GpsDirectory.TAG_SPEED);
272            if (speed != null) {
273                final String speedRef = dirGps.getString(GpsDirectory.TAG_SPEED_REF);
274                if ("M".equalsIgnoreCase(speedRef)) {
275                    // miles per hour
276                    speed *= SystemOfMeasurement.IMPERIAL.bValue / 1000;
277                } else if ("N".equalsIgnoreCase(speedRef)) {
278                    // knots == nautical miles per hour
279                    speed *= SystemOfMeasurement.NAUTICAL_MILE.bValue / 1000;
280                }
281                // default is K (km/h)
282                return speed;
283            }
284        }
285        return null;
286    }
287
288    /**
289     * Returns the elevation of the given JPEG file.
290     * @param filename The JPEG file to read
291     * @return The elevation of the camera when the image was captured (in m),
292     *         or {@code null} if not found
293     * @since 11745
294     */
295    public static Double readElevation(File filename) {
296        try {
297            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
298            final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
299            return readElevation(dirGps);
300        } catch (JpegProcessingException | IOException e) {
301            Logging.error(e);
302        }
303        return null;
304    }
305
306    /**
307     * Returns the elevation of the given EXIF GPS directory.
308     * @param dirGps The EXIF GPS directory
309     * @return The elevation of the camera when the image was captured (in m),
310     *         or {@code null} if missing or if {@code dirGps} is null
311     * @since 11745
312     */
313    public static Double readElevation(GpsDirectory dirGps) {
314        if (dirGps != null) {
315            Double ele = dirGps.getDoubleObject(GpsDirectory.TAG_ALTITUDE);
316            if (ele != null) {
317                final Integer d = dirGps.getInteger(GpsDirectory.TAG_ALTITUDE_REF);
318                if (d != null && d.intValue() == 1) {
319                    ele *= -1;
320                }
321                return ele;
322            }
323        }
324        return null;
325    }
326
327    /**
328     * Returns a Transform that fixes the image orientation.
329     *
330     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated as 1.
331     * @param orientation the exif-orientation of the image
332     * @param width the original width of the image
333     * @param height the original height of the image
334     * @return a transform that rotates the image, so it is upright
335     */
336    public static AffineTransform getRestoreOrientationTransform(final int orientation, final int width, final int height) {
337        final int q;
338        final double ax, ay;
339        switch (orientation) {
340        case 8:
341            q = -1;
342            ax = width / 2d;
343            ay = width / 2d;
344            break;
345        case 3:
346            q = 2;
347            ax = width / 2d;
348            ay = height / 2d;
349            break;
350        case 6:
351            q = 1;
352            ax = height / 2d;
353            ay = height / 2d;
354            break;
355        default:
356            q = 0;
357            ax = 0;
358            ay = 0;
359        }
360        return AffineTransform.getQuadrantRotateInstance(q, ax, ay);
361    }
362
363    /**
364     * Check, if the given orientation switches width and height of the image.
365     * E.g. 90 degree rotation
366     *
367     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
368     * as 1.
369     * @param orientation the exif-orientation of the image
370     * @return true, if it switches width and height
371     */
372    public static boolean orientationSwitchesDimensions(int orientation) {
373        return orientation == 6 || orientation == 8;
374    }
375
376    /**
377     * Check, if the given orientation requires any correction to the image.
378     *
379     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
380     * as 1.
381     * @param orientation the exif-orientation of the image
382     * @return true, unless the orientation value is 1 or unsupported.
383     */
384    public static boolean orientationNeedsCorrection(int orientation) {
385        return orientation == 3 || orientation == 6 || orientation == 8;
386    }
387}