source: josm/trunk/src/org/openstreetmap/josm/tools/ExifReader.java @ 9499

Last change on this file since 9499 was 9499, checked in by simon04, 3 years ago

fix #11685 - Use sub-second exif/xmp data

  • Property svn:eol-style set to native
File size: 10.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import java.awt.geom.AffineTransform;
5import java.io.File;
6import java.io.IOException;
7import java.util.Date;
8
9import org.openstreetmap.josm.Main;
10import org.openstreetmap.josm.data.coor.LatLon;
11import org.openstreetmap.josm.tools.date.DateUtils;
12
13import com.drew.imaging.jpeg.JpegMetadataReader;
14import com.drew.imaging.jpeg.JpegProcessingException;
15import com.drew.lang.Rational;
16import com.drew.metadata.Directory;
17import com.drew.metadata.Metadata;
18import com.drew.metadata.MetadataException;
19import com.drew.metadata.Tag;
20import com.drew.metadata.exif.ExifIFD0Directory;
21import com.drew.metadata.exif.ExifSubIFDDirectory;
22import com.drew.metadata.exif.GpsDirectory;
23
24/**
25 * Read out EXIF information from a JPEG file
26 * @author Imi
27 * @since 99
28 */
29public final class ExifReader {
30
31    private ExifReader() {
32        // Hide default constructor for utils classes
33    }
34
35    /**
36     * Returns the date/time from the given JPEG file.
37     * @param filename The JPEG file to read
38     * @return The date/time read in the EXIF section, or {@code null} if not found
39     */
40    public static Date readTime(File filename) {
41        try {
42            Metadata metadata = JpegMetadataReader.readMetadata(filename);
43            String dateStr = null;
44            String subSeconds = null;
45            for (Directory dirIt : metadata.getDirectories()) {
46                for (Tag tag : dirIt.getTags()) {
47                    if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */ &&
48                            !tag.getDescription().matches("\\[[0-9]+ .+\\]")) {
49                        dateStr = tag.getDescription();
50                    }
51                    if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */ ||
52                        tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) {
53                        if (dateStr != null) {
54                            // prefer TAG_DATETIME_ORIGINAL
55                            dateStr = tag.getDescription();
56                        }
57                    }
58                    if (tag.getTagType() == ExifIFD0Directory.TAG_SUBSECOND_TIME_ORIGINAL) {
59                        subSeconds = tag.getDescription();
60                    }
61                }
62            }
63            if (dateStr != null) {
64                dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228
65                final Date date = DateUtils.fromString(dateStr);
66                if (subSeconds != null) {
67                    try {
68                        date.setTime(date.getTime() + Integer.parseInt(subSeconds));
69                    } catch (NumberFormatException e) {
70                        Main.warn("Failed parsing sub seconds from [{0}]", subSeconds);
71                        Main.warn(e);
72                    }
73                }
74                return date;
75            }
76        } catch (Exception e) {
77            Main.error(e);
78        }
79        return null;
80    }
81
82    /**
83     * Returns the image orientation of the given JPEG file.
84     * @param filename The JPEG file to read
85     * @return The image orientation as an {@code int}. Default value is 1. Possible values are listed in EXIF spec as follows:<br><ol>
86     * <li>The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.</li>
87     * <li>The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.</li>
88     * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.</li>
89     * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.</li>
90     * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.</li>
91     * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.</li>
92     * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.</li>
93     * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.</li></ol>
94     * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a>
95     * @see <a href="http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto">
96     * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto</a>
97     */
98    public static Integer readOrientation(File filename) {
99        try {
100            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
101            final Directory dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
102            return dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
103        } catch (JpegProcessingException | MetadataException | IOException e) {
104            Main.error(e);
105        }
106        return null;
107    }
108
109    /**
110     * Returns the geolocation of the given JPEG file.
111     * @param filename The JPEG file to read
112     * @return The lat/lon read in the EXIF section, or {@code null} if not found
113     * @since 6209
114     */
115    public static LatLon readLatLon(File filename) {
116        try {
117            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
118            final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
119            return readLatLon(dirGps);
120        } catch (JpegProcessingException e) {
121            Main.error(e);
122        } catch (IOException e) {
123            Main.error(e);
124        } catch (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 e) {
159            Main.error(e);
160        } catch (IOException e) {
161            Main.error(e);
162        }
163        return null;
164    }
165
166    /**
167     * Returns the direction of the given EXIF GPS directory.
168     * @param dirGps The EXIF GPS directory
169     * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99),
170     * or {@code null} if missing or if {@code dirGps} is null
171     * @since 6209
172     */
173    public static Double readDirection(GpsDirectory dirGps) {
174        if (dirGps != null) {
175            Rational direction = dirGps.getRational(GpsDirectory.TAG_IMG_DIRECTION);
176            if (direction != null) {
177                return direction.doubleValue();
178            }
179        }
180        return null;
181    }
182
183    private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException  {
184        double value;
185        Rational[] components = dirGps.getRationalArray(gpsTag);
186        if (components != null) {
187            double deg = components[0].doubleValue();
188            double min = components[1].doubleValue();
189            double sec = components[2].doubleValue();
190
191            if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
192                throw new IllegalArgumentException("deg, min and sec are NaN");
193
194            value = (Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600)));
195
196            if (dirGps.getString(gpsTagRef).charAt(0) == cRef) {
197                value = -value;
198            }
199        } else {
200            // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220)
201            value = dirGps.getDouble(gpsTag);
202        }
203        return value;
204    }
205
206    /**
207     * Returns a Transform that fixes the image orientation.
208     *
209     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
210     * as 1.
211     * @param orientation the exif-orientation of the image
212     * @param width the original width of the image
213     * @param height the original height of the image
214     * @return a transform that rotates the image, so it is upright
215     */
216    public static AffineTransform getRestoreOrientationTransform(final int orientation, final int width, final int height) {
217        final int q;
218        final double ax, ay;
219        switch (orientation) {
220        case 8:
221            q = -1;
222            ax = width / 2d;
223            ay = width / 2d;
224            break;
225        case 3:
226            q = 2;
227            ax = width / 2d;
228            ay = height / 2d;
229            break;
230        case 6:
231            q = 1;
232            ax = height / 2d;
233            ay = height / 2d;
234            break;
235        default:
236            q = 0;
237            ax = 0;
238            ay = 0;
239        }
240        return AffineTransform.getQuadrantRotateInstance(q, ax, ay);
241    }
242
243    /**
244     * Check, if the given orientation switches width and height of the image.
245     * E.g. 90 degree rotation
246     *
247     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
248     * as 1.
249     * @param orientation the exif-orientation of the image
250     * @return true, if it switches width and height
251     */
252    public static boolean orientationSwitchesDimensions(int orientation) {
253        return orientation == 6 || orientation == 8;
254    }
255
256    /**
257     * Check, if the given orientation requires any correction to the image.
258     *
259     * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
260     * as 1.
261     * @param orientation the exif-orientation of the image
262     * @return true, unless the orientation value is 1 or unsupported.
263     */
264    public static boolean orientationNeedsCorrection(int orientation) {
265        return orientation == 3 || orientation == 6 || orientation == 8;
266    }
267}
Note: See TracBrowser for help on using the repository browser.