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;
008
009import org.openstreetmap.josm.Main;
010import org.openstreetmap.josm.data.coor.LatLon;
011import org.openstreetmap.josm.tools.date.DateUtils;
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.ExifDirectoryBase;
021import com.drew.metadata.exif.ExifIFD0Directory;
022import com.drew.metadata.exif.ExifSubIFDDirectory;
023import com.drew.metadata.exif.GpsDirectory;
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     */
041    public static Date readTime(File filename) {
042        try {
043            Metadata metadata = JpegMetadataReader.readMetadata(filename);
044            String dateStr = null;
045            String subSeconds = null;
046            for (Directory dirIt : metadata.getDirectories()) {
047                if (!(dirIt instanceof ExifDirectoryBase)) {
048                    continue;
049                }
050                for (Tag tag : dirIt.getTags()) {
051                    if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */ &&
052                            !tag.getDescription().matches("\\[[0-9]+ .+\\]")) {
053                        dateStr = tag.getDescription();
054                    }
055                    if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */ ||
056                        tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) {
057                        if (dateStr != null) {
058                            // prefer TAG_DATETIME_ORIGINAL
059                            dateStr = tag.getDescription();
060                        }
061                    }
062                    if (tag.getTagType() == ExifIFD0Directory.TAG_SUBSECOND_TIME_ORIGINAL) {
063                        subSeconds = tag.getDescription();
064                    }
065                }
066            }
067            if (dateStr != null) {
068                dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228
069                final Date date = DateUtils.fromString(dateStr);
070                if (subSeconds != null) {
071                    try {
072                        date.setTime(date.getTime() + (long) (1000L * Double.parseDouble("0." + subSeconds)));
073                    } catch (NumberFormatException e) {
074                        Main.warn("Failed parsing sub seconds from [{0}]", subSeconds);
075                        Main.warn(e);
076                    }
077                }
078                return date;
079            }
080        } catch (UncheckedParseException | JpegProcessingException | IOException e) {
081            Main.error(e);
082        }
083        return null;
084    }
085
086    /**
087     * Returns the image orientation of the given JPEG file.
088     * @param filename The JPEG file to read
089     * @return The image orientation as an {@code int}. Default value is 1. Possible values are listed in EXIF spec as follows:<br><ol>
090     * <li>The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.</li>
091     * <li>The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.</li>
092     * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.</li>
093     * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.</li>
094     * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.</li>
095     * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.</li>
096     * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.</li>
097     * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.</li></ol>
098     * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a>
099     * @see <a href="http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto">
100     * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto</a>
101     */
102    public static Integer readOrientation(File filename) {
103        try {
104            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
105            final Directory dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
106            return dir == null ? null : dir.getInteger(ExifIFD0Directory.TAG_ORIENTATION);
107        } catch (JpegProcessingException | IOException e) {
108            Main.error(e);
109        }
110        return null;
111    }
112
113    /**
114     * Returns the geolocation of the given JPEG file.
115     * @param filename The JPEG file to read
116     * @return The lat/lon read in the EXIF section, or {@code null} if not found
117     * @since 6209
118     */
119    public static LatLon readLatLon(File filename) {
120        try {
121            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
122            final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
123            return readLatLon(dirGps);
124        } catch (JpegProcessingException | IOException | MetadataException e) {
125            Main.error(e);
126        }
127        return null;
128    }
129
130    /**
131     * Returns the geolocation of the given EXIF GPS directory.
132     * @param dirGps The EXIF GPS directory
133     * @return The lat/lon read in the EXIF section, or {@code null} if {@code dirGps} is null
134     * @throws MetadataException if invalid metadata is given
135     * @since 6209
136     */
137    public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException {
138        if (dirGps != null) {
139            double lat = readAxis(dirGps, GpsDirectory.TAG_LATITUDE, GpsDirectory.TAG_LATITUDE_REF, 'S');
140            double lon = readAxis(dirGps, GpsDirectory.TAG_LONGITUDE, GpsDirectory.TAG_LONGITUDE_REF, 'W');
141            return new LatLon(lat, lon);
142        }
143        return null;
144    }
145
146    /**
147     * Returns the direction of the given JPEG file.
148     * @param filename The JPEG file to read
149     * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99),
150     * or {@code null} if missing or if {@code dirGps} is null
151     * @since 6209
152     */
153    public static Double readDirection(File filename) {
154        try {
155            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
156            final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
157            return readDirection(dirGps);
158        } catch (JpegProcessingException | IOException e) {
159            Main.error(e);
160        }
161        return null;
162    }
163
164    /**
165     * Returns the direction of the given EXIF GPS directory.
166     * @param dirGps The EXIF GPS directory
167     * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99),
168     * or {@code null} if missing or if {@code dirGps} is null
169     * @since 6209
170     */
171    public static Double readDirection(GpsDirectory dirGps) {
172        if (dirGps != null) {
173            Rational direction = dirGps.getRational(GpsDirectory.TAG_IMG_DIRECTION);
174            if (direction != null) {
175                return direction.doubleValue();
176            }
177        }
178        return null;
179    }
180
181    private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException {
182        double value;
183        Rational[] components = dirGps.getRationalArray(gpsTag);
184        if (components != null) {
185            double deg = components[0].doubleValue();
186            double min = components[1].doubleValue();
187            double sec = components[2].doubleValue();
188
189            if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
190                throw new IllegalArgumentException("deg, min and sec are NaN");
191
192            value = Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600));
193
194            if (dirGps.getString(gpsTagRef).charAt(0) == cRef) {
195                value = -value;
196            }
197        } else {
198            // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220)
199            value = dirGps.getDouble(gpsTag);
200        }
201        return value;
202    }
203
204    /**
205     * Returns a Transform that fixes the image orientation.
206     *
207     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated as 1.
208     * @param orientation the exif-orientation of the image
209     * @param width the original width of the image
210     * @param height the original height of the image
211     * @return a transform that rotates the image, so it is upright
212     */
213    public static AffineTransform getRestoreOrientationTransform(final int orientation, final int width, final int height) {
214        final int q;
215        final double ax, ay;
216        switch (orientation) {
217        case 8:
218            q = -1;
219            ax = width / 2d;
220            ay = width / 2d;
221            break;
222        case 3:
223            q = 2;
224            ax = width / 2d;
225            ay = height / 2d;
226            break;
227        case 6:
228            q = 1;
229            ax = height / 2d;
230            ay = height / 2d;
231            break;
232        default:
233            q = 0;
234            ax = 0;
235            ay = 0;
236        }
237        return AffineTransform.getQuadrantRotateInstance(q, ax, ay);
238    }
239
240    /**
241     * Check, if the given orientation switches width and height of the image.
242     * E.g. 90 degree rotation
243     *
244     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
245     * as 1.
246     * @param orientation the exif-orientation of the image
247     * @return true, if it switches width and height
248     */
249    public static boolean orientationSwitchesDimensions(int orientation) {
250        return orientation == 6 || orientation == 8;
251    }
252
253    /**
254     * Check, if the given orientation requires any correction to the image.
255     *
256     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
257     * as 1.
258     * @param orientation the exif-orientation of the image
259     * @return true, unless the orientation value is 1 or unsupported.
260     */
261    public static boolean orientationNeedsCorrection(int orientation) {
262        return orientation == 3 || orientation == 6 || orientation == 8;
263    }
264}