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

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

fix #15427 - NPE

  • Property svn:eol-style set to native
File size: 15.7 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.data.SystemOfMeasurement;
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 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
45 return readTime(metadata);
46 } catch (JpegProcessingException | IOException e) {
47 Logging.error(e);
48 }
49 return null;
50 }
51
52 /**
53 * Returns the date/time from the given JPEG file.
54 * @param metadata The EXIF metadata
55 * @return The date/time read in the EXIF section, or {@code null} if not found
56 * @since 11745
57 */
58 public static Date readTime(Metadata metadata) {
59 try {
60 String dateTimeOrig = null;
61 String dateTime = null;
62 String dateTimeDig = null;
63 String subSecOrig = null;
64 String subSec = null;
65 String subSecDig = null;
66 // The date fields are preferred in this order: DATETIME_ORIGINAL
67 // (0x9003), DATETIME (0x0132), DATETIME_DIGITIZED (0x9004). Some
68 // cameras store the fields in the wrong directory, so all
69 // directories are searched. Assume that the order of the fields
70 // in the directories is random.
71 for (Directory dirIt : metadata.getDirectories()) {
72 if (!(dirIt instanceof ExifDirectoryBase)) {
73 continue;
74 }
75 for (Tag tag : dirIt.getTags()) {
76 if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */ &&
77 !tag.getDescription().matches("\\[[0-9]+ .+\\]")) {
78 dateTimeOrig = tag.getDescription();
79 } else if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */) {
80 dateTime = tag.getDescription();
81 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) {
82 dateTimeDig = tag.getDescription();
83 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_ORIGINAL /* 0x9291 */) {
84 subSecOrig = tag.getDescription();
85 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME /* 0x9290 */) {
86 subSec = tag.getDescription();
87 } else if (tag.getTagType() == ExifSubIFDDirectory.TAG_SUBSECOND_TIME_DIGITIZED /* 0x9292 */) {
88 subSecDig = tag.getDescription();
89 }
90 }
91 }
92 String dateStr = null;
93 String subSeconds = null;
94 if (dateTimeOrig != null) {
95 // prefer TAG_DATETIME_ORIGINAL
96 dateStr = dateTimeOrig;
97 subSeconds = subSecOrig;
98 } else if (dateTime != null) {
99 // TAG_DATETIME is second choice, see #14209
100 dateStr = dateTime;
101 subSeconds = subSec;
102 } else if (dateTimeDig != null) {
103 dateStr = dateTimeDig;
104 subSeconds = subSecDig;
105 }
106 if (dateStr != null) {
107 dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228
108 final Date date = DateUtils.fromString(dateStr);
109 if (subSeconds != null) {
110 try {
111 date.setTime(date.getTime() + (long) (TimeUnit.SECONDS.toMillis(1) * Double.parseDouble("0." + subSeconds)));
112 } catch (NumberFormatException e) {
113 Logging.warn("Failed parsing sub seconds from [{0}]", subSeconds);
114 Logging.warn(e);
115 }
116 }
117 return date;
118 }
119 } catch (UncheckedParseException e) {
120 Logging.error(e);
121 }
122 return null;
123 }
124
125 /**
126 * Returns the image orientation of the given JPEG file.
127 * @param filename The JPEG file to read
128 * @return The image orientation as an {@code int}. Default value is 1. Possible values are listed in EXIF spec as follows:<br><ol>
129 * <li>The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.</li>
130 * <li>The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.</li>
131 * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.</li>
132 * <li>The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.</li>
133 * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.</li>
134 * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.</li>
135 * <li>The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.</li>
136 * <li>The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.</li></ol>
137 * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a>
138 * @see <a href="http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto">
139 * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto</a>
140 */
141 public static Integer readOrientation(File filename) {
142 try {
143 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
144 final Directory dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
145 return dir == null ? null : dir.getInteger(ExifIFD0Directory.TAG_ORIENTATION);
146 } catch (JpegProcessingException | IOException e) {
147 Logging.error(e);
148 }
149 return null;
150 }
151
152 /**
153 * Returns the geolocation of the given JPEG file.
154 * @param filename The JPEG file to read
155 * @return The lat/lon read in the EXIF section, or {@code null} if not found
156 * @since 6209
157 */
158 public static LatLon readLatLon(File filename) {
159 try {
160 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
161 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
162 return readLatLon(dirGps);
163 } catch (JpegProcessingException | IOException | MetadataException e) {
164 Logging.error(e);
165 }
166 return null;
167 }
168
169 /**
170 * Returns the geolocation of the given EXIF GPS directory.
171 * @param dirGps The EXIF GPS directory
172 * @return The lat/lon read in the EXIF section, or {@code null} if {@code dirGps} is null
173 * @throws MetadataException if invalid metadata is given
174 * @since 6209
175 */
176 public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException {
177 if (dirGps != null) {
178 double lat = readAxis(dirGps, GpsDirectory.TAG_LATITUDE, GpsDirectory.TAG_LATITUDE_REF, 'S');
179 double lon = readAxis(dirGps, GpsDirectory.TAG_LONGITUDE, GpsDirectory.TAG_LONGITUDE_REF, 'W');
180 return new LatLon(lat, lon);
181 }
182 return null;
183 }
184
185 /**
186 * Returns the direction of the given JPEG file.
187 * @param filename The JPEG file to read
188 * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99),
189 * or {@code null} if not found
190 * @since 6209
191 */
192 public static Double readDirection(File filename) {
193 try {
194 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
195 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
196 return readDirection(dirGps);
197 } catch (JpegProcessingException | IOException e) {
198 Logging.error(e);
199 }
200 return null;
201 }
202
203 /**
204 * Returns the direction of the given EXIF GPS directory.
205 * @param dirGps The EXIF GPS directory
206 * @return The direction of the image when it was captured (in degrees between 0.0 and 359.99),
207 * or {@code null} if missing or if {@code dirGps} is null
208 * @since 6209
209 */
210 public static Double readDirection(GpsDirectory dirGps) {
211 if (dirGps != null) {
212 Rational direction = dirGps.getRational(GpsDirectory.TAG_IMG_DIRECTION);
213 if (direction != null) {
214 return direction.doubleValue();
215 }
216 }
217 return null;
218 }
219
220 private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException {
221 double value;
222 Rational[] components = dirGps.getRationalArray(gpsTag);
223 if (components != null) {
224 double deg = components[0].doubleValue();
225 double min = components[1].doubleValue();
226 double sec = components[2].doubleValue();
227
228 if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
229 throw new IllegalArgumentException("deg, min and sec are NaN");
230
231 value = Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600));
232
233 String s = dirGps.getString(gpsTagRef);
234 if (s != null && s.charAt(0) == cRef) {
235 value = -value;
236 }
237 } else {
238 // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220)
239 value = dirGps.getDouble(gpsTag);
240 }
241 return value;
242 }
243
244 /**
245 * Returns the speed of the given JPEG file.
246 * @param filename The JPEG file to read
247 * @return The speed of the camera when the image was captured (in km/h),
248 * or {@code null} if not found
249 * @since 11745
250 */
251 public static Double readSpeed(File filename) {
252 try {
253 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
254 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
255 return readSpeed(dirGps);
256 } catch (JpegProcessingException | IOException e) {
257 Logging.error(e);
258 }
259 return null;
260 }
261
262 /**
263 * Returns the speed of the given EXIF GPS directory.
264 * @param dirGps The EXIF GPS directory
265 * @return The speed of the camera when the image was captured (in km/h),
266 * or {@code null} if missing or if {@code dirGps} is null
267 * @since 11745
268 */
269 public static Double readSpeed(GpsDirectory dirGps) {
270 if (dirGps != null) {
271 Double speed = dirGps.getDoubleObject(GpsDirectory.TAG_SPEED);
272 if (speed != null) {
273 final String speedRef = dirGps.getString(GpsDirectory.TAG_SPEED_REF);
274 if ("M".equalsIgnoreCase(speedRef)) {
275 // miles per hour
276 speed *= SystemOfMeasurement.IMPERIAL.bValue / 1000;
277 } else if ("N".equalsIgnoreCase(speedRef)) {
278 // knots == nautical miles per hour
279 speed *= SystemOfMeasurement.NAUTICAL_MILE.bValue / 1000;
280 }
281 // default is K (km/h)
282 return speed;
283 }
284 }
285 return null;
286 }
287
288 /**
289 * Returns the elevation of the given JPEG file.
290 * @param filename The JPEG file to read
291 * @return The elevation of the camera when the image was captured (in m),
292 * or {@code null} if not found
293 * @since 11745
294 */
295 public static Double readElevation(File filename) {
296 try {
297 final Metadata metadata = JpegMetadataReader.readMetadata(filename);
298 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
299 return readElevation(dirGps);
300 } catch (JpegProcessingException | IOException e) {
301 Logging.error(e);
302 }
303 return null;
304 }
305
306 /**
307 * Returns the elevation of the given EXIF GPS directory.
308 * @param dirGps The EXIF GPS directory
309 * @return The elevation of the camera when the image was captured (in m),
310 * or {@code null} if missing or if {@code dirGps} is null
311 * @since 11745
312 */
313 public static Double readElevation(GpsDirectory dirGps) {
314 if (dirGps != null) {
315 Double ele = dirGps.getDoubleObject(GpsDirectory.TAG_ALTITUDE);
316 if (ele != null) {
317 final Integer d = dirGps.getInteger(GpsDirectory.TAG_ALTITUDE_REF);
318 if (d != null && d.intValue() == 1) {
319 ele *= -1;
320 }
321 return ele;
322 }
323 }
324 return null;
325 }
326
327 /**
328 * Returns a Transform that fixes the image orientation.
329 *
330 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated as 1.
331 * @param orientation the exif-orientation of the image
332 * @param width the original width of the image
333 * @param height the original height of the image
334 * @return a transform that rotates the image, so it is upright
335 */
336 public static AffineTransform getRestoreOrientationTransform(final int orientation, final int width, final int height) {
337 final int q;
338 final double ax, ay;
339 switch (orientation) {
340 case 8:
341 q = -1;
342 ax = width / 2d;
343 ay = width / 2d;
344 break;
345 case 3:
346 q = 2;
347 ax = width / 2d;
348 ay = height / 2d;
349 break;
350 case 6:
351 q = 1;
352 ax = height / 2d;
353 ay = height / 2d;
354 break;
355 default:
356 q = 0;
357 ax = 0;
358 ay = 0;
359 }
360 return AffineTransform.getQuadrantRotateInstance(q, ax, ay);
361 }
362
363 /**
364 * Check, if the given orientation switches width and height of the image.
365 * E.g. 90 degree rotation
366 *
367 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
368 * as 1.
369 * @param orientation the exif-orientation of the image
370 * @return true, if it switches width and height
371 */
372 public static boolean orientationSwitchesDimensions(int orientation) {
373 return orientation == 6 || orientation == 8;
374 }
375
376 /**
377 * Check, if the given orientation requires any correction to the image.
378 *
379 * Only orientation 1, 3, 6 and 8 are supported. Everything else is treated
380 * as 1.
381 * @param orientation the exif-orientation of the image
382 * @return true, unless the orientation value is 1 or unsupported.
383 */
384 public static boolean orientationNeedsCorrection(int orientation) {
385 return orientation == 3 || orientation == 6 || orientation == 8;
386 }
387}
Note: See TracBrowser for help on using the repository browser.