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