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