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