001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import java.awt.geom.AffineTransform; 005import java.io.File; 006import java.io.IOException; 007import java.time.DateTimeException; 008import java.time.Instant; 009import java.util.List; 010import java.util.concurrent.TimeUnit; 011 012import org.openstreetmap.josm.data.SystemOfMeasurement; 013import org.openstreetmap.josm.data.coor.LatLon; 014import org.openstreetmap.josm.tools.date.DateUtils; 015 016import com.drew.imaging.jpeg.JpegMetadataReader; 017import com.drew.imaging.jpeg.JpegProcessingException; 018import com.drew.lang.Rational; 019import com.drew.metadata.Directory; 020import com.drew.metadata.Metadata; 021import com.drew.metadata.MetadataException; 022import com.drew.metadata.Tag; 023import com.drew.metadata.exif.ExifDirectoryBase; 024import com.drew.metadata.exif.ExifIFD0Directory; 025import com.drew.metadata.exif.ExifSubIFDDirectory; 026import com.drew.metadata.exif.GpsDirectory; 027import com.drew.metadata.iptc.IptcDirectory; 028 029/** 030 * Read out EXIF information from a JPEG file 031 * @author Imi 032 * @since 99 033 */ 034public final class ExifReader { 035 036 private ExifReader() { 037 // Hide default constructor for utils classes 038 } 039 040 /** 041 * Returns the date/time from the given JPEG file. 042 * @param filename The JPEG file to read 043 * @return The date/time read in the EXIF section, or {@code null} if not found 044 */ 045 public static Instant readInstant(File filename) { 046 try { 047 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 048 return readInstant(metadata); 049 } catch (JpegProcessingException | IOException e) { 050 Logging.error(e); 051 } 052 return null; 053 } 054 055 /** 056 * Returns the date/time from the given JPEG file. 057 * @param metadata The EXIF metadata 058 * @return The date/time read in the EXIF section, or {@code null} if not found 059 */ 060 public static Instant readInstant(Metadata metadata) { 061 try { 062 String dateTimeOrig = null; 063 String dateTime = null; 064 String dateTimeDig = null; 065 String subSecOrig = null; 066 String subSec = null; 067 String subSecDig = null; 068 // The date fields are preferred in this order: DATETIME_ORIGINAL 069 // (0x9003), DATETIME (0x0132), DATETIME_DIGITIZED (0x9004). Some 070 // cameras store the fields in the wrong directory, so all 071 // directories are searched. Assume that the order of the fields 072 // in the directories is random. 073 for (Directory dirIt : metadata.getDirectories()) { 074 if (!(dirIt instanceof ExifDirectoryBase)) { 075 continue; 076 } 077 for (Tag tag : dirIt.getTags()) { 078 if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */ && 079 !tag.getDescription().matches("\\[[0-9]+ .+\\]")) { 080 dateTimeOrig = tag.getDescription(); 081 } else if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */) { 082 dateTime = tag.getDescription(); 083 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) { 084 dateTimeDig = tag.getDescription(); 085 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_ORIGINAL /* 0x9291 */) { 086 subSecOrig = tag.getDescription(); 087 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME /* 0x9290 */) { 088 subSec = tag.getDescription(); 089 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_DIGITIZED /* 0x9292 */) { 090 subSecDig = tag.getDescription(); 091 } 092 } 093 } 094 String dateStr = null; 095 String subSeconds = null; 096 if (dateTimeOrig != null) { 097 // prefer TAG_DATETIME_ORIGINAL 098 dateStr = dateTimeOrig; 099 subSeconds = subSecOrig; 100 } else if (dateTime != null) { 101 // TAG_DATETIME is second choice, see #14209 102 dateStr = dateTime; 103 subSeconds = subSec; 104 } else if (dateTimeDig != null) { 105 dateStr = dateTimeDig; 106 subSeconds = subSecDig; 107 } 108 if (dateStr != null) { 109 dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228 110 Instant date = DateUtils.parseInstant(dateStr); 111 if (subSeconds != null) { 112 try { 113 date = date.plusMillis((long) (TimeUnit.SECONDS.toMillis(1) * Double.parseDouble("0." + subSeconds))); 114 } catch (NumberFormatException e) { 115 Logging.warn("Failed parsing sub seconds from [{0}]", subSeconds); 116 Logging.warn(e); 117 } 118 } 119 return date; 120 } 121 } catch (UncheckedParseException | DateTimeException e) { 122 Logging.error(e); 123 } 124 return null; 125 } 126 127 /** 128 * Returns the image orientation of the given JPEG file. 129 * @param filename The JPEG file to read 130 * @return The image orientation as an {@code int}. Default value is 1. Possible values are listed in EXIF spec as follows:<br><ol> 131 * <li>The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.</li> 132 * <li>The 0th row is at the visual top 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 right-hand side.</li> 134 * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.</li> 135 * <li>The 0th row is the visual left-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 top.</li> 137 * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.</li> 138 * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.</li></ol> 139 * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a> 140 * @see <a href="http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto"> 141 * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto</a> 142 */ 143 public static Integer readOrientation(File filename) { 144 try { 145 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 146 final Directory dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); 147 return dir == null ? null : dir.getInteger(ExifIFD0Directory.TAG_ORIENTATION); 148 } catch (JpegProcessingException | IOException e) { 149 Logging.error(e); 150 } 151 return null; 152 } 153 154 /** 155 * Returns the geolocation of the given JPEG file. 156 * @param filename The JPEG file to read 157 * @return The lat/lon read in the EXIF section, or {@code null} if not found 158 * @since 6209 159 */ 160 public static LatLon readLatLon(File filename) { 161 try { 162 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 163 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class); 164 return readLatLon(dirGps); 165 } catch (JpegProcessingException | IOException | MetadataException e) { 166 Logging.error(e); 167 } 168 return null; 169 } 170 171 /** 172 * Returns the geolocation of the given EXIF GPS directory. 173 * @param dirGps The EXIF GPS directory 174 * @return The lat/lon read in the EXIF section, or {@code null} if {@code dirGps} is null 175 * @throws MetadataException if invalid metadata is given 176 * @since 6209 177 */ 178 public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException { 179 if (dirGps != null && dirGps.containsTag(GpsDirectory.TAG_LATITUDE) && dirGps.containsTag(GpsDirectory.TAG_LONGITUDE)) { 180 double lat = readAxis(dirGps, GpsDirectory.TAG_LATITUDE, GpsDirectory.TAG_LATITUDE_REF, 'S'); 181 double lon = readAxis(dirGps, GpsDirectory.TAG_LONGITUDE, GpsDirectory.TAG_LONGITUDE_REF, 'W'); 182 return new LatLon(lat, lon); 183 } 184 return null; 185 } 186 187 /** 188 * Returns the direction of the given JPEG file. 189 * @param filename The JPEG file to read 190 * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99), 191 * or {@code null} if not found 192 * @since 6209 193 */ 194 public static Double readDirection(File filename) { 195 try { 196 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 197 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class); 198 return readDirection(dirGps); 199 } catch (JpegProcessingException | IOException e) { 200 Logging.error(e); 201 } 202 return null; 203 } 204 205 /** 206 * Returns the direction of the given EXIF GPS directory. 207 * @param dirGps The EXIF GPS directory 208 * @return The direction of the image when it was captured (in degrees between 0.0 and 359.99), 209 * or {@code null} if missing or if {@code dirGps} is null 210 * @since 6209 211 */ 212 public static Double readDirection(GpsDirectory dirGps) { 213 if (dirGps != null) { 214 Rational direction = dirGps.getRational(GpsDirectory.TAG_IMG_DIRECTION); 215 if (direction != null) { 216 return direction.doubleValue(); 217 } 218 } 219 return null; 220 } 221 222 private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException { 223 double value; 224 Rational[] components = dirGps.getRationalArray(gpsTag); 225 if (components != null) { 226 double deg = components[0].doubleValue(); 227 double min = components[1].doubleValue(); 228 double sec = components[2].doubleValue(); 229 230 if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec)) 231 throw new IllegalArgumentException("deg, min and sec are NaN"); 232 233 value = Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600)); 234 235 String s = dirGps.getString(gpsTagRef); 236 if (s != null && s.charAt(0) == cRef) { 237 value = -value; 238 } 239 } else { 240 // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220) 241 value = dirGps.getDouble(gpsTag); 242 } 243 return value; 244 } 245 246 /** 247 * Returns the speed of the given JPEG file. 248 * @param filename The JPEG file to read 249 * @return The speed of the camera when the image was captured (in km/h), 250 * or {@code null} if not found 251 * @since 11745 252 */ 253 public static Double readSpeed(File filename) { 254 try { 255 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 256 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class); 257 return readSpeed(dirGps); 258 } catch (JpegProcessingException | IOException e) { 259 Logging.error(e); 260 } 261 return null; 262 } 263 264 /** 265 * Returns the speed of the given EXIF GPS directory. 266 * @param dirGps The EXIF GPS directory 267 * @return The speed of the camera when the image was captured (in km/h), 268 * or {@code null} if missing or if {@code dirGps} is null 269 * @since 11745 270 */ 271 public static Double readSpeed(GpsDirectory dirGps) { 272 if (dirGps != null) { 273 Double speed = dirGps.getDoubleObject(GpsDirectory.TAG_SPEED); 274 if (speed != null) { 275 final String speedRef = dirGps.getString(GpsDirectory.TAG_SPEED_REF); 276 if ("M".equalsIgnoreCase(speedRef)) { 277 // miles per hour 278 speed *= SystemOfMeasurement.IMPERIAL.bValue / 1000; 279 } else if ("N".equalsIgnoreCase(speedRef)) { 280 // knots == nautical miles per hour 281 speed *= SystemOfMeasurement.NAUTICAL_MILE.bValue / 1000; 282 } 283 // default is K (km/h) 284 return speed; 285 } 286 } 287 return null; 288 } 289 290 /** 291 * Returns the elevation of the given JPEG file. 292 * @param filename The JPEG file to read 293 * @return The elevation of the camera when the image was captured (in m), 294 * or {@code null} if not found 295 * @since 11745 296 */ 297 public static Double readElevation(File filename) { 298 try { 299 return readElevation(JpegMetadataReader.readMetadata(filename).getFirstDirectoryOfType(GpsDirectory.class)); 300 } catch (JpegProcessingException | IOException e) { 301 Logging.error(e); 302 return null; 303 } 304 } 305 306 /** 307 * Returns the elevation of the given EXIF GPS directory. 308 * @param dirGps The EXIF GPS directory 309 * @return The elevation of the camera when the image was captured (in m), 310 * or {@code null} if missing or if {@code dirGps} is null 311 * @since 11745 312 */ 313 public static Double readElevation(GpsDirectory dirGps) { 314 if (dirGps != null) { 315 Double ele = dirGps.getDoubleObject(GpsDirectory.TAG_ALTITUDE); 316 if (ele != null) { 317 final Integer d = dirGps.getInteger(GpsDirectory.TAG_ALTITUDE_REF); 318 if (d != null && d.intValue() == 1) { 319 ele *= -1; 320 } 321 return ele; 322 } 323 } 324 return null; 325 } 326 327 /** 328 * Returns the caption of the given IPTC directory. 329 * @param dirIptc The IPTC directory 330 * @return The caption entered, or {@code null} if missing or if {@code dirIptc} is null 331 * @since 15219 332 */ 333 public static String readCaption(IptcDirectory dirIptc) { 334 return dirIptc == null ? null : dirIptc.getDescription(IptcDirectory.TAG_CAPTION); 335 } 336 337 /** 338 * Returns the headline of the given IPTC directory. 339 * @param dirIptc The IPTC directory 340 * @return The headline entered, or {@code null} if missing or if {@code dirIptc} is null 341 * @since 15219 342 */ 343 public static String readHeadline(IptcDirectory dirIptc) { 344 return dirIptc == null ? null : dirIptc.getDescription(IptcDirectory.TAG_HEADLINE); 345 } 346 347 /** 348 * Returns the keywords of the given IPTC directory. 349 * @param dirIptc The IPTC directory 350 * @return The keywords entered, or {@code null} if missing or if {@code dirIptc} is null 351 * @since 15219 352 */ 353 public static List<String> readKeywords(IptcDirectory dirIptc) { 354 return dirIptc == null ? null : dirIptc.getKeywords(); 355 } 356 357 /** 358 * Returns the object name of the given IPTC directory. 359 * @param dirIptc The IPTC directory 360 * @return The object name entered, or {@code null} if missing or if {@code dirIptc} is null 361 * @since 15219 362 */ 363 public static String readObjectName(IptcDirectory dirIptc) { 364 return dirIptc == null ? null : dirIptc.getDescription(IptcDirectory.TAG_OBJECT_NAME); 365 } 366 367 /** 368 * Returns a Transform that fixes the image orientation. 369 * 370 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated as 1. 371 * @param orientation the exif-orientation of the image 372 * @param width the original width of the image 373 * @param height the original height of the image 374 * @return a transform that rotates the image, so it is upright 375 */ 376 public static AffineTransform getRestoreOrientationTransform(final int orientation, final int width, final int height) { 377 final int q; 378 final double ax, ay; 379 switch (orientation) { 380 case 8: 381 q = -1; 382 ax = width / 2d; 383 ay = width / 2d; 384 break; 385 case 3: 386 q = 2; 387 ax = width / 2d; 388 ay = height / 2d; 389 break; 390 case 6: 391 q = 1; 392 ax = height / 2d; 393 ay = height / 2d; 394 break; 395 default: 396 q = 0; 397 ax = 0; 398 ay = 0; 399 } 400 return AffineTransform.getQuadrantRotateInstance(q, ax, ay); 401 } 402 403 /** 404 * Check, if the given orientation switches width and height of the image. 405 * E.g. 90 degree rotation 406 * 407 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated 408 * as 1. 409 * @param orientation the exif-orientation of the image 410 * @return true, if it switches width and height 411 */ 412 public static boolean orientationSwitchesDimensions(int orientation) { 413 return orientation == 6 || orientation == 8; 414 } 415 416 /** 417 * Check, if the given orientation requires any correction to the image. 418 * 419 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated 420 * as 1. 421 * @param orientation the exif-orientation of the image 422 * @return true, unless the orientation value is 1 or unsupported. 423 */ 424 public static boolean orientationNeedsCorrection(int orientation) { 425 return orientation == 3 || orientation == 6 || orientation == 8; 426 } 427}