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