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