/*
* Copyright 2002-2016 Drew Noakes
*
* 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.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 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<T extends Directory>
{
@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 <code>getString(tagType)</code> 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
* <code>null</code> 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 %s]", length, length == 1 ? "value" : "values");
}
}
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.
* <p>
* Two different formats are processed:
* <ul>
* <li>[30 32 31 30] -> 2.10</li>
* <li>[0 1 0 0] -> 1.00</li>
* </ul>
*
* @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 Integer index = _directory.getInteger(tagType);
if (index == null)
return null;
final int arrayIndex = index - baseIndex;
if (arrayIndex >= 0 && arrayIndex < descriptions.length) {
String description = descriptions[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<String> parts = new ArrayList<String>();
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 getAsciiStringFromBytes(int tag)
{
byte[] values = _directory.getByteArray(tag);
if (values == null)
return null;
try {
return new String(values, "ASCII").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;
}
@Nullable
protected static String getFStopDescription(double fStop)
{
DecimalFormat format = new DecimalFormat("0.0");
format.setRoundingMode(RoundingMode.HALF_UP);
return "f/" + format.format(fStop);
}
@Nullable
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].doubleValue() == 0 && values[2].doubleValue() == 0))
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].doubleValue() != 0) {
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();
}
}