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

Last change on this file since 11692 was 11514, checked in by Don-vip, 7 years ago

fix #14209 - EXIF: prefer DATETIME over DATETIME_DIGITIZED

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