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.Main; 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 Metadata metadata = JpegMetadataReader.readMetadata(filename); 045 String dateStr = null; 046 String subSeconds = null; 047 for (Directory dirIt : metadata.getDirectories()) { 048 if (!(dirIt instanceof ExifDirectoryBase)) { 049 continue; 050 } 051 for (Tag tag : dirIt.getTags()) { 052 if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */ && 053 !tag.getDescription().matches("\\[[0-9]+ .+\\]")) { 054 dateStr = tag.getDescription(); 055 } 056 if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */ || 057 tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) { 058 if (dateStr != null) { 059 // prefer TAG_DATETIME_ORIGINAL 060 dateStr = tag.getDescription(); 061 } 062 } 063 if (tag.getTagType() == ExifIFD0Directory.TAG_SUBSECOND_TIME_ORIGINAL) { 064 subSeconds = tag.getDescription(); 065 } 066 } 067 } 068 if (dateStr != null) { 069 dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228 070 final Date date = DateUtils.fromString(dateStr); 071 if (subSeconds != null) { 072 try { 073 date.setTime(date.getTime() + (long) (TimeUnit.SECONDS.toMillis(1) * Double.parseDouble("0." + subSeconds))); 074 } catch (NumberFormatException e) { 075 Main.warn("Failed parsing sub seconds from [{0}]", subSeconds); 076 Main.warn(e); 077 } 078 } 079 return date; 080 } 081 } catch (UncheckedParseException | JpegProcessingException | IOException e) { 082 Main.error(e); 083 } 084 return null; 085 } 086 087 /** 088 * Returns the image orientation of the given JPEG file. 089 * @param filename The JPEG file to read 090 * @return The image orientation as an {@code int}. Default value is 1. Possible values are listed in EXIF spec as follows:<br><ol> 091 * <li>The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.</li> 092 * <li>The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.</li> 093 * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.</li> 094 * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.</li> 095 * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.</li> 096 * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.</li> 097 * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.</li> 098 * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.</li></ol> 099 * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a> 100 * @see <a href="http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto"> 101 * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto</a> 102 */ 103 public static Integer readOrientation(File filename) { 104 try { 105 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 106 final Directory dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); 107 return dir == null ? null : dir.getInteger(ExifIFD0Directory.TAG_ORIENTATION); 108 } catch (JpegProcessingException | IOException e) { 109 Main.error(e); 110 } 111 return null; 112 } 113 114 /** 115 * Returns the geolocation of the given JPEG file. 116 * @param filename The JPEG file to read 117 * @return The lat/lon read in the EXIF section, or {@code null} if not found 118 * @since 6209 119 */ 120 public static LatLon readLatLon(File filename) { 121 try { 122 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 123 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class); 124 return readLatLon(dirGps); 125 } catch (JpegProcessingException | IOException | MetadataException e) { 126 Main.error(e); 127 } 128 return null; 129 } 130 131 /** 132 * Returns the geolocation of the given EXIF GPS directory. 133 * @param dirGps The EXIF GPS directory 134 * @return The lat/lon read in the EXIF section, or {@code null} if {@code dirGps} is null 135 * @throws MetadataException if invalid metadata is given 136 * @since 6209 137 */ 138 public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException { 139 if (dirGps != null) { 140 double lat = readAxis(dirGps, GpsDirectory.TAG_LATITUDE, GpsDirectory.TAG_LATITUDE_REF, 'S'); 141 double lon = readAxis(dirGps, GpsDirectory.TAG_LONGITUDE, GpsDirectory.TAG_LONGITUDE_REF, 'W'); 142 return new LatLon(lat, lon); 143 } 144 return null; 145 } 146 147 /** 148 * Returns the direction of the given JPEG file. 149 * @param filename The JPEG file to read 150 * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99), 151 * or {@code null} if missing or if {@code dirGps} is null 152 * @since 6209 153 */ 154 public static Double readDirection(File filename) { 155 try { 156 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 157 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class); 158 return readDirection(dirGps); 159 } catch (JpegProcessingException | IOException e) { 160 Main.error(e); 161 } 162 return null; 163 } 164 165 /** 166 * Returns the direction of the given EXIF GPS directory. 167 * @param dirGps The EXIF GPS directory 168 * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99), 169 * or {@code null} if missing or if {@code dirGps} is null 170 * @since 6209 171 */ 172 public static Double readDirection(GpsDirectory dirGps) { 173 if (dirGps != null) { 174 Rational direction = dirGps.getRational(GpsDirectory.TAG_IMG_DIRECTION); 175 if (direction != null) { 176 return direction.doubleValue(); 177 } 178 } 179 return null; 180 } 181 182 private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException { 183 double value; 184 Rational[] components = dirGps.getRationalArray(gpsTag); 185 if (components != null) { 186 double deg = components[0].doubleValue(); 187 double min = components[1].doubleValue(); 188 double sec = components[2].doubleValue(); 189 190 if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec)) 191 throw new IllegalArgumentException("deg, min and sec are NaN"); 192 193 value = Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600)); 194 195 if (dirGps.getString(gpsTagRef).charAt(0) == cRef) { 196 value = -value; 197 } 198 } else { 199 // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220) 200 value = dirGps.getDouble(gpsTag); 201 } 202 return value; 203 } 204 205 /** 206 * Returns a Transform that fixes the image orientation. 207 * 208 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated as 1. 209 * @param orientation the exif-orientation of the image 210 * @param width the original width of the image 211 * @param height the original height of the image 212 * @return a transform that rotates the image, so it is upright 213 */ 214 public static AffineTransform getRestoreOrientationTransform(final int orientation, final int width, final int height) { 215 final int q; 216 final double ax, ay; 217 switch (orientation) { 218 case 8: 219 q = -1; 220 ax = width / 2d; 221 ay = width / 2d; 222 break; 223 case 3: 224 q = 2; 225 ax = width / 2d; 226 ay = height / 2d; 227 break; 228 case 6: 229 q = 1; 230 ax = height / 2d; 231 ay = height / 2d; 232 break; 233 default: 234 q = 0; 235 ax = 0; 236 ay = 0; 237 } 238 return AffineTransform.getQuadrantRotateInstance(q, ax, ay); 239 } 240 241 /** 242 * Check, if the given orientation switches width and height of the image. 243 * E.g. 90 degree rotation 244 * 245 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated 246 * as 1. 247 * @param orientation the exif-orientation of the image 248 * @return true, if it switches width and height 249 */ 250 public static boolean orientationSwitchesDimensions(int orientation) { 251 return orientation == 6 || orientation == 8; 252 } 253 254 /** 255 * Check, if the given orientation requires any correction to the image. 256 * 257 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated 258 * as 1. 259 * @param orientation the exif-orientation of the image 260 * @return true, unless the orientation value is 1 or unsupported. 261 */ 262 public static boolean orientationNeedsCorrection(int orientation) { 263 return orientation == 3 || orientation == 6 || orientation == 8; 264 } 265}