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

Last change on this file since 9648 was 9648, checked in by simon04, 8 years ago

fix #11685 - Obtain EXIF fields only from EXIF directories

Fixes unit test ExifReaderTest.testReadTime

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