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