001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import java.io.File; 005import java.io.IOException; 006import java.text.ParseException; 007import java.util.Date; 008 009import org.openstreetmap.josm.Main; 010import org.openstreetmap.josm.data.coor.LatLon; 011import org.openstreetmap.josm.tools.date.PrimaryDateParser; 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.ExifIFD0Directory; 021import com.drew.metadata.exif.ExifSubIFDDirectory; 022import com.drew.metadata.exif.GpsDirectory; 023 024/** 025 * Read out EXIF information from a JPEG file 026 * @author Imi 027 * @since 99 028 */ 029public final class ExifReader { 030 031 private ExifReader() { 032 // Hide default constructor for utils classes 033 } 034 035 /** 036 * Returns the date/time from the given JPEG file. 037 * @param filename The JPEG file to read 038 * @return The date/time read in the EXIF section, or {@code null} if not found 039 * @throws ParseException if {@link PrimaryDateParser#parse} fails to parse date/time 040 */ 041 public static Date readTime(File filename) throws ParseException { 042 try { 043 Metadata metadata = JpegMetadataReader.readMetadata(filename); 044 String dateStr = null; 045 OUTER: 046 for (Directory dirIt : metadata.getDirectories()) { 047 for (Tag tag : dirIt.getTags()) { 048 if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */) { 049 dateStr = tag.getDescription(); 050 break OUTER; // prefer this tag 051 } 052 if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */ || 053 tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) { 054 dateStr = tag.getDescription(); 055 } 056 } 057 } 058 if (dateStr != null) { 059 dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228 060 return new PrimaryDateParser().parse(dateStr); 061 } 062 } catch (ParseException e) { 063 throw e; 064 } catch (Exception e) { 065 Main.error(e); 066 } 067 return null; 068 } 069 070 /** 071 * Returns the image orientation of the given JPEG file. 072 * @param filename The JPEG file to read 073 * @return The image orientation as an {@code int}. Default value is 1. Possible values are listed in EXIF spec as follows:<br><ol> 074 * <li>The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.</li> 075 * <li>The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.</li> 076 * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.</li> 077 * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.</li> 078 * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.</li> 079 * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.</li> 080 * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.</li> 081 * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.</li></ol> 082 * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a> 083 * @see <a href="http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto">http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto</a> 084 */ 085 public static Integer readOrientation(File filename) { 086 try { 087 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 088 final Directory dir = metadata.getDirectory(ExifIFD0Directory.class); 089 return dir.getInt(ExifIFD0Directory.TAG_ORIENTATION); 090 } catch (JpegProcessingException | MetadataException | IOException e) { 091 Main.error(e); 092 } 093 return null; 094 } 095 096 /** 097 * Returns the geolocation of the given JPEG file. 098 * @param filename The JPEG file to read 099 * @return The lat/lon read in the EXIF section, or {@code null} if not found 100 * @since 6209 101 */ 102 public static LatLon readLatLon(File filename) { 103 try { 104 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 105 final GpsDirectory dirGps = metadata.getDirectory(GpsDirectory.class); 106 return readLatLon(dirGps); 107 } catch (JpegProcessingException e) { 108 Main.error(e); 109 } catch (IOException e) { 110 Main.error(e); 111 } catch (MetadataException e) { 112 Main.error(e); 113 } 114 return null; 115 } 116 117 /** 118 * Returns the geolocation of the given EXIF GPS directory. 119 * @param dirGps The EXIF GPS directory 120 * @return The lat/lon read in the EXIF section, or {@code null} if {@code dirGps} is null 121 * @throws MetadataException 122 * @since 6209 123 */ 124 public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException { 125 if (dirGps != null) { 126 double lat = readAxis(dirGps, GpsDirectory.TAG_GPS_LATITUDE, GpsDirectory.TAG_GPS_LATITUDE_REF, 'S'); 127 double lon = readAxis(dirGps, GpsDirectory.TAG_GPS_LONGITUDE, GpsDirectory.TAG_GPS_LONGITUDE_REF, 'W'); 128 return new LatLon(lat, lon); 129 } 130 return null; 131 } 132 133 /** 134 * Returns the direction of the given JPEG file. 135 * @param filename The JPEG file to read 136 * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99), or {@code null} if missing or if {@code dirGps} is null 137 * @since 6209 138 */ 139 public static Double readDirection(File filename) { 140 try { 141 final Metadata metadata = JpegMetadataReader.readMetadata(filename); 142 final GpsDirectory dirGps = metadata.getDirectory(GpsDirectory.class); 143 return readDirection(dirGps); 144 } catch (JpegProcessingException e) { 145 Main.error(e); 146 } catch (IOException e) { 147 Main.error(e); 148 } 149 return null; 150 } 151 152 /** 153 * Returns the direction of the given EXIF GPS directory. 154 * @param dirGps The EXIF GPS directory 155 * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99), or {@code null} if missing or if {@code dirGps} is null 156 * @since 6209 157 */ 158 public static Double readDirection(GpsDirectory dirGps) { 159 if (dirGps != null) { 160 Rational direction = dirGps.getRational(GpsDirectory.TAG_GPS_IMG_DIRECTION); 161 if (direction != null) { 162 return direction.doubleValue(); 163 } 164 } 165 return null; 166 } 167 168 private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException { 169 double value; 170 Rational[] components = dirGps.getRationalArray(gpsTag); 171 if (components != null) { 172 double deg = components[0].doubleValue(); 173 double min = components[1].doubleValue(); 174 double sec = components[2].doubleValue(); 175 176 if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec)) 177 throw new IllegalArgumentException("deg, min and sec are NaN"); 178 179 value = (Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600))); 180 181 if (dirGps.getString(gpsTagRef).charAt(0) == cRef) { 182 value = -value; 183 } 184 } else { 185 // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220) 186 value = dirGps.getDouble(gpsTag); 187 } 188 return value; 189 } 190}