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.concurrent.TimeUnit; 010 011import org.openstreetmap.josm.data.SystemOfMeasurement; 012import org.openstreetmap.josm.data.coor.LatLon; 013import org.openstreetmap.josm.tools.date.DateUtils; 014 015import com.drew.imaging.jpeg.JpegMetadataReader; 016import com.drew.imaging.jpeg.JpegProcessingException; 017import com.drew.lang.Rational; 018import com.drew.metadata.Directory; 019import com.drew.metadata.Metadata; 020import com.drew.metadata.MetadataException; 021import com.drew.metadata.Tag; 022import com.drew.metadata.exif.ExifDirectoryBase; 023import com.drew.metadata.exif.ExifIFD0Directory; 024import com.drew.metadata.exif.ExifSubIFDDirectory; 025import com.drew.metadata.exif.GpsDirectory; 026 027/** 028 * Read out EXIF information from a JPEG file 029 * @author Imi 030 * @since 99 031 */ 032public final class ExifReader { 033 034 private ExifReader() { 035 // Hide default constructor for utils classes 036 } 037 038 /** 039 * Returns the date/time from the given JPEG file. 040 * @param filename The JPEG file to read 041 * @return The date/time read in the EXIF section, or {@code null} if not found 042 */ 043 public static Date readTime(File filename) { 044 try { 045 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 046 return readTime(metadata); 047 } catch (JpegProcessingException | IOException e) { 048 Logging.error(e); 049 } 050 return null; 051 } 052 053 /** 054 * Returns the date/time from the given JPEG file. 055 * @param metadata The EXIF metadata 056 * @return The date/time read in the EXIF section, or {@code null} if not found 057 * @since 11745 058 */ 059 public static Date readTime(Metadata metadata) { 060 try { 061 String dateTimeOrig = null; 062 String dateTime = null; 063 String dateTimeDig = null; 064 String subSecOrig = null; 065 String subSec = null; 066 String subSecDig = null; 067 // The date fields are preferred in this order: DATETIME_ORIGINAL 068 // (0x9003), DATETIME (0x0132), DATETIME_DIGITIZED (0x9004). Some 069 // cameras store the fields in the wrong directory, so all 070 // directories are searched. Assume that the order of the fields 071 // in the directories is random. 072 for (Directory dirIt : metadata.getDirectories()) { 073 if (!(dirIt instanceof ExifDirectoryBase)) { 074 continue; 075 } 076 for (Tag tag : dirIt.getTags()) { 077 if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */ && 078 !tag.getDescription().matches("\\[[0-9]+ .+\\]")) { 079 dateTimeOrig = tag.getDescription(); 080 } else if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */) { 081 dateTime = tag.getDescription(); 082 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) { 083 dateTimeDig = tag.getDescription(); 084 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_ORIGINAL /* 0x9291 */) { 085 subSecOrig = tag.getDescription(); 086 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME /* 0x9290 */) { 087 subSec = tag.getDescription(); 088 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_DIGITIZED /* 0x9292 */) { 089 subSecDig = tag.getDescription(); 090 } 091 } 092 } 093 String dateStr = null; 094 String subSeconds = null; 095 if (dateTimeOrig != null) { 096 // prefer TAG_DATETIME_ORIGINAL 097 dateStr = dateTimeOrig; 098 subSeconds = subSecOrig; 099 } 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}