[6380] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[626] | 2 | package org.openstreetmap.josm.tools;
|
---|
| 3 |
|
---|
[8132] | 4 | import java.awt.geom.AffineTransform;
|
---|
[626] | 5 | import java.io.File;
|
---|
[6127] | 6 | import java.io.IOException;
|
---|
[626] | 7 | import java.util.Date;
|
---|
[11288] | 8 | import java.util.concurrent.TimeUnit;
|
---|
[626] | 9 |
|
---|
[6643] | 10 | import org.openstreetmap.josm.Main;
|
---|
[11745] | 11 | import org.openstreetmap.josm.data.SystemOfMeasurement;
|
---|
[6209] | 12 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
[9383] | 13 | import org.openstreetmap.josm.tools.date.DateUtils;
|
---|
[6209] | 14 |
|
---|
[626] | 15 | import com.drew.imaging.jpeg.JpegMetadataReader;
|
---|
[4241] | 16 | import com.drew.imaging.jpeg.JpegProcessingException;
|
---|
[6209] | 17 | import com.drew.lang.Rational;
|
---|
[626] | 18 | import com.drew.metadata.Directory;
|
---|
| 19 | import com.drew.metadata.Metadata;
|
---|
[4241] | 20 | import com.drew.metadata.MetadataException;
|
---|
[626] | 21 | import com.drew.metadata.Tag;
|
---|
[9672] | 22 | import com.drew.metadata.exif.ExifDirectoryBase;
|
---|
[6127] | 23 | import com.drew.metadata.exif.ExifIFD0Directory;
|
---|
| 24 | import com.drew.metadata.exif.ExifSubIFDDirectory;
|
---|
[6209] | 25 | import com.drew.metadata.exif.GpsDirectory;
|
---|
[626] | 26 |
|
---|
| 27 | /**
|
---|
[6209] | 28 | * Read out EXIF information from a JPEG file
|
---|
[626] | 29 | * @author Imi
|
---|
[6209] | 30 | * @since 99
|
---|
[626] | 31 | */
|
---|
[6362] | 32 | public final class ExifReader {
|
---|
[626] | 33 |
|
---|
[6360] | 34 | private ExifReader() {
|
---|
| 35 | // Hide default constructor for utils classes
|
---|
| 36 | }
|
---|
[6830] | 37 |
|
---|
[6209] | 38 | /**
|
---|
| 39 | * Returns the date/time from the given JPEG file.
|
---|
| 40 | * @param filename The JPEG file to read
|
---|
| 41 | * @return The date/time read in the EXIF section, or {@code null} if not found
|
---|
| 42 | */
|
---|
[9383] | 43 | public static Date readTime(File filename) {
|
---|
[1169] | 44 | try {
|
---|
[11745] | 45 | final Metadata metadata = JpegMetadataReader.readMetadata(filename);
|
---|
| 46 | return readTime(metadata);
|
---|
| 47 | } catch (JpegProcessingException | IOException e) {
|
---|
| 48 | Main.error(e);
|
---|
| 49 | }
|
---|
| 50 | return null;
|
---|
| 51 | }
|
---|
| 52 |
|
---|
| 53 | /**
|
---|
| 54 | * Returns the date/time from the given JPEG file.
|
---|
| 55 | * @param metadata The EXIF metadata
|
---|
| 56 | * @return The date/time read in the EXIF section, or {@code null} if not found
|
---|
| 57 | * @since 11745
|
---|
| 58 | */
|
---|
| 59 | public static Date readTime(Metadata metadata) {
|
---|
| 60 | try {
|
---|
| 61 | String dateTimeOrig = null;
|
---|
[11514] | 62 | String dateTime = null;
|
---|
[11745] | 63 | String dateTimeDig = null;
|
---|
| 64 | String subSecOrig = null;
|
---|
| 65 | String subSec = null;
|
---|
| 66 | String subSecDig = null;
|
---|
| 67 | // The date fields are preferred in this order: DATETIME_ORIGINAL
|
---|
| 68 | // (0x9003), DATETIME (0x0132), DATETIME_DIGITIZED (0x9004). Some
|
---|
| 69 | // cameras store the fields in the wrong directory, so all
|
---|
| 70 | // directories are searched. Assume that the order of the fields
|
---|
| 71 | // in the directories is random.
|
---|
[6127] | 72 | for (Directory dirIt : metadata.getDirectories()) {
|
---|
[9648] | 73 | if (!(dirIt instanceof ExifDirectoryBase)) {
|
---|
| 74 | continue;
|
---|
| 75 | }
|
---|
[6127] | 76 | for (Tag tag : dirIt.getTags()) {
|
---|
[8244] | 77 | if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */ &&
|
---|
| 78 | !tag.getDescription().matches("\\[[0-9]+ .+\\]")) {
|
---|
[11745] | 79 | dateTimeOrig = tag.getDescription();
|
---|
| 80 | } else if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */) {
|
---|
[11514] | 81 | dateTime = tag.getDescription();
|
---|
[11745] | 82 | } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) {
|
---|
| 83 | dateTimeDig = tag.getDescription();
|
---|
| 84 | } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_ORIGINAL /* 0x9291 */) {
|
---|
| 85 | subSecOrig = tag.getDescription();
|
---|
| 86 | } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME /* 0x9290 */) {
|
---|
| 87 | subSec = tag.getDescription();
|
---|
| 88 | } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_DIGITIZED /* 0x9292 */) {
|
---|
| 89 | subSecDig = tag.getDescription();
|
---|
[4772] | 90 | }
|
---|
[1169] | 91 | }
|
---|
| 92 | }
|
---|
[11745] | 93 | String dateStr = null;
|
---|
| 94 | String subSeconds = null;
|
---|
| 95 | if (dateTimeOrig != null) {
|
---|
| 96 | // prefer TAG_DATETIME_ORIGINAL
|
---|
| 97 | dateStr = dateTimeOrig;
|
---|
| 98 | subSeconds = subSecOrig;
|
---|
| 99 | } else if (dateTime != null) {
|
---|
| 100 | // TAG_DATETIME is second choice, see #14209
|
---|
[11514] | 101 | dateStr = dateTime;
|
---|
[11745] | 102 | subSeconds = subSec;
|
---|
| 103 | } else if (dateTimeDig != null) {
|
---|
| 104 | dateStr = dateTimeDig;
|
---|
| 105 | subSeconds = subSecDig;
|
---|
[11514] | 106 | }
|
---|
[5610] | 107 | if (dateStr != null) {
|
---|
| 108 | dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228
|
---|
[9499] | 109 | final Date date = DateUtils.fromString(dateStr);
|
---|
| 110 | if (subSeconds != null) {
|
---|
| 111 | try {
|
---|
[11288] | 112 | date.setTime(date.getTime() + (long) (TimeUnit.SECONDS.toMillis(1) * Double.parseDouble("0." + subSeconds)));
|
---|
[9499] | 113 | } catch (NumberFormatException e) {
|
---|
| 114 | Main.warn("Failed parsing sub seconds from [{0}]", subSeconds);
|
---|
| 115 | Main.warn(e);
|
---|
| 116 | }
|
---|
| 117 | }
|
---|
| 118 | return date;
|
---|
[5610] | 119 | }
|
---|
[11745] | 120 | } catch (UncheckedParseException e) {
|
---|
[6643] | 121 | Main.error(e);
|
---|
[626] | 122 | }
|
---|
[4772] | 123 | return null;
|
---|
[1169] | 124 | }
|
---|
[4241] | 125 |
|
---|
[6209] | 126 | /**
|
---|
| 127 | * Returns the image orientation of the given JPEG file.
|
---|
| 128 | * @param filename The JPEG file to read
|
---|
[6830] | 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>
|
---|
[6209] | 138 | * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a>
|
---|
[8509] | 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>
|
---|
[6209] | 141 | */
|
---|
| 142 | public static Integer readOrientation(File filename) {
|
---|
[4241] | 143 | try {
|
---|
| 144 | final Metadata metadata = JpegMetadataReader.readMetadata(filename);
|
---|
[8243] | 145 | final Directory dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
|
---|
[9697] | 146 | return dir == null ? null : dir.getInteger(ExifIFD0Directory.TAG_ORIENTATION);
|
---|
| 147 | } catch (JpegProcessingException | IOException e) {
|
---|
[6643] | 148 | Main.error(e);
|
---|
[4241] | 149 | }
|
---|
[6209] | 150 | return null;
|
---|
[4241] | 151 | }
|
---|
| 152 |
|
---|
[6209] | 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);
|
---|
[8243] | 162 | final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
|
---|
[6209] | 163 | return readLatLon(dirGps);
|
---|
[9697] | 164 | } catch (JpegProcessingException | IOException | MetadataException e) {
|
---|
[6643] | 165 | Main.error(e);
|
---|
[6209] | 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
|
---|
[8470] | 174 | * @throws MetadataException if invalid metadata is given
|
---|
[6209] | 175 | * @since 6209
|
---|
| 176 | */
|
---|
| 177 | public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException {
|
---|
| 178 | if (dirGps != null) {
|
---|
[8132] | 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');
|
---|
[6209] | 181 | return new LatLon(lat, lon);
|
---|
| 182 | }
|
---|
| 183 | return null;
|
---|
| 184 | }
|
---|
[6830] | 185 |
|
---|
[6209] | 186 | /**
|
---|
| 187 | * Returns the direction of the given JPEG file.
|
---|
| 188 | * @param filename The JPEG file to read
|
---|
[8509] | 189 | * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99),
|
---|
[11745] | 190 | * or {@code null} if not found
|
---|
[6209] | 191 | * @since 6209
|
---|
| 192 | */
|
---|
| 193 | public static Double readDirection(File filename) {
|
---|
| 194 | try {
|
---|
| 195 | final Metadata metadata = JpegMetadataReader.readMetadata(filename);
|
---|
[8243] | 196 | final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
|
---|
[6209] | 197 | return readDirection(dirGps);
|
---|
[9697] | 198 | } catch (JpegProcessingException | IOException e) {
|
---|
[6643] | 199 | Main.error(e);
|
---|
[6209] | 200 | }
|
---|
| 201 | return null;
|
---|
| 202 | }
|
---|
[6830] | 203 |
|
---|
[6209] | 204 | /**
|
---|
| 205 | * Returns the direction of the given EXIF GPS directory.
|
---|
| 206 | * @param dirGps The EXIF GPS directory
|
---|
[11745] | 207 | * @return The direction of the image when it was captured (in degrees between 0.0 and 359.99),
|
---|
[8509] | 208 | * or {@code null} if missing or if {@code dirGps} is null
|
---|
[6209] | 209 | * @since 6209
|
---|
| 210 | */
|
---|
| 211 | public static Double readDirection(GpsDirectory dirGps) {
|
---|
| 212 | if (dirGps != null) {
|
---|
[8132] | 213 | Rational direction = dirGps.getRational(GpsDirectory.TAG_IMG_DIRECTION);
|
---|
[6209] | 214 | if (direction != null) {
|
---|
| 215 | return direction.doubleValue();
|
---|
| 216 | }
|
---|
| 217 | }
|
---|
| 218 | return null;
|
---|
| 219 | }
|
---|
| 220 |
|
---|
[10378] | 221 | private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException {
|
---|
[6209] | 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();
|
---|
[6830] | 228 |
|
---|
[6209] | 229 | if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
|
---|
[7864] | 230 | throw new IllegalArgumentException("deg, min and sec are NaN");
|
---|
[6830] | 231 |
|
---|
[9970] | 232 | value = Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600));
|
---|
[6830] | 233 |
|
---|
[6209] | 234 | if (dirGps.getString(gpsTagRef).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 | }
|
---|
[7956] | 243 |
|
---|
| 244 | /**
|
---|
[11745] | 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 | Main.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 | Main.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 | /**
|
---|
[7956] | 328 | * Returns a Transform that fixes the image orientation.
|
---|
| 329 | *
|
---|
[10378] | 330 | * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated as 1.
|
---|
[7956] | 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;
|
---|
[8364] | 342 | ax = width / 2d;
|
---|
| 343 | ay = width / 2d;
|
---|
[7956] | 344 | break;
|
---|
| 345 | case 3:
|
---|
| 346 | q = 2;
|
---|
[8364] | 347 | ax = width / 2d;
|
---|
| 348 | ay = height / 2d;
|
---|
[7956] | 349 | break;
|
---|
| 350 | case 6:
|
---|
| 351 | q = 1;
|
---|
[8364] | 352 | ax = height / 2d;
|
---|
| 353 | ay = height / 2d;
|
---|
[7956] | 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 | }
|
---|
[626] | 387 | }
|
---|