com.drew.metadata.TagDescriptor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of metadata-extractor Show documentation
Show all versions of metadata-extractor Show documentation
Java library for extracting EXIF, IPTC, XMP, ICC and other metadata from image and video files.
The newest version!
/*
* Copyright 2002-2019 Drew Noakes and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* More information about this project is available at:
*
* https://drewnoakes.com/code/exif/
* https://github.com/drewnoakes/metadata-extractor
*/
package com.drew.metadata;
import com.drew.lang.Rational;
import com.drew.lang.StringUtil;
import com.drew.lang.annotations.NotNull;
import com.drew.lang.annotations.Nullable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.math.RoundingMode;
import java.nio.charset.Charset;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Base class for all tag descriptor classes. Implementations are responsible for
* providing the human-readable string representation of tag values stored in a directory.
* The directory is provided to the tag descriptor via its constructor.
*
* @author Drew Noakes https://drewnoakes.com
*/
public class TagDescriptor
{
@NotNull
protected final T _directory;
public TagDescriptor(@NotNull T directory)
{
_directory = directory;
}
/**
* Returns a descriptive value of the specified tag for this image.
* Where possible, known values will be substituted here in place of the raw
* tokens actually kept in the metadata segment. If no substitution is
* available, the value provided by getString(tagType)
will be returned.
*
* @param tagType the tag to find a description for
* @return a description of the image's value for the specified tag, or
* null
if the tag hasn't been defined.
*/
@Nullable
public String getDescription(int tagType)
{
Object object = _directory.getObject(tagType);
if (object == null)
return null;
// special presentation for long arrays
if (object.getClass().isArray()) {
final int length = Array.getLength(object);
if (length > 16) {
return String.format("[%d values]", length);
}
}
if (object instanceof Date) {
// Produce a date string having a format that includes the offset in form "+00:00"
return new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy")
.format((Date) object)
.replaceAll("([0-9]{2} [^ ]+)$", ":$1");
}
// no special handling required, so use default conversion to a string
return _directory.getString(tagType);
}
/**
* Takes a series of 4 bytes from the specified offset, and converts these to a
* well-known version number, where possible.
*
* Two different formats are processed:
*
* - [30 32 31 30] -> 2.10
* - [0 1 0 0] -> 1.00
*
*
* @param components the four version values
* @param majorDigits the number of components to be
* @return the version as a string of form "2.10" or null if the argument cannot be converted
*/
@Nullable
public static String convertBytesToVersionString(@Nullable int[] components, final int majorDigits)
{
if (components == null)
return null;
StringBuilder version = new StringBuilder();
for (int i = 0; i < 4 && i < components.length; i++) {
if (i == majorDigits)
version.append('.');
char c = (char)components[i];
if (c < '0')
c += '0';
if (i == 0 && c == '0')
continue;
version.append(c);
}
return version.toString();
}
@Nullable
protected String getVersionBytesDescription(final int tagType, int majorDigits)
{
int[] values = _directory.getIntArray(tagType);
return values == null ? null : convertBytesToVersionString(values, majorDigits);
}
@Nullable
protected String getIndexedDescription(final int tagType, @NotNull String... descriptions)
{
return getIndexedDescription(tagType, 0, descriptions);
}
@Nullable
protected String getIndexedDescription(final int tagType, final int baseIndex, @NotNull String... descriptions)
{
final Long index = _directory.getLongObject(tagType);
if (index == null)
return null;
final long arrayIndex = index - baseIndex;
if (arrayIndex >= 0 && arrayIndex < (long)descriptions.length) {
String description = descriptions[(int)arrayIndex];
if (description != null)
return description;
}
return "Unknown (" + index + ")";
}
@Nullable
protected String getByteLengthDescription(final int tagType)
{
byte[] bytes = _directory.getByteArray(tagType);
if (bytes == null)
return null;
return String.format("(%d byte%s)", bytes.length, bytes.length == 1 ? "" : "s");
}
@Nullable
protected String getSimpleRational(final int tagType)
{
Rational value = _directory.getRational(tagType);
if (value == null)
return null;
return value.toSimpleString(true);
}
@Nullable
protected String getDecimalRational(final int tagType, final int decimalPlaces)
{
Rational value = _directory.getRational(tagType);
if (value == null)
return null;
return String.format("%." + decimalPlaces + "f", value.doubleValue());
}
@Nullable
protected String getFormattedInt(final int tagType, @NotNull final String format)
{
Integer value = _directory.getInteger(tagType);
if (value == null)
return null;
return String.format(format, value);
}
@Nullable
protected String getFormattedFloat(final int tagType, @NotNull final String format)
{
Float value = _directory.getFloatObject(tagType);
if (value == null)
return null;
return String.format(format, value);
}
@Nullable
protected String getFormattedString(final int tagType, @NotNull final String format)
{
String value = _directory.getString(tagType);
if (value == null)
return null;
return String.format(format, value);
}
@Nullable
protected String getEpochTimeDescription(final int tagType)
{
// TODO have observed a byte[8] here which is likely some kind of date (ticks as long?)
Long value = _directory.getLongObject(tagType);
if (value == null)
return null;
return new Date(value).toString();
}
/**
* LSB first. Labels may be null, a String, or a String[2] with (low label,high label) values.
*/
@Nullable
protected String getBitFlagDescription(final int tagType, @NotNull final Object... labels)
{
Integer value = _directory.getInteger(tagType);
if (value == null)
return null;
List parts = new ArrayList();
int bitIndex = 0;
while (labels.length > bitIndex) {
Object labelObj = labels[bitIndex];
if (labelObj != null) {
boolean isBitSet = (value & 1) == 1;
if (labelObj instanceof String[]) {
String[] labelPair = (String[])labelObj;
assert(labelPair.length == 2);
parts.add(labelPair[isBitSet ? 1 : 0]);
} else if (isBitSet && labelObj instanceof String) {
parts.add((String)labelObj);
}
}
value >>= 1;
bitIndex++;
}
return StringUtil.join(parts, ", ");
}
@Nullable
protected String get7BitStringFromBytes(final int tagType)
{
final byte[] bytes = _directory.getByteArray(tagType);
if (bytes == null)
return null;
int length = bytes.length;
for (int index = 0; index < bytes.length; index++) {
int i = bytes[index] & 0xFF;
if (i == 0 || i > 0x7F) {
length = index;
break;
}
}
return new String(bytes, 0, length);
}
@Nullable
protected String getStringFromBytes(int tag, Charset cs)
{
byte[] values = _directory.getByteArray(tag);
if (values == null)
return null;
try {
return new String(values, cs.name()).trim();
} catch (UnsupportedEncodingException e) {
return null;
}
}
@Nullable
protected String getRationalOrDoubleString(int tagType)
{
Rational rational = _directory.getRational(tagType);
if (rational != null)
return rational.toSimpleString(true);
Double d = _directory.getDoubleObject(tagType);
if (d != null) {
DecimalFormat format = new DecimalFormat("0.###");
return format.format(d);
}
return null;
}
@NotNull
protected static String getFStopDescription(double fStop)
{
DecimalFormat format = new DecimalFormat("0.0");
format.setRoundingMode(RoundingMode.HALF_UP);
return "f/" + format.format(fStop);
}
@NotNull
protected static String getFocalLengthDescription(double mm)
{
DecimalFormat format = new DecimalFormat("0.#");
format.setRoundingMode(RoundingMode.HALF_UP);
return format.format(mm) + " mm";
}
@Nullable
protected String getLensSpecificationDescription(int tag)
{
Rational[] values = _directory.getRationalArray(tag);
if (values == null || values.length != 4 || (values[0].isZero() && values[2].isZero()))
return null;
StringBuilder sb = new StringBuilder();
if (values[0].equals(values[1]))
sb.append(values[0].toSimpleString(true)).append("mm");
else
sb.append(values[0].toSimpleString(true)).append('-').append(values[1].toSimpleString(true)).append("mm");
if (!values[2].isZero()) {
sb.append(' ');
DecimalFormat format = new DecimalFormat("0.0");
format.setRoundingMode(RoundingMode.HALF_UP);
if (values[2].equals(values[3]))
sb.append(getFStopDescription(values[2].doubleValue()));
else
sb.append("f/").append(format.format(values[2].doubleValue())).append('-').append(format.format(values[3].doubleValue()));
}
return sb.toString();
}
@Nullable
protected String getOrientationDescription(int tag)
{
return getIndexedDescription(tag, 1,
"Top, left side (Horizontal / normal)",
"Top, right side (Mirror horizontal)",
"Bottom, right side (Rotate 180)",
"Bottom, left side (Mirror vertical)",
"Left side, top (Mirror horizontal and rotate 270 CW)",
"Right side, top (Rotate 90 CW)",
"Right side, bottom (Mirror horizontal and rotate 90 CW)",
"Left side, bottom (Rotate 270 CW)");
}
@Nullable
protected String getShutterSpeedDescription(int tag)
{
// Thanks to Mark Edwards for spotting and patching a bug in the calculation of this
// description (spotted bug using a Canon EOS 300D).
// Thanks also to Gli Blr for spotting this bug.
Float apexValue = _directory.getFloatObject(tag);
if (apexValue == null)
return null;
if (apexValue <= 1) {
float apexPower = (float)(1 / (Math.exp(apexValue * Math.log(2))));
long apexPower10 = Math.round((double)apexPower * 10.0);
float fApexPower = (float)apexPower10 / 10.0f;
DecimalFormat format = new DecimalFormat("0.##");
format.setRoundingMode(RoundingMode.HALF_UP);
return format.format(fApexPower) + " sec";
} else {
int apexPower = (int)((Math.exp(apexValue * Math.log(2))));
return "1/" + apexPower + " sec";
}
}
// EXIF UserComment, GPSProcessingMethod and GPSAreaInformation
@Nullable
protected String getEncodedTextDescription(int tagType)
{
byte[] commentBytes = _directory.getByteArray(tagType);
if (commentBytes == null)
return null;
if (commentBytes.length == 0)
return "";
final Map encodingMap = new HashMap();
encodingMap.put("ASCII", System.getProperty("file.encoding")); // Someone suggested "ISO-8859-1".
encodingMap.put("UNICODE", "UTF-16LE");
encodingMap.put("JIS", "Shift-JIS"); // We assume this charset for now. Another suggestion is "JIS".
try {
if (commentBytes.length >= 10) {
String firstTenBytesString = new String(commentBytes, 0, 10);
// try each encoding name
for (Map.Entry pair : encodingMap.entrySet()) {
String encodingName = pair.getKey();
String charset = pair.getValue();
if (firstTenBytesString.startsWith(encodingName)) {
// skip any null or blank characters commonly present after the encoding name, up to a limit of 10 from the start
for (int j = encodingName.length(); j < 10; j++) {
byte b = commentBytes[j];
if (b != '\0' && b != ' ')
return new String(commentBytes, j, commentBytes.length - j, charset).trim();
}
return new String(commentBytes, 10, commentBytes.length - 10, charset).trim();
}
}
}
// special handling fell through, return a plain string representation
return new String(commentBytes, System.getProperty("file.encoding")).trim();
} catch (UnsupportedEncodingException ex) {
return null;
}
}
}