source: josm/trunk/src/org/openstreetmap/josm/tools/ExifReader.java@ 9499

Last change on this file since 9499 was 9499, checked in by simon04, 8 years ago

fix #11685 - Use sub-second exif/xmp data

  • Property svn:eol-style set to native
File size: 10.7 KB
RevLine 
[6380]1// License: GPL. For details, see LICENSE file.
[626]2package org.openstreetmap.josm.tools;
3
[8132]4import java.awt.geom.AffineTransform;
[626]5import java.io.File;
[6127]6import java.io.IOException;
[626]7import java.util.Date;
8
[6643]9import org.openstreetmap.josm.Main;
[6209]10import org.openstreetmap.josm.data.coor.LatLon;
[9383]11import org.openstreetmap.josm.tools.date.DateUtils;
[6209]12
[626]13import com.drew.imaging.jpeg.JpegMetadataReader;
[4241]14import com.drew.imaging.jpeg.JpegProcessingException;
[6209]15import com.drew.lang.Rational;
[626]16import com.drew.metadata.Directory;
17import com.drew.metadata.Metadata;
[4241]18import com.drew.metadata.MetadataException;
[626]19import com.drew.metadata.Tag;
[6127]20import com.drew.metadata.exif.ExifIFD0Directory;
21import com.drew.metadata.exif.ExifSubIFDDirectory;
[6209]22import com.drew.metadata.exif.GpsDirectory;
[626]23
24/**
[6209]25 * Read out EXIF information from a JPEG file
[626]26 * @author Imi
[6209]27 * @since 99
[626]28 */
[6362]29public final class ExifReader {
[626]30
[6360]31 private ExifReader() {
32 // Hide default constructor for utils classes
33 }
[6830]34
[6209]35 /**
36 * Returns the date/time from the given JPEG file.
37 * @param filename The JPEG file to read
38 * @return The date/time read in the EXIF section, or {@code null} if not found
39 */
[9383]40 public static Date readTime(File filename) {
[1169]41 try {
42 Metadata metadata = JpegMetadataReader.readMetadata(filename);
[4772]43 String dateStr = null;
[9499]44 String subSeconds = null;
[6127]45 for (Directory dirIt : metadata.getDirectories()) {
46 for (Tag tag : dirIt.getTags()) {
[8244]47 if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */ &&
48 !tag.getDescription().matches("\\[[0-9]+ .+\\]")) {
[4772]49 dateStr = tag.getDescription();
50 }
[6127]51 if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */ ||
52 tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) {
[9499]53 if (dateStr != null) {
54 // prefer TAG_DATETIME_ORIGINAL
55 dateStr = tag.getDescription();
56 }
[4772]57 }
[9499]58 if (tag.getTagType() == ExifIFD0Directory.TAG_SUBSECOND_TIME_ORIGINAL) {
59 subSeconds = tag.getDescription();
60 }
[1169]61 }
62 }
[5610]63 if (dateStr != null) {
64 dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228
[9499]65 final Date date = DateUtils.fromString(dateStr);
66 if (subSeconds != null) {
67 try {
68 date.setTime(date.getTime() + Integer.parseInt(subSeconds));
69 } catch (NumberFormatException e) {
70 Main.warn("Failed parsing sub seconds from [{0}]", subSeconds);
71 Main.warn(e);
72 }
73 }
74 return date;
[5610]75 }
[626]76 } catch (Exception e) {
[6643]77 Main.error(e);
[626]78 }
[4772]79 return null;
[1169]80 }
[4241]81
[6209]82 /**
83 * Returns the image orientation of the given JPEG file.
84 * @param filename The JPEG file to read
[6830]85 * @return The image orientation as an {@code int}. Default value is 1. Possible values are listed in EXIF spec as follows:<br><ol>
86 * <li>The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.</li>
87 * <li>The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.</li>
88 * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.</li>
89 * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.</li>
90 * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.</li>
91 * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.</li>
92 * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.</li>
93 * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.</li></ol>
[6209]94 * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a>
[8509]95 * @see <a href="http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto">
96 * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto</a>
[6209]97 */
98 public static Integer readOrientation(File filename) {
[4241]99 try {
100 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
[8243]101 final Directory dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
[6209]102 return dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
[7004]103 } catch (JpegProcessingException | MetadataException | IOException e) {
[6643]104 Main.error(e);
[4241]105 }
[6209]106 return null;
[4241]107 }
108
[6209]109 /**
110 * Returns the geolocation of the given JPEG file.
111 * @param filename The JPEG file to read
112 * @return The lat/lon read in the EXIF section, or {@code null} if not found
113 * @since 6209
114 */
115 public static LatLon readLatLon(File filename) {
116 try {
117 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
[8243]118 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
[6209]119 return readLatLon(dirGps);
120 } catch (JpegProcessingException e) {
[6643]121 Main.error(e);
[6209]122 } catch (IOException e) {
[6643]123 Main.error(e);
[6209]124 } catch (MetadataException e) {
[6643]125 Main.error(e);
[6209]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
[8470]134 * @throws MetadataException if invalid metadata is given
[6209]135 * @since 6209
136 */
137 public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException {
138 if (dirGps != null) {
[8132]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');
[6209]141 return new LatLon(lat, lon);
142 }
143 return null;
144 }
[6830]145
[6209]146 /**
147 * Returns the direction of the given JPEG file.
148 * @param filename The JPEG file to read
[8509]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
[6209]151 * @since 6209
152 */
153 public static Double readDirection(File filename) {
154 try {
155 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
[8243]156 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
[6209]157 return readDirection(dirGps);
158 } catch (JpegProcessingException e) {
[6643]159 Main.error(e);
[6209]160 } catch (IOException e) {
[6643]161 Main.error(e);
[6209]162 }
163 return null;
164 }
[6830]165
[6209]166 /**
167 * Returns the direction of the given EXIF GPS directory.
168 * @param dirGps The EXIF GPS directory
[8509]169 * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99),
170 * or {@code null} if missing or if {@code dirGps} is null
[6209]171 * @since 6209
172 */
173 public static Double readDirection(GpsDirectory dirGps) {
174 if (dirGps != null) {
[8132]175 Rational direction = dirGps.getRational(GpsDirectory.TAG_IMG_DIRECTION);
[6209]176 if (direction != null) {
177 return direction.doubleValue();
178 }
179 }
180 return null;
181 }
182
183 private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException {
184 double value;
185 Rational[] components = dirGps.getRationalArray(gpsTag);
186 if (components != null) {
187 double deg = components[0].doubleValue();
188 double min = components[1].doubleValue();
189 double sec = components[2].doubleValue();
[6830]190
[6209]191 if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
[7864]192 throw new IllegalArgumentException("deg, min and sec are NaN");
[6830]193
[6209]194 value = (Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600)));
[6830]195
[6209]196 if (dirGps.getString(gpsTagRef).charAt(0) == cRef) {
197 value = -value;
198 }
199 } else {
200 // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220)
201 value = dirGps.getDouble(gpsTag);
202 }
203 return value;
204 }
[7956]205
206 /**
207 * Returns a Transform that fixes the image orientation.
208 *
209 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
210 * as 1.
211 * @param orientation the exif-orientation of the image
212 * @param width the original width of the image
213 * @param height the original height of the image
214 * @return a transform that rotates the image, so it is upright
215 */
216 public static AffineTransform getRestoreOrientationTransform(final int orientation, final int width, final int height) {
217 final int q;
218 final double ax, ay;
219 switch (orientation) {
220 case 8:
221 q = -1;
[8364]222 ax = width / 2d;
223 ay = width / 2d;
[7956]224 break;
225 case 3:
226 q = 2;
[8364]227 ax = width / 2d;
228 ay = height / 2d;
[7956]229 break;
230 case 6:
231 q = 1;
[8364]232 ax = height / 2d;
233 ay = height / 2d;
[7956]234 break;
235 default:
236 q = 0;
237 ax = 0;
238 ay = 0;
239 }
240 return AffineTransform.getQuadrantRotateInstance(q, ax, ay);
241 }
242
243 /**
244 * Check, if the given orientation switches width and height of the image.
245 * E.g. 90 degree rotation
246 *
247 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
248 * as 1.
249 * @param orientation the exif-orientation of the image
250 * @return true, if it switches width and height
251 */
252 public static boolean orientationSwitchesDimensions(int orientation) {
253 return orientation == 6 || orientation == 8;
254 }
255
256 /**
257 * Check, if the given orientation requires any correction to the image.
258 *
259 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
260 * as 1.
261 * @param orientation the exif-orientation of the image
262 * @return true, unless the orientation value is 1 or unsupported.
263 */
264 public static boolean orientationNeedsCorrection(int orientation) {
265 return orientation == 3 || orientation == 6 || orientation == 8;
266 }
[626]267}
Note: See TracBrowser for help on using the repository browser.