source: josm/trunk/src/com/drew/metadata/TagDescriptor.java@ 13500

Last change on this file since 13500 was 13061, checked in by Don-vip, 6 years ago

fix #15505 - update to metadata-extractor 2.10.1

File size: 15.1 KB
Line 
1/*
2 * Copyright 2002-2017 Drew Noakes
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 * More information about this project is available at:
17 *
18 * https://drewnoakes.com/code/exif/
19 * https://github.com/drewnoakes/metadata-extractor
20 */
21package com.drew.metadata;
22
23import com.drew.lang.Rational;
24import com.drew.lang.StringUtil;
25import com.drew.lang.annotations.NotNull;
26import com.drew.lang.annotations.Nullable;
27
28import java.io.UnsupportedEncodingException;
29import java.lang.reflect.Array;
30import java.math.RoundingMode;
31import java.nio.charset.Charset;
32import java.text.DecimalFormat;
33import java.text.SimpleDateFormat;
34import java.util.ArrayList;
35import java.util.Date;
36import java.util.List;
37
38/**
39 * Base class for all tag descriptor classes. Implementations are responsible for
40 * providing the human-readable string representation of tag values stored in a directory.
41 * The directory is provided to the tag descriptor via its constructor.
42 *
43 * @author Drew Noakes https://drewnoakes.com
44 */
45public class TagDescriptor<T extends Directory>
46{
47 @NotNull
48 protected final T _directory;
49
50 public TagDescriptor(@NotNull T directory)
51 {
52 _directory = directory;
53 }
54
55 /**
56 * Returns a descriptive value of the specified tag for this image.
57 * Where possible, known values will be substituted here in place of the raw
58 * tokens actually kept in the metadata segment. If no substitution is
59 * available, the value provided by <code>getString(tagType)</code> will be returned.
60 *
61 * @param tagType the tag to find a description for
62 * @return a description of the image's value for the specified tag, or
63 * <code>null</code> if the tag hasn't been defined.
64 */
65 @Nullable
66 public String getDescription(int tagType)
67 {
68 Object object = _directory.getObject(tagType);
69
70 if (object == null)
71 return null;
72
73 // special presentation for long arrays
74 if (object.getClass().isArray()) {
75 final int length = Array.getLength(object);
76 if (length > 16) {
77 return String.format("[%d values]", length);
78 }
79 }
80
81 if (object instanceof Date)
82 {
83 // Produce a date string having a format that includes the offset in form "+00:00"
84 return new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy")
85 .format((Date) object)
86 .replaceAll("([0-9]{2} [^ ]+)$", ":$1");
87 }
88
89 // no special handling required, so use default conversion to a string
90 return _directory.getString(tagType);
91 }
92
93 /**
94 * Takes a series of 4 bytes from the specified offset, and converts these to a
95 * well-known version number, where possible.
96 * <p>
97 * Two different formats are processed:
98 * <ul>
99 * <li>[30 32 31 30] -&gt; 2.10</li>
100 * <li>[0 1 0 0] -&gt; 1.00</li>
101 * </ul>
102 *
103 * @param components the four version values
104 * @param majorDigits the number of components to be
105 * @return the version as a string of form "2.10" or null if the argument cannot be converted
106 */
107 @Nullable
108 public static String convertBytesToVersionString(@Nullable int[] components, final int majorDigits)
109 {
110 if (components == null)
111 return null;
112 StringBuilder version = new StringBuilder();
113 for (int i = 0; i < 4 && i < components.length; i++) {
114 if (i == majorDigits)
115 version.append('.');
116 char c = (char)components[i];
117 if (c < '0')
118 c += '0';
119 if (i == 0 && c == '0')
120 continue;
121 version.append(c);
122 }
123 return version.toString();
124 }
125
126 @Nullable
127 protected String getVersionBytesDescription(final int tagType, int majorDigits)
128 {
129 int[] values = _directory.getIntArray(tagType);
130 return values == null ? null : convertBytesToVersionString(values, majorDigits);
131 }
132
133 @Nullable
134 protected String getIndexedDescription(final int tagType, @NotNull String... descriptions)
135 {
136 return getIndexedDescription(tagType, 0, descriptions);
137 }
138
139 @Nullable
140 protected String getIndexedDescription(final int tagType, final int baseIndex, @NotNull String... descriptions)
141 {
142 final Integer index = _directory.getInteger(tagType);
143 if (index == null)
144 return null;
145 final int arrayIndex = index - baseIndex;
146 if (arrayIndex >= 0 && arrayIndex < descriptions.length) {
147 String description = descriptions[arrayIndex];
148 if (description != null)
149 return description;
150 }
151 return "Unknown (" + index + ")";
152 }
153
154 @Nullable
155 protected String getByteLengthDescription(final int tagType)
156 {
157 byte[] bytes = _directory.getByteArray(tagType);
158 if (bytes == null)
159 return null;
160 return String.format("(%d byte%s)", bytes.length, bytes.length == 1 ? "" : "s");
161 }
162
163 @Nullable
164 protected String getSimpleRational(final int tagType)
165 {
166 Rational value = _directory.getRational(tagType);
167 if (value == null)
168 return null;
169 return value.toSimpleString(true);
170 }
171
172 @Nullable
173 protected String getDecimalRational(final int tagType, final int decimalPlaces)
174 {
175 Rational value = _directory.getRational(tagType);
176 if (value == null)
177 return null;
178 return String.format("%." + decimalPlaces + "f", value.doubleValue());
179 }
180
181 @Nullable
182 protected String getFormattedInt(final int tagType, @NotNull final String format)
183 {
184 Integer value = _directory.getInteger(tagType);
185 if (value == null)
186 return null;
187 return String.format(format, value);
188 }
189
190 @Nullable
191 protected String getFormattedFloat(final int tagType, @NotNull final String format)
192 {
193 Float value = _directory.getFloatObject(tagType);
194 if (value == null)
195 return null;
196 return String.format(format, value);
197 }
198
199 @Nullable
200 protected String getFormattedString(final int tagType, @NotNull final String format)
201 {
202 String value = _directory.getString(tagType);
203 if (value == null)
204 return null;
205 return String.format(format, value);
206 }
207
208 @Nullable
209 protected String getEpochTimeDescription(final int tagType)
210 {
211 // TODO have observed a byte[8] here which is likely some kind of date (ticks as long?)
212 Long value = _directory.getLongObject(tagType);
213 if (value==null)
214 return null;
215 return new Date(value).toString();
216 }
217
218 /**
219 * LSB first. Labels may be null, a String, or a String[2] with (low label,high label) values.
220 */
221 @Nullable
222 protected String getBitFlagDescription(final int tagType, @NotNull final Object... labels)
223 {
224 Integer value = _directory.getInteger(tagType);
225
226 if (value == null)
227 return null;
228
229 List<String> parts = new ArrayList<String>();
230
231 int bitIndex = 0;
232 while (labels.length > bitIndex) {
233 Object labelObj = labels[bitIndex];
234 if (labelObj != null) {
235 boolean isBitSet = (value & 1) == 1;
236 if (labelObj instanceof String[]) {
237 String[] labelPair = (String[])labelObj;
238 assert(labelPair.length == 2);
239 parts.add(labelPair[isBitSet ? 1 : 0]);
240 } else if (isBitSet && labelObj instanceof String) {
241 parts.add((String)labelObj);
242 }
243 }
244 value >>= 1;
245 bitIndex++;
246 }
247
248 return StringUtil.join(parts, ", ");
249 }
250
251 @Nullable
252 protected String get7BitStringFromBytes(final int tagType)
253 {
254 final byte[] bytes = _directory.getByteArray(tagType);
255
256 if (bytes == null)
257 return null;
258
259 int length = bytes.length;
260 for (int index = 0; index < bytes.length; index++) {
261 int i = bytes[index] & 0xFF;
262 if (i == 0 || i > 0x7F) {
263 length = index;
264 break;
265 }
266 }
267
268 return new String(bytes, 0, length);
269 }
270
271 @Nullable
272 protected String getStringFromBytes(int tag, Charset cs)
273 {
274 byte[] values = _directory.getByteArray(tag);
275
276 if (values == null)
277 return null;
278
279 try {
280 return new String(values, cs.name()).trim();
281 } catch (UnsupportedEncodingException e) {
282 return null;
283 }
284 }
285
286 @Nullable
287 protected String getRationalOrDoubleString(int tagType)
288 {
289 Rational rational = _directory.getRational(tagType);
290 if (rational != null)
291 return rational.toSimpleString(true);
292
293 Double d = _directory.getDoubleObject(tagType);
294 if (d != null)
295 {
296 DecimalFormat format = new DecimalFormat("0.###");
297 return format.format(d);
298 }
299
300 return null;
301 }
302
303 @Nullable
304 protected static String getFStopDescription(double fStop)
305 {
306 DecimalFormat format = new DecimalFormat("0.0");
307 format.setRoundingMode(RoundingMode.HALF_UP);
308 return "f/" + format.format(fStop);
309 }
310
311 @Nullable
312 protected static String getFocalLengthDescription(double mm)
313 {
314 DecimalFormat format = new DecimalFormat("0.#");
315 format.setRoundingMode(RoundingMode.HALF_UP);
316 return format.format(mm) + " mm";
317 }
318
319 @Nullable
320 protected String getLensSpecificationDescription(int tag)
321 {
322 Rational[] values = _directory.getRationalArray(tag);
323
324 if (values == null || values.length != 4 || (values[0].isZero() && values[2].isZero()))
325 return null;
326
327 StringBuilder sb = new StringBuilder();
328
329 if (values[0].equals(values[1]))
330 sb.append(values[0].toSimpleString(true)).append("mm");
331 else
332 sb.append(values[0].toSimpleString(true)).append('-').append(values[1].toSimpleString(true)).append("mm");
333
334 if (!values[2].isZero()) {
335 sb.append(' ');
336
337 DecimalFormat format = new DecimalFormat("0.0");
338 format.setRoundingMode(RoundingMode.HALF_UP);
339
340 if (values[2].equals(values[3]))
341 sb.append(getFStopDescription(values[2].doubleValue()));
342 else
343 sb.append("f/").append(format.format(values[2].doubleValue())).append('-').append(format.format(values[3].doubleValue()));
344 }
345
346 return sb.toString();
347 }
348
349 @Nullable
350 protected String getOrientationDescription(int tag)
351 {
352 return getIndexedDescription(tag, 1,
353 "Top, left side (Horizontal / normal)",
354 "Top, right side (Mirror horizontal)",
355 "Bottom, right side (Rotate 180)",
356 "Bottom, left side (Mirror vertical)",
357 "Left side, top (Mirror horizontal and rotate 270 CW)",
358 "Right side, top (Rotate 90 CW)",
359 "Right side, bottom (Mirror horizontal and rotate 90 CW)",
360 "Left side, bottom (Rotate 270 CW)");
361 }
362
363 @Nullable
364 protected String getShutterSpeedDescription(int tag)
365 {
366 // I believe this method to now be stable, but am leaving some alternative snippets of
367 // code in here, to assist anyone who's looking into this (given that I don't have a public CVS).
368
369// float apexValue = _directory.getFloat(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
370// int apexPower = (int)Math.pow(2.0, apexValue);
371// return "1/" + apexPower + " sec";
372 // TODO test this method
373 // thanks to Mark Edwards for spotting and patching a bug in the calculation of this
374 // description (spotted bug using a Canon EOS 300D)
375 // thanks also to Gli Blr for spotting this bug
376 Float apexValue = _directory.getFloatObject(tag);
377 if (apexValue == null)
378 return null;
379 if (apexValue <= 1) {
380 float apexPower = (float)(1 / (Math.exp(apexValue * Math.log(2))));
381 long apexPower10 = Math.round((double)apexPower * 10.0);
382 float fApexPower = (float)apexPower10 / 10.0f;
383 DecimalFormat format = new DecimalFormat("0.##");
384 format.setRoundingMode(RoundingMode.HALF_UP);
385 return format.format(fApexPower) + " sec";
386 } else {
387 int apexPower = (int)((Math.exp(apexValue * Math.log(2))));
388 return "1/" + apexPower + " sec";
389 }
390
391/*
392 // This alternative implementation offered by Bill Richards
393 // TODO determine which is the correct / more-correct implementation
394 double apexValue = _directory.getDouble(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
395 double apexPower = Math.pow(2.0, apexValue);
396
397 StringBuffer sb = new StringBuffer();
398 if (apexPower > 1)
399 apexPower = Math.floor(apexPower);
400
401 if (apexPower < 1) {
402 sb.append((int)Math.round(1/apexPower));
403 } else {
404 sb.append("1/");
405 sb.append((int)apexPower);
406 }
407 sb.append(" sec");
408 return sb.toString();
409*/
410 }
411
412 // EXIF LightSource
413 @Nullable
414 protected String getLightSourceDescription(short wbtype)
415 {
416 switch (wbtype)
417 {
418 case 0:
419 return "Unknown";
420 case 1:
421 return "Daylight";
422 case 2:
423 return "Fluorescent";
424 case 3:
425 return "Tungsten (Incandescent)";
426 case 4:
427 return "Flash";
428 case 9:
429 return "Fine Weather";
430 case 10:
431 return "Cloudy";
432 case 11:
433 return "Shade";
434 case 12:
435 return "Daylight Fluorescent"; // (D 5700 - 7100K)
436 case 13:
437 return "Day White Fluorescent"; // (N 4600 - 5500K)
438 case 14:
439 return "Cool White Fluorescent"; // (W 3800 - 4500K)
440 case 15:
441 return "White Fluorescent"; // (WW 3250 - 3800K)
442 case 16:
443 return "Warm White Fluorescent"; // (L 2600 - 3250K)
444 case 17:
445 return "Standard Light A";
446 case 18:
447 return "Standard Light B";
448 case 19:
449 return "Standard Light C";
450 case 20:
451 return "D55";
452 case 21:
453 return "D65";
454 case 22:
455 return "D75";
456 case 23:
457 return "D50";
458 case 24:
459 return "ISO Studio Tungsten";
460 case 255:
461 return "Other";
462 }
463
464 return getDescription(wbtype);
465 }
466}
Note: See TracBrowser for help on using the repository browser.