/* * Copyright 2002-2012 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: * * http://drewnoakes.com/code/exif/ * http://code.google.com/p/metadata-extractor/ */ package com.drew.metadata.exif; import com.drew.imaging.PhotographicConversions; import com.drew.lang.Rational; import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; import java.io.UnsupportedEncodingException; import java.text.DecimalFormat; import java.util.HashMap; import java.util.Map; /** * Provides human-readable string representations of tag values stored in a ExifSubIFDDirectory. * * @author Drew Noakes http://drewnoakes.com */ public class ExifSubIFDDescriptor extends TagDescriptor { /** * Dictates whether rational values will be represented in decimal format in instances * where decimal notation is elegant (such as 1/2 -> 0.5, but not 1/3). */ private final boolean _allowDecimalRepresentationOfRationals = true; @NotNull private static final java.text.DecimalFormat SimpleDecimalFormatter = new DecimalFormat("0.#"); public ExifSubIFDDescriptor(@NotNull ExifSubIFDDirectory directory) { super(directory); } // Note for the potential addition of brightness presentation in eV: // Brightness of taken subject. To calculate Exposure(Ev) from BrightnessValue(Bv), // you must add SensitivityValue(Sv). // Ev=BV+Sv Sv=log2(ISOSpeedRating/3.125) // ISO100:Sv=5, ISO200:Sv=6, ISO400:Sv=7, ISO125:Sv=5.32. /** * Returns a descriptive value of the the specified tag for this image. * Where possible, known values will be substituted here in place of the raw * tokens actually kept in the Exif segment. If no substitution is * available, the value provided by getString(int) 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) { switch (tagType) { case ExifSubIFDDirectory.TAG_NEW_SUBFILE_TYPE: return getNewSubfileTypeDescription(); case ExifSubIFDDirectory.TAG_SUBFILE_TYPE: return getSubfileTypeDescription(); case ExifSubIFDDirectory.TAG_THRESHOLDING: return getThresholdingDescription(); case ExifSubIFDDirectory.TAG_FILL_ORDER: return getFillOrderDescription(); case ExifSubIFDDirectory.TAG_EXPOSURE_TIME: return getExposureTimeDescription(); case ExifSubIFDDirectory.TAG_SHUTTER_SPEED: return getShutterSpeedDescription(); case ExifSubIFDDirectory.TAG_FNUMBER: return getFNumberDescription(); case ExifSubIFDDirectory.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL: return getCompressedAverageBitsPerPixelDescription(); case ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE: return getSubjectDistanceDescription(); case ExifSubIFDDirectory.TAG_METERING_MODE: return getMeteringModeDescription(); case ExifSubIFDDirectory.TAG_WHITE_BALANCE: return getWhiteBalanceDescription(); case ExifSubIFDDirectory.TAG_FLASH: return getFlashDescription(); case ExifSubIFDDirectory.TAG_FOCAL_LENGTH: return getFocalLengthDescription(); case ExifSubIFDDirectory.TAG_COLOR_SPACE: return getColorSpaceDescription(); case ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH: return getExifImageWidthDescription(); case ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT: return getExifImageHeightDescription(); case ExifSubIFDDirectory.TAG_FOCAL_PLANE_UNIT: return getFocalPlaneResolutionUnitDescription(); case ExifSubIFDDirectory.TAG_FOCAL_PLANE_X_RES: return getFocalPlaneXResolutionDescription(); case ExifSubIFDDirectory.TAG_FOCAL_PLANE_Y_RES: return getFocalPlaneYResolutionDescription(); case ExifSubIFDDirectory.TAG_BITS_PER_SAMPLE: return getBitsPerSampleDescription(); case ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION: return getPhotometricInterpretationDescription(); case ExifSubIFDDirectory.TAG_ROWS_PER_STRIP: return getRowsPerStripDescription(); case ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS: return getStripByteCountsDescription(); case ExifSubIFDDirectory.TAG_SAMPLES_PER_PIXEL: return getSamplesPerPixelDescription(); case ExifSubIFDDirectory.TAG_PLANAR_CONFIGURATION: return getPlanarConfigurationDescription(); case ExifSubIFDDirectory.TAG_YCBCR_SUBSAMPLING: return getYCbCrSubsamplingDescription(); case ExifSubIFDDirectory.TAG_EXPOSURE_PROGRAM: return getExposureProgramDescription(); case ExifSubIFDDirectory.TAG_APERTURE: return getApertureValueDescription(); case ExifSubIFDDirectory.TAG_MAX_APERTURE: return getMaxApertureValueDescription(); case ExifSubIFDDirectory.TAG_SENSING_METHOD: return getSensingMethodDescription(); case ExifSubIFDDirectory.TAG_EXPOSURE_BIAS: return getExposureBiasDescription(); case ExifSubIFDDirectory.TAG_FILE_SOURCE: return getFileSourceDescription(); case ExifSubIFDDirectory.TAG_SCENE_TYPE: return getSceneTypeDescription(); case ExifSubIFDDirectory.TAG_COMPONENTS_CONFIGURATION: return getComponentConfigurationDescription(); case ExifSubIFDDirectory.TAG_EXIF_VERSION: return getExifVersionDescription(); case ExifSubIFDDirectory.TAG_FLASHPIX_VERSION: return getFlashPixVersionDescription(); case ExifSubIFDDirectory.TAG_ISO_EQUIVALENT: return getIsoEquivalentDescription(); case ExifSubIFDDirectory.TAG_USER_COMMENT: return getUserCommentDescription(); case ExifSubIFDDirectory.TAG_CUSTOM_RENDERED: return getCustomRenderedDescription(); case ExifSubIFDDirectory.TAG_EXPOSURE_MODE: return getExposureModeDescription(); case ExifSubIFDDirectory.TAG_WHITE_BALANCE_MODE: return getWhiteBalanceModeDescription(); case ExifSubIFDDirectory.TAG_DIGITAL_ZOOM_RATIO: return getDigitalZoomRatioDescription(); case ExifSubIFDDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH: return get35mmFilmEquivFocalLengthDescription(); case ExifSubIFDDirectory.TAG_SCENE_CAPTURE_TYPE: return getSceneCaptureTypeDescription(); case ExifSubIFDDirectory.TAG_GAIN_CONTROL: return getGainControlDescription(); case ExifSubIFDDirectory.TAG_CONTRAST: return getContrastDescription(); case ExifSubIFDDirectory.TAG_SATURATION: return getSaturationDescription(); case ExifSubIFDDirectory.TAG_SHARPNESS: return getSharpnessDescription(); case ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE_RANGE: return getSubjectDistanceRangeDescription(); default: return super.getDescription(tagType); } } @Nullable public String getNewSubfileTypeDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_NEW_SUBFILE_TYPE); if (value==null) return null; switch (value) { case 1: return "Full-resolution image"; case 2: return "Reduced-resolution image"; case 3: return "Single page of multi-page reduced-resolution image"; case 4: return "Transparency mask"; case 5: return "Transparency mask of reduced-resolution image"; case 6: return "Transparency mask of multi-page image"; case 7: return "Transparency mask of reduced-resolution multi-page image"; default: return "Unknown (" + value + ")"; } } @Nullable public String getSubfileTypeDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SUBFILE_TYPE); if (value==null) return null; switch (value) { case 1: return "Full-resolution image"; case 2: return "Reduced-resolution image"; case 3: return "Single page of multi-page image"; default: return "Unknown (" + value + ")"; } } @Nullable public String getThresholdingDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_THRESHOLDING); if (value==null) return null; switch (value) { case 1: return "No dithering or halftoning"; case 2: return "Ordered dither or halftone"; case 3: return "Randomized dither"; default: return "Unknown (" + value + ")"; } } @Nullable public String getFillOrderDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FILL_ORDER); if (value==null) return null; switch (value) { case 1: return "Normal"; case 2: return "Reversed"; default: return "Unknown (" + value + ")"; } } @Nullable public String getSubjectDistanceRangeDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE_RANGE); if (value==null) return null; switch (value) { case 0: return "Unknown"; case 1: return "Macro"; case 2: return "Close view"; case 3: return "Distant view"; default: return "Unknown (" + value + ")"; } } @Nullable public String getSharpnessDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SHARPNESS); if (value==null) return null; switch (value) { case 0: return "None"; case 1: return "Low"; case 2: return "Hard"; default: return "Unknown (" + value + ")"; } } @Nullable public String getSaturationDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SATURATION); if (value==null) return null; switch (value) { case 0: return "None"; case 1: return "Low saturation"; case 2: return "High saturation"; default: return "Unknown (" + value + ")"; } } @Nullable public String getContrastDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_CONTRAST); if (value==null) return null; switch (value) { case 0: return "None"; case 1: return "Soft"; case 2: return "Hard"; default: return "Unknown (" + value + ")"; } } @Nullable public String getGainControlDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_GAIN_CONTROL); if (value==null) return null; switch (value) { case 0: return "None"; case 1: return "Low gain up"; case 2: return "Low gain down"; case 3: return "High gain up"; case 4: return "High gain down"; default: return "Unknown (" + value + ")"; } } @Nullable public String getSceneCaptureTypeDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SCENE_CAPTURE_TYPE); if (value==null) return null; switch (value) { case 0: return "Standard"; case 1: return "Landscape"; case 2: return "Portrait"; case 3: return "Night scene"; default: return "Unknown (" + value + ")"; } } @Nullable public String get35mmFilmEquivFocalLengthDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH); if (value==null) return null; if (value==0) return "Unknown"; else return SimpleDecimalFormatter.format(value) + "mm"; } @Nullable public String getDigitalZoomRatioDescription() { Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_DIGITAL_ZOOM_RATIO); if (value==null) return null; if (value.getNumerator()==0) return "Digital zoom not used."; return SimpleDecimalFormatter.format(value.doubleValue()); } @Nullable public String getWhiteBalanceModeDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_WHITE_BALANCE_MODE); if (value==null) return null; switch (value) { case 0: return "Auto white balance"; case 1: return "Manual white balance"; default: return "Unknown (" + value + ")"; } } @Nullable public String getExposureModeDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXPOSURE_MODE); if (value==null) return null; switch (value) { case 0: return "Auto exposure"; case 1: return "Manual exposure"; case 2: return "Auto bracket"; default: return "Unknown (" + value + ")"; } } @Nullable public String getCustomRenderedDescription() { Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_CUSTOM_RENDERED); if (value==null) return null; switch (value) { case 0: return "Normal process"; case 1: return "Custom process"; default: return "Unknown (" + value + ")"; } } @Nullable public String getUserCommentDescription() { byte[] commentBytes = _directory.getByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT); 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; } } @Nullable public String getIsoEquivalentDescription() { // Have seen an exception here from files produced by ACDSEE that stored an int[] here with two values Integer isoEquiv = _directory.getInteger(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT); if (isoEquiv==null) return null; // There used to be a check here that multiplied ISO values < 50 by 200. // Issue 36 shows a smart-phone image from a Samsung Galaxy S2 with ISO-40. return Integer.toString(isoEquiv); } @Nullable public String getExifVersionDescription() { int[] ints = _directory.getIntArray(ExifSubIFDDirectory.TAG_EXIF_VERSION); if (ints==null) return null; return ExifSubIFDDescriptor.convertBytesToVersionString(ints, 2); } @Nullable public String getFlashPixVersionDescription() { int[] ints = _directory.getIntArray(ExifSubIFDDirectory.TAG_FLASHPIX_VERSION); if (ints==null) return null; return ExifSubIFDDescriptor.convertBytesToVersionString(ints, 2); } @Nullable public String getSceneTypeDescription() { Integer sceneType = _directory.getInteger(ExifSubIFDDirectory.TAG_SCENE_TYPE); if (sceneType==null) return null; return sceneType == 1 ? "Directly photographed image" : "Unknown (" + sceneType + ")"; } @Nullable public String getFileSourceDescription() { Integer fileSource = _directory.getInteger(ExifSubIFDDirectory.TAG_FILE_SOURCE); if (fileSource==null) return null; return fileSource == 3 ? "Digital Still Camera (DSC)" : "Unknown (" + fileSource + ")"; } @Nullable public String getExposureBiasDescription() { Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_EXPOSURE_BIAS); if (value==null) return null; return value.toSimpleString(true) + " EV"; } @Nullable public String getMaxApertureValueDescription() { Double aperture = _directory.getDoubleObject(ExifSubIFDDirectory.TAG_MAX_APERTURE); if (aperture==null) return null; double fStop = PhotographicConversions.apertureToFStop(aperture); return "F" + SimpleDecimalFormatter.format(fStop); } @Nullable public String getApertureValueDescription() { Double aperture = _directory.getDoubleObject(ExifSubIFDDirectory.TAG_APERTURE); if (aperture==null) return null; double fStop = PhotographicConversions.apertureToFStop(aperture); return "F" + SimpleDecimalFormatter.format(fStop); } @Nullable public String getExposureProgramDescription() { // '1' means manual control, '2' program normal, '3' aperture priority, // '4' shutter priority, '5' program creative (slow program), // '6' program action(high-speed program), '7' portrait mode, '8' landscape mode. Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXPOSURE_PROGRAM); if (value==null) return null; switch (value) { case 1: return "Manual control"; case 2: return "Program normal"; case 3: return "Aperture priority"; case 4: return "Shutter priority"; case 5: return "Program creative (slow program)"; case 6: return "Program action (high-speed program)"; case 7: return "Portrait mode"; case 8: return "Landscape mode"; default: return "Unknown program (" + value + ")"; } } @Nullable public String getYCbCrSubsamplingDescription() { int[] positions = _directory.getIntArray(ExifSubIFDDirectory.TAG_YCBCR_SUBSAMPLING); if (positions==null) return null; if (positions[0] == 2 && positions[1] == 1) { return "YCbCr4:2:2"; } else if (positions[0] == 2 && positions[1] == 2) { return "YCbCr4:2:0"; } else { return "(Unknown)"; } } @Nullable public String getPlanarConfigurationDescription() { // When image format is no compression YCbCr, this value shows byte aligns of YCbCr // data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr // plane format. Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_PLANAR_CONFIGURATION); if (value==null) return null; switch (value) { case 1: return "Chunky (contiguous for each subsampling pixel)"; case 2: return "Separate (Y-plane/Cb-plane/Cr-plane format)"; default: return "Unknown configuration"; } } @Nullable public String getSamplesPerPixelDescription() { String value = _directory.getString(ExifSubIFDDirectory.TAG_SAMPLES_PER_PIXEL); return value==null ? null : value + " samples/pixel"; } @Nullable public String getRowsPerStripDescription() { final String value = _directory.getString(ExifSubIFDDirectory.TAG_ROWS_PER_STRIP); return value==null ? null : value + " rows/strip"; } @Nullable public String getStripByteCountsDescription() { final String value = _directory.getString(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS); return value==null ? null : value + " bytes"; } @Nullable public String getPhotometricInterpretationDescription() { // Shows the color space of the image data components Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION); if (value==null) return null; switch (value) { case 0: return "WhiteIsZero"; case 1: return "BlackIsZero"; case 2: return "RGB"; case 3: return "RGB Palette"; case 4: return "Transparency Mask"; case 5: return "CMYK"; case 6: return "YCbCr"; case 8: return "CIELab"; case 9: return "ICCLab"; case 10: return "ITULab"; case 32803: return "Color Filter Array"; case 32844: return "Pixar LogL"; case 32845: return "Pixar LogLuv"; case 32892: return "Linear Raw"; default: return "Unknown colour space"; } } @Nullable public String getBitsPerSampleDescription() { String value = _directory.getString(ExifSubIFDDirectory.TAG_BITS_PER_SAMPLE); return value==null ? null : value + " bits/component/pixel"; } @Nullable public String getFocalPlaneXResolutionDescription() { Rational rational = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_PLANE_X_RES); if (rational==null) return null; final String unit = getFocalPlaneResolutionUnitDescription(); return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) + (unit==null ? "" : " " + unit.toLowerCase()); } @Nullable public String getFocalPlaneYResolutionDescription() { Rational rational = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_PLANE_Y_RES); if (rational==null) return null; final String unit = getFocalPlaneResolutionUnitDescription(); return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) + (unit==null ? "" : " " + unit.toLowerCase()); } @Nullable public String getFocalPlaneResolutionUnitDescription() { // Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit, // '2' inch, '3' centimeter. Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FOCAL_PLANE_UNIT); if (value==null) return null; switch (value) { case 1: return "(No unit)"; case 2: return "Inches"; case 3: return "cm"; default: return ""; } } @Nullable public String getExifImageWidthDescription() { final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH); if (value==null) return null; return value + " pixels"; } @Nullable public String getExifImageHeightDescription() { final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT); if (value==null) return null; return value + " pixels"; } @Nullable public String getColorSpaceDescription() { final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_COLOR_SPACE); if (value==null) return null; if (value == 1) return "sRGB"; if (value == 65535) return "Undefined"; return "Unknown"; } @Nullable public String getFocalLengthDescription() { Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_LENGTH); if (value==null) return null; java.text.DecimalFormat formatter = new DecimalFormat("0.0##"); return formatter.format(value.doubleValue()) + " mm"; } @Nullable public String getFlashDescription() { /* * This is a bitmask. * 0 = flash fired * 1 = return detected * 2 = return able to be detected * 3 = unknown * 4 = auto used * 5 = unknown * 6 = red eye reduction used */ final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FLASH); if (value==null) return null; StringBuilder sb = new StringBuilder(); if ((value & 0x1)!=0) sb.append("Flash fired"); else sb.append("Flash did not fire"); // check if we're able to detect a return, before we mention it if ((value & 0x4)!=0) { if ((value & 0x2)!=0) sb.append(", return detected"); else sb.append(", return not detected"); } if ((value & 0x10)!=0) sb.append(", auto"); if ((value & 0x40)!=0) sb.append(", red-eye reduction"); return sb.toString(); } @Nullable public String getWhiteBalanceDescription() { // '0' means unknown, '1' daylight, '2' fluorescent, '3' tungsten, '10' flash, // '17' standard light A, '18' standard light B, '19' standard light C, '20' D55, // '21' D65, '22' D75, '255' other. final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_WHITE_BALANCE); if (value==null) return null; switch (value) { case 0: return "Unknown"; case 1: return "Daylight"; case 2: return "Florescent"; case 3: return "Tungsten"; case 10: return "Flash"; case 17: return "Standard light"; case 18: return "Standard light (B)"; case 19: return "Standard light (C)"; case 20: return "D55"; case 21: return "D65"; case 22: return "D75"; case 255: return "(Other)"; default: return "Unknown (" + value + ")"; } } @Nullable public String getMeteringModeDescription() { // '0' means unknown, '1' average, '2' center weighted average, '3' spot // '4' multi-spot, '5' multi-segment, '6' partial, '255' other Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_METERING_MODE); if (value==null) return null; switch (value) { case 0: return "Unknown"; case 1: return "Average"; case 2: return "Center weighted average"; case 3: return "Spot"; case 4: return "Multi-spot"; case 5: return "Multi-segment"; case 6: return "Partial"; case 255: return "(Other)"; default: return ""; } } @Nullable public String getSubjectDistanceDescription() { Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE); if (value==null) return null; java.text.DecimalFormat formatter = new DecimalFormat("0.0##"); return formatter.format(value.doubleValue()) + " metres"; } @Nullable public String getCompressedAverageBitsPerPixelDescription() { Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL); if (value==null) return null; String ratio = value.toSimpleString(_allowDecimalRepresentationOfRationals); if (value.isInteger() && value.intValue() == 1) { return ratio + " bit/pixel"; } else { return ratio + " bits/pixel"; } } @Nullable public String getExposureTimeDescription() { String value = _directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME); return value==null ? null : value + " sec"; } @Nullable public String getShutterSpeedDescription() { // I believe this method to now be stable, but am leaving some alternative snippets of // code in here, to assist anyone who's looking into this (given that I don't have a public CVS). // float apexValue = _directory.getFloat(ExifSubIFDDirectory.TAG_SHUTTER_SPEED); // int apexPower = (int)Math.pow(2.0, apexValue); // return "1/" + apexPower + " sec"; // TODO test this method // 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(ExifSubIFDDirectory.TAG_SHUTTER_SPEED); 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; return fApexPower + " sec"; } else { int apexPower = (int)((Math.exp(apexValue*Math.log(2)))); return "1/" + apexPower + " sec"; } /* // This alternative implementation offered by Bill Richards // TODO determine which is the correct / more-correct implementation double apexValue = _directory.getDouble(ExifSubIFDDirectory.TAG_SHUTTER_SPEED); double apexPower = Math.pow(2.0, apexValue); StringBuffer sb = new StringBuffer(); if (apexPower > 1) apexPower = Math.floor(apexPower); if (apexPower < 1) { sb.append((int)Math.round(1/apexPower)); } else { sb.append("1/"); sb.append((int)apexPower); } sb.append(" sec"); return sb.toString(); */ } @Nullable public String getFNumberDescription() { Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_FNUMBER); if (value==null) return null; return "F" + SimpleDecimalFormatter.format(value.doubleValue()); } @Nullable public String getSensingMethodDescription() { // '1' Not defined, '2' One-chip color area sensor, '3' Two-chip color area sensor // '4' Three-chip color area sensor, '5' Color sequential area sensor // '7' Trilinear sensor '8' Color sequential linear sensor, 'Other' reserved Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SENSING_METHOD); if (value==null) return null; switch (value) { case 1: return "(Not defined)"; case 2: return "One-chip color area sensor"; case 3: return "Two-chip color area sensor"; case 4: return "Three-chip color area sensor"; case 5: return "Color sequential area sensor"; case 7: return "Trilinear sensor"; case 8: return "Color sequential linear sensor"; default: return ""; } } @Nullable public String getComponentConfigurationDescription() { int[] components = _directory.getIntArray(ExifSubIFDDirectory.TAG_COMPONENTS_CONFIGURATION); if (components==null) return null; String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"}; StringBuilder componentConfig = new StringBuilder(); for (int i = 0; i < Math.min(4, components.length); i++) { int j = components[i]; if (j > 0 && j < componentStrings.length) { componentConfig.append(componentStrings[j]); } } return componentConfig.toString(); } }