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

Last change on this file since 14180 was 14159, checked in by Don-vip, 6 years ago

fix #16633 - add robustness against invalid time entries in GPX files

  • Property svn:eol-style set to native
File size: 15.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.time.DateTimeException;
8import java.util.Date;
9import java.util.concurrent.TimeUnit;
10
11import org.openstreetmap.josm.data.SystemOfMeasurement;
12import org.openstreetmap.josm.data.coor.LatLon;
13import org.openstreetmap.josm.tools.date.DateUtils;
14
15import com.drew.imaging.jpeg.JpegMetadataReader;
16import com.drew.imaging.jpeg.JpegProcessingException;
17import com.drew.lang.Rational;
18import com.drew.metadata.Directory;
19import com.drew.metadata.Metadata;
20import com.drew.metadata.MetadataException;
21import com.drew.metadata.Tag;
22import com.drew.metadata.exif.ExifDirectoryBase;
23import com.drew.metadata.exif.ExifIFD0Directory;
24import com.drew.metadata.exif.ExifSubIFDDirectory;
25import com.drew.metadata.exif.GpsDirectory;
26
27/**
28 * Read out EXIF information from a JPEG file
29 * @author Imi
30 * @since 99
31 */
32public final class ExifReader {
33
34 private ExifReader() {
35 // Hide default constructor for utils classes
36 }
37
38 /**
39 * Returns the date/time from the given JPEG file.
40 * @param filename The JPEG file to read
41 * @return The date/time read in the EXIF section, or {@code null} if not found
42 */
43 public static Date readTime(File filename) {
44 try {
45 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
46 return readTime(metadata);
47 } catch (JpegProcessingException | IOException e) {
48 Logging.error(e);
49 }
50 return null;
51 }
52
53 /**
54 * Returns the date/time from the given JPEG file.
55 * @param metadata The EXIF metadata
56 * @return The date/time read in the EXIF section, or {@code null} if not found
57 * @since 11745
58 */
59 public static Date readTime(Metadata metadata) {
60 try {
61 String dateTimeOrig = null;
62 String dateTime = null;
63 String dateTimeDig = null;
64 String subSecOrig = null;
65 String subSec = null;
66 String subSecDig = null;
67 // The date fields are preferred in this order: DATETIME_ORIGINAL
68 // (0x9003), DATETIME (0x0132), DATETIME_DIGITIZED (0x9004). Some
69 // cameras store the fields in the wrong directory, so all
70 // directories are searched. Assume that the order of the fields
71 // in the directories is random.
72 for (Directory dirIt : metadata.getDirectories()) {
73 if (!(dirIt instanceof ExifDirectoryBase)) {
74 continue;
75 }
76 for (Tag tag : dirIt.getTags()) {
77 if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */ &&
78 !tag.getDescription().matches("\\[[0-9]+ .+\\]")) {
79 dateTimeOrig = tag.getDescription();
80 } else if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */) {
81 dateTime = tag.getDescription();
82 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) {
83 dateTimeDig = tag.getDescription();
84 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_ORIGINAL /* 0x9291 */) {
85 subSecOrig = tag.getDescription();
86 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME /* 0x9290 */) {
87 subSec = tag.getDescription();
88 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_DIGITIZED /* 0x9292 */) {
89 subSecDig = tag.getDescription();
90 }
91 }
92 }
93 String dateStr = null;
94 String subSeconds = null;
95 if (dateTimeOrig != null) {
96 // prefer TAG_DATETIME_ORIGINAL
97 dateStr = dateTimeOrig;
98 subSeconds = subSecOrig;
99 } else if (dateTime != null) {
100 // TAG_DATETIME is second choice, see #14209
101 dateStr = dateTime;
102 subSeconds = subSec;
103 } else if (dateTimeDig != null) {
104 dateStr = dateTimeDig;
105 subSeconds = subSecDig;
106 }
107 if (dateStr != null) {
108 dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228
109 final Date date = DateUtils.fromString(dateStr);
110 if (subSeconds != null) {
111 try {
112 date.setTime(date.getTime() + (long) (TimeUnit.SECONDS.toMillis(1) * Double.parseDouble("0." + subSeconds)));
113 } catch (NumberFormatException e) {
114 Logging.warn("Failed parsing sub seconds from [{0}]", subSeconds);
115 Logging.warn(e);
116 }
117 }
118 return date;
119 }
120 } catch (UncheckedParseException | DateTimeException e) {
121 Logging.error(e);
122 }
123 return null;
124 }
125
126 /**
127 * Returns the image orientation of the given JPEG file.
128 * @param filename The JPEG file to read
129 * @return The image orientation as an {@code int}. Default value is 1. Possible values are listed in EXIF spec as follows:<br><ol>
130 * <li>The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.</li>
131 * <li>The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.</li>
132 * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.</li>
133 * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.</li>
134 * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.</li>
135 * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.</li>
136 * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.</li>
137 * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.</li></ol>
138 * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a>
139 * @see <a href="http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto">
140 * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto</a>
141 */
142 public static Integer readOrientation(File filename) {
143 try {
144 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
145 final Directory dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
146 return dir == null ? null : dir.getInteger(ExifIFD0Directory.TAG_ORIENTATION);
147 } catch (JpegProcessingException | IOException e) {
148 Logging.error(e);
149 }
150 return null;
151 }
152
153 /**
154 * Returns the geolocation of the given JPEG file.
155 * @param filename The JPEG file to read
156 * @return The lat/lon read in the EXIF section, or {@code null} if not found
157 * @since 6209
158 */
159 public static LatLon readLatLon(File filename) {
160 try {
161 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
162 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
163 return readLatLon(dirGps);
164 } catch (JpegProcessingException | IOException | MetadataException e) {
165 Logging.error(e);
166 }
167 return null;
168 }
169
170 /**
171 * Returns the geolocation of the given EXIF GPS directory.
172 * @param dirGps The EXIF GPS directory
173 * @return The lat/lon read in the EXIF section, or {@code null} if {@code dirGps} is null
174 * @throws MetadataException if invalid metadata is given
175 * @since 6209
176 */
177 public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException {
178 if (dirGps != null) {
179 double lat = readAxis(dirGps, GpsDirectory.TAG_LATITUDE, GpsDirectory.TAG_LATITUDE_REF, 'S');
180 double lon = readAxis(dirGps, GpsDirectory.TAG_LONGITUDE, GpsDirectory.TAG_LONGITUDE_REF, 'W');
181 return new LatLon(lat, lon);
182 }
183 return null;
184 }
185
186 /**
187 * Returns the direction of the given JPEG file.
188 * @param filename The JPEG file to read
189 * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99),
190 * or {@code null} if not found
191 * @since 6209
192 */
193 public static Double readDirection(File filename) {
194 try {
195 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
196 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
197 return readDirection(dirGps);
198 } catch (JpegProcessingException | IOException e) {
199 Logging.error(e);
200 }
201 return null;
202 }
203
204 /**
205 * Returns the direction of the given EXIF GPS directory.
206 * @param dirGps The EXIF GPS directory
207 * @return The direction of the image when it was captured (in degrees between 0.0 and 359.99),
208 * or {@code null} if missing or if {@code dirGps} is null
209 * @since 6209
210 */
211 public static Double readDirection(GpsDirectory dirGps) {
212 if (dirGps != null) {
213 Rational direction = dirGps.getRational(GpsDirectory.TAG_IMG_DIRECTION);
214 if (direction != null) {
215 return direction.doubleValue();
216 }
217 }
218 return null;
219 }
220
221 private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException {
222 double value;
223 Rational[] components = dirGps.getRationalArray(gpsTag);
224 if (components != null) {
225 double deg = components[0].doubleValue();
226 double min = components[1].doubleValue();
227 double sec = components[2].doubleValue();
228
229 if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
230 throw new IllegalArgumentException("deg, min and sec are NaN");
231
232 value = Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600));
233
234 String s = dirGps.getString(gpsTagRef);
235 if (s != null && s.charAt(0) == cRef) {
236 value = -value;
237 }
238 } else {
239 // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220)
240 value = dirGps.getDouble(gpsTag);
241 }
242 return value;
243 }
244
245 /**
246 * Returns the speed of the given JPEG file.
247 * @param filename The JPEG file to read
248 * @return The speed of the camera when the image was captured (in km/h),
249 * or {@code null} if not found
250 * @since 11745
251 */
252 public static Double readSpeed(File filename) {
253 try {
254 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
255 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
256 return readSpeed(dirGps);
257 } catch (JpegProcessingException | IOException e) {
258 Logging.error(e);
259 }
260 return null;
261 }
262
263 /**
264 * Returns the speed of the given EXIF GPS directory.
265 * @param dirGps The EXIF GPS directory
266 * @return The speed of the camera when the image was captured (in km/h),
267 * or {@code null} if missing or if {@code dirGps} is null
268 * @since 11745
269 */
270 public static Double readSpeed(GpsDirectory dirGps) {
271 if (dirGps != null) {
272 Double speed = dirGps.getDoubleObject(GpsDirectory.TAG_SPEED);
273 if (speed != null) {
274 final String speedRef = dirGps.getString(GpsDirectory.TAG_SPEED_REF);
275 if ("M".equalsIgnoreCase(speedRef)) {
276 // miles per hour
277 speed *= SystemOfMeasurement.IMPERIAL.bValue / 1000;
278 } else if ("N".equalsIgnoreCase(speedRef)) {
279 // knots == nautical miles per hour
280 speed *= SystemOfMeasurement.NAUTICAL_MILE.bValue / 1000;
281 }
282 // default is K (km/h)
283 return speed;
284 }
285 }
286 return null;
287 }
288
289 /**
290 * Returns the elevation of the given JPEG file.
291 * @param filename The JPEG file to read
292 * @return The elevation of the camera when the image was captured (in m),
293 * or {@code null} if not found
294 * @since 11745
295 */
296 public static Double readElevation(File filename) {
297 try {
298 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
299 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
300 return readElevation(dirGps);
301 } catch (JpegProcessingException | IOException e) {
302 Logging.error(e);
303 }
304 return null;
305 }
306
307 /**
308 * Returns the elevation of the given EXIF GPS directory.
309 * @param dirGps The EXIF GPS directory
310 * @return The elevation of the camera when the image was captured (in m),
311 * or {@code null} if missing or if {@code dirGps} is null
312 * @since 11745
313 */
314 public static Double readElevation(GpsDirectory dirGps) {
315 if (dirGps != null) {
316 Double ele = dirGps.getDoubleObject(GpsDirectory.TAG_ALTITUDE);
317 if (ele != null) {
318 final Integer d = dirGps.getInteger(GpsDirectory.TAG_ALTITUDE_REF);
319 if (d != null && d.intValue() == 1) {
320 ele *= -1;
321 }
322 return ele;
323 }
324 }
325 return null;
326 }
327
328 /**
329 * Returns a Transform that fixes the image orientation.
330 *
331 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated as 1.
332 * @param orientation the exif-orientation of the image
333 * @param width the original width of the image
334 * @param height the original height of the image
335 * @return a transform that rotates the image, so it is upright
336 */
337 public static AffineTransform getRestoreOrientationTransform(final int orientation, final int width, final int height) {
338 final int q;
339 final double ax, ay;
340 switch (orientation) {
341 case 8:
342 q = -1;
343 ax = width / 2d;
344 ay = width / 2d;
345 break;
346 case 3:
347 q = 2;
348 ax = width / 2d;
349 ay = height / 2d;
350 break;
351 case 6:
352 q = 1;
353 ax = height / 2d;
354 ay = height / 2d;
355 break;
356 default:
357 q = 0;
358 ax = 0;
359 ay = 0;
360 }
361 return AffineTransform.getQuadrantRotateInstance(q, ax, ay);
362 }
363
364 /**
365 * Check, if the given orientation switches width and height of the image.
366 * E.g. 90 degree rotation
367 *
368 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
369 * as 1.
370 * @param orientation the exif-orientation of the image
371 * @return true, if it switches width and height
372 */
373 public static boolean orientationSwitchesDimensions(int orientation) {
374 return orientation == 6 || orientation == 8;
375 }
376
377 /**
378 * Check, if the given orientation requires any correction to the image.
379 *
380 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
381 * as 1.
382 * @param orientation the exif-orientation of the image
383 * @return true, unless the orientation value is 1 or unsupported.
384 */
385 public static boolean orientationNeedsCorrection(int orientation) {
386 return orientation == 3 || orientation == 6 || orientation == 8;
387 }
388}
Note: See TracBrowser for help on using the repository browser.