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
RevLine 
[6380]1// License: GPL. For details, see LICENSE file.
[626]2package org.openstreetmap.josm.tools;
3
[8132]4import java.awt.geom.AffineTransform;
[626]5import java.io.File;
[6127]6import java.io.IOException;
[14159]7import java.time.DateTimeException;
[17715]8import java.time.Instant;
[626]9import java.util.Date;
[15219]10import java.util.List;
[11288]11import java.util.concurrent.TimeUnit;
[626]12
[11745]13import org.openstreetmap.josm.data.SystemOfMeasurement;
[6209]14import org.openstreetmap.josm.data.coor.LatLon;
[9383]15import org.openstreetmap.josm.tools.date.DateUtils;
[6209]16
[626]17import com.drew.imaging.jpeg.JpegMetadataReader;
[4241]18import com.drew.imaging.jpeg.JpegProcessingException;
[6209]19import com.drew.lang.Rational;
[626]20import com.drew.metadata.Directory;
21import com.drew.metadata.Metadata;
[4241]22import com.drew.metadata.MetadataException;
[626]23import com.drew.metadata.Tag;
[9672]24import com.drew.metadata.exif.ExifDirectoryBase;
[6127]25import com.drew.metadata.exif.ExifIFD0Directory;
26import com.drew.metadata.exif.ExifSubIFDDirectory;
[6209]27import com.drew.metadata.exif.GpsDirectory;
[15219]28import com.drew.metadata.iptc.IptcDirectory;
[626]29
30/**
[6209]31 * Read out EXIF information from a JPEG file
[626]32 * @author Imi
[6209]33 * @since 99
[626]34 */
[6362]35public final class ExifReader {
[626]36
[6360]37 private ExifReader() {
38 // Hide default constructor for utils classes
39 }
[6830]40
[6209]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
[17715]45 * @deprecated Use {@link #readInstant(File)}
[6209]46 */
[17715]47 @Deprecated
[9383]48 public static Date readTime(File filename) {
[17715]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) {
[1169]59 try {
[11745]60 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
[17715]61 return readInstant(metadata);
[11745]62 } catch (JpegProcessingException | IOException e) {
[12620]63 Logging.error(e);
[11745]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
[17715]73 * @deprecated Use {@link #readInstant(Metadata)}
[11745]74 */
[17715]75 @Deprecated
[11745]76 public static Date readTime(Metadata metadata) {
[17715]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) {
[11745]87 try {
88 String dateTimeOrig = null;
[11514]89 String dateTime = null;
[11745]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.
[6127]99 for (Directory dirIt : metadata.getDirectories()) {
[9648]100 if (!(dirIt instanceof ExifDirectoryBase)) {
101 continue;
102 }
[6127]103 for (Tag tag : dirIt.getTags()) {
[8244]104 if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */ &&
105 !tag.getDescription().matches("\\[[0-9]+ .+\\]")) {
[11745]106 dateTimeOrig = tag.getDescription();
107 } else if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */) {
[11514]108 dateTime = tag.getDescription();
[11745]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();
[4772]117 }
[1169]118 }
119 }
[11745]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
[11514]128 dateStr = dateTime;
[11745]129 subSeconds = subSec;
130 } else if (dateTimeDig != null) {
131 dateStr = dateTimeDig;
132 subSeconds = subSecDig;
[11514]133 }
[5610]134 if (dateStr != null) {
135 dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228
[17715]136 Instant date = DateUtils.parseInstant(dateStr);
[9499]137 if (subSeconds != null) {
138 try {
[17715]139 date = date.plusMillis((long) (TimeUnit.SECONDS.toMillis(1) * Double.parseDouble("0." + subSeconds)));
[9499]140 } catch (NumberFormatException e) {
[12620]141 Logging.warn("Failed parsing sub seconds from [{0}]", subSeconds);
142 Logging.warn(e);
[9499]143 }
144 }
145 return date;
[5610]146 }
[14159]147 } catch (UncheckedParseException | DateTimeException e) {
[12620]148 Logging.error(e);
[626]149 }
[4772]150 return null;
[1169]151 }
[4241]152
[6209]153 /**
154 * Returns the image orientation of the given JPEG file.
155 * @param filename The JPEG file to read
[6830]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>
[6209]165 * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a>
[8509]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>
[6209]168 */
169 public static Integer readOrientation(File filename) {
[4241]170 try {
171 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
[8243]172 final Directory dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
[9697]173 return dir == null ? null : dir.getInteger(ExifIFD0Directory.TAG_ORIENTATION);
174 } catch (JpegProcessingException | IOException e) {
[12620]175 Logging.error(e);
[4241]176 }
[6209]177 return null;
[4241]178 }
179
[6209]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);
[8243]189 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
[6209]190 return readLatLon(dirGps);
[9697]191 } catch (JpegProcessingException | IOException | MetadataException e) {
[12620]192 Logging.error(e);
[6209]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
[8470]201 * @throws MetadataException if invalid metadata is given
[6209]202 * @since 6209
203 */
204 public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException {
[17548]205 if (dirGps != null && dirGps.getTagCount() > 1) {
[8132]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');
[6209]208 return new LatLon(lat, lon);
209 }
210 return null;
211 }
[6830]212
[6209]213 /**
214 * Returns the direction of the given JPEG file.
215 * @param filename The JPEG file to read
[8509]216 * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99),
[11745]217 * or {@code null} if not found
[6209]218 * @since 6209
219 */
220 public static Double readDirection(File filename) {
221 try {
222 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
[8243]223 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
[6209]224 return readDirection(dirGps);
[9697]225 } catch (JpegProcessingException | IOException e) {
[12620]226 Logging.error(e);
[6209]227 }
228 return null;
229 }
[6830]230
[6209]231 /**
232 * Returns the direction of the given EXIF GPS directory.
233 * @param dirGps The EXIF GPS directory
[11745]234 * @return The direction of the image when it was captured (in degrees between 0.0 and 359.99),
[8509]235 * or {@code null} if missing or if {@code dirGps} is null
[6209]236 * @since 6209
237 */
238 public static Double readDirection(GpsDirectory dirGps) {
239 if (dirGps != null) {
[8132]240 Rational direction = dirGps.getRational(GpsDirectory.TAG_IMG_DIRECTION);
[6209]241 if (direction != null) {
242 return direction.doubleValue();
243 }
244 }
245 return null;
246 }
247
[10378]248 private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException {
[6209]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();
[6830]255
[6209]256 if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
[7864]257 throw new IllegalArgumentException("deg, min and sec are NaN");
[6830]258
[9970]259 value = Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600));
[6830]260
[12985]261 String s = dirGps.getString(gpsTagRef);
262 if (s != null && s.charAt(0) == cRef) {
[6209]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 }
[7956]271
272 /**
[11745]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) {
[12620]285 Logging.error(e);
[11745]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) {
[12620]329 Logging.error(e);
[11745]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 /**
[15219]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 /**
[7956]396 * Returns a Transform that fixes the image orientation.
397 *
[10378]398 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated as 1.
[7956]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;
[8364]410 ax = width / 2d;
411 ay = width / 2d;
[7956]412 break;
413 case 3:
414 q = 2;
[8364]415 ax = width / 2d;
416 ay = height / 2d;
[7956]417 break;
418 case 6:
419 q = 1;
[8364]420 ax = height / 2d;
421 ay = height / 2d;
[7956]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 }
[626]455}
Note: See TracBrowser for help on using the repository browser.