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