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