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