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

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

see #14176 - Migrate GPX to Instant

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