/*
 * ExifDescriptor.java
 *
 * This is public domain software - that is, you can do whatever you want
 * with it, and include it software that is licensed under the GNU or the
 * BSD license, or whatever other licence you choose, including proprietary
 * closed source licenses.  I do ask that you leave this header in tact.
 *
 * If you make modifications to this code that you think would benefit the
 * wider community, please send me a copy and I'll post it on my site.
 *
 * If you make use of this code, I'd appreciate hearing about it.
 *   drew@drewnoakes.com
 * Latest version of this software kept at
 *   http://drewnoakes.com/
 *
 * Created by dnoakes on 12-Nov-2002 22:27:15 using IntelliJ IDEA.
 */
package com.drew.metadata.exif;

import com.drew.imaging.PhotographicConversions;
import com.drew.lang.Rational;
import com.drew.metadata.Directory;
import com.drew.metadata.MetadataException;
import com.drew.metadata.TagDescriptor;

import java.io.UnsupportedEncodingException;
import java.text.DecimalFormat;

/**
 * Contains all logic for the presentation of raw Exif data, as stored in ExifDirectory.  Use
 * this class to provide human-readable descriptions of tag values.
 */
public class ExifDescriptor 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 boolean _allowDecimalRepresentationOfRationals = true;

    private static final java.text.DecimalFormat SimpleDecimalFormatter = new DecimalFormat("0.#");

    public ExifDescriptor(Directory directory)
    {
        super(directory);
    }

    // Note for the potential addition of brightness presentation in eV:
    // Brightness of taken subject. To calculate Exposure(Ev) from BrigtnessValue(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
     *         <code>null</code> if the tag hasn't been defined.
     */
    public String getDescription(int tagType) throws MetadataException
    {
        switch (tagType) {
            case ExifDirectory.TAG_ORIENTATION:
                return getOrientationDescription();
            case ExifDirectory.TAG_NEW_SUBFILE_TYPE:
                return getNewSubfileTypeDescription();
            case ExifDirectory.TAG_SUBFILE_TYPE:
                return getSubfileTypeDescription();
            case ExifDirectory.TAG_THRESHOLDING:
                return getThresholdingDescription();
            case ExifDirectory.TAG_FILL_ORDER:
                return getFillOrderDescription();
            case ExifDirectory.TAG_RESOLUTION_UNIT:
                return getResolutionDescription();
            case ExifDirectory.TAG_YCBCR_POSITIONING:
                return getYCbCrPositioningDescription();
            case ExifDirectory.TAG_EXPOSURE_TIME:
                return getExposureTimeDescription();
            case ExifDirectory.TAG_SHUTTER_SPEED:
                return getShutterSpeedDescription();
            case ExifDirectory.TAG_FNUMBER:
                return getFNumberDescription();
            case ExifDirectory.TAG_X_RESOLUTION:
                return getXResolutionDescription();
            case ExifDirectory.TAG_Y_RESOLUTION:
                return getYResolutionDescription();
            case ExifDirectory.TAG_THUMBNAIL_OFFSET:
                return getThumbnailOffsetDescription();
            case ExifDirectory.TAG_THUMBNAIL_LENGTH:
                return getThumbnailLengthDescription();
            case ExifDirectory.TAG_COMPRESSION_LEVEL:
                return getCompressionLevelDescription();
            case ExifDirectory.TAG_SUBJECT_DISTANCE:
                return getSubjectDistanceDescription();
            case ExifDirectory.TAG_METERING_MODE:
                return getMeteringModeDescription();
            case ExifDirectory.TAG_WHITE_BALANCE:
                return getWhiteBalanceDescription();
            case ExifDirectory.TAG_FLASH:
                return getFlashDescription();
            case ExifDirectory.TAG_FOCAL_LENGTH:
                return getFocalLengthDescription();
            case ExifDirectory.TAG_COLOR_SPACE:
                return getColorSpaceDescription();
            case ExifDirectory.TAG_EXIF_IMAGE_WIDTH:
                return getExifImageWidthDescription();
            case ExifDirectory.TAG_EXIF_IMAGE_HEIGHT:
                return getExifImageHeightDescription();
            case ExifDirectory.TAG_FOCAL_PLANE_UNIT:
                return getFocalPlaneResolutionUnitDescription();
            case ExifDirectory.TAG_FOCAL_PLANE_X_RES:
                return getFocalPlaneXResolutionDescription();
            case ExifDirectory.TAG_FOCAL_PLANE_Y_RES:
                return getFocalPlaneYResolutionDescription();
            case ExifDirectory.TAG_THUMBNAIL_IMAGE_WIDTH:
                return getThumbnailImageWidthDescription();
            case ExifDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT:
                return getThumbnailImageHeightDescription();
            case ExifDirectory.TAG_BITS_PER_SAMPLE:
                return getBitsPerSampleDescription();
            case ExifDirectory.TAG_COMPRESSION:
                return getCompressionDescription();
            case ExifDirectory.TAG_PHOTOMETRIC_INTERPRETATION:
                return getPhotometricInterpretationDescription();
            case ExifDirectory.TAG_ROWS_PER_STRIP:
                return getRowsPerStripDescription();
            case ExifDirectory.TAG_STRIP_BYTE_COUNTS:
                return getStripByteCountsDescription();
            case ExifDirectory.TAG_SAMPLES_PER_PIXEL:
                return getSamplesPerPixelDescription();
            case ExifDirectory.TAG_PLANAR_CONFIGURATION:
                return getPlanarConfigurationDescription();
            case ExifDirectory.TAG_YCBCR_SUBSAMPLING:
                return getYCbCrSubsamplingDescription();
            case ExifDirectory.TAG_EXPOSURE_PROGRAM:
                return getExposureProgramDescription();
            case ExifDirectory.TAG_APERTURE:
                return getApertureValueDescription();
            case ExifDirectory.TAG_MAX_APERTURE:
                return getMaxApertureValueDescription();
            case ExifDirectory.TAG_SENSING_METHOD:
                return getSensingMethodDescription();
            case ExifDirectory.TAG_EXPOSURE_BIAS:
                return getExposureBiasDescription();
            case ExifDirectory.TAG_FILE_SOURCE:
                return getFileSourceDescription();
            case ExifDirectory.TAG_SCENE_TYPE:
                return getSceneTypeDescription();
            case ExifDirectory.TAG_COMPONENTS_CONFIGURATION:
                return getComponentConfigurationDescription();
            case ExifDirectory.TAG_EXIF_VERSION:
                return getExifVersionDescription();
            case ExifDirectory.TAG_FLASHPIX_VERSION:
                return getFlashPixVersionDescription();
            case ExifDirectory.TAG_REFERENCE_BLACK_WHITE:
                return getReferenceBlackWhiteDescription();
            case ExifDirectory.TAG_ISO_EQUIVALENT:
                return getIsoEquivalentDescription();
            case ExifDirectory.TAG_THUMBNAIL_DATA:
                return getThumbnailDescription();
            case ExifDirectory.TAG_USER_COMMENT:
                return getUserCommentDescription();
            case ExifDirectory.TAG_CUSTOM_RENDERED:
                return getCustomRenderedDescription();
            case ExifDirectory.TAG_EXPOSURE_MODE:
                return getExposureModeDescription();
            case ExifDirectory.TAG_WHITE_BALANCE_MODE:
                return getWhiteBalanceModeDescription();
            case ExifDirectory.TAG_DIGITAL_ZOOM_RATIO:
                return getDigitalZoomRatioDescription();
            case ExifDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH:
                return get35mmFilmEquivFocalLengthDescription();
            case ExifDirectory.TAG_SCENE_CAPTURE_TYPE:
                return getSceneCaptureTypeDescription();
            case ExifDirectory.TAG_GAIN_CONTROL:
                return getGainControlDescription();
            case ExifDirectory.TAG_CONTRAST:
                return getContrastDescription();
            case ExifDirectory.TAG_SATURATION:
                return getSaturationDescription();
            case ExifDirectory.TAG_SHARPNESS:
                return getSharpnessDescription();
            case ExifDirectory.TAG_SUBJECT_DISTANCE_RANGE:
                return getSubjectDistanceRangeDescription();

            case ExifDirectory.TAG_WIN_AUTHOR:
               return getWindowsAuthorDescription();
            case ExifDirectory.TAG_WIN_COMMENT:
               return getWindowsCommentDescription();
            case ExifDirectory.TAG_WIN_KEYWORDS:
               return getWindowsKeywordsDescription();
            case ExifDirectory.TAG_WIN_SUBJECT:
               return getWindowsSubjectDescription();
            case ExifDirectory.TAG_WIN_TITLE:
               return getWindowsTitleDescription();
            default:
                return _directory.getString(tagType);
        }
    }

    public String getNewSubfileTypeDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_NEW_SUBFILE_TYPE)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_NEW_SUBFILE_TYPE)) {
            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 (" + _directory.getInt(ExifDirectory.TAG_NEW_SUBFILE_TYPE) + ")";
        }
    }

    public String getSubfileTypeDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_SUBFILE_TYPE)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_SUBFILE_TYPE)) {
            case 1: return "Full-resolution image";
            case 2: return "Reduced-resolution image";
            case 3: return "Single page of multi-page image";
            default:
                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_SUBFILE_TYPE) + ")";
        }
    }

    public String getThresholdingDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_THRESHOLDING)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_THRESHOLDING)) {
            case 1: return "No dithering or halftoning";
            case 2: return "Ordered dither or halftone";
            case 3: return "Randomized dither";
            default:
                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_THRESHOLDING) + ")";
        }
    }

    public String getFillOrderDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_FILL_ORDER)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_FILL_ORDER)) {
            case 1: return "Normal";
            case 2: return "Reversed";
            default:
                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_FILL_ORDER) + ")";
        }
    }

    public String getSubjectDistanceRangeDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_SUBJECT_DISTANCE_RANGE)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_SUBJECT_DISTANCE_RANGE)) {
            case 0:
                return "Unknown";
            case 1:
                return "Macro";
            case 2:
                return "Close view";
            case 3:
                return "Distant view";
            default:
                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_SUBJECT_DISTANCE_RANGE) + ")";
        }
    }

    public String getSharpnessDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_SHARPNESS)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_SHARPNESS)) {
            case 0:
                return "None";
            case 1:
                return "Low";
            case 2:
                return "Hard";
            default:
                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_SHARPNESS) + ")";
        }
    }

    public String getSaturationDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_SATURATION)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_SATURATION)) {
            case 0:
                return "None";
            case 1:
                return "Low saturation";
            case 2:
                return "High saturation";
            default:
                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_SATURATION) + ")";
        }
    }

    public String getContrastDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_CONTRAST)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_CONTRAST)) {
            case 0:
                return "None";
            case 1:
                return "Soft";
            case 2:
                return "Hard";
            default:
                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_CONTRAST) + ")";
        }
    }

    public String getGainControlDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_GAIN_CONTROL)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_GAIN_CONTROL)) {
            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 (" + _directory.getInt(ExifDirectory.TAG_GAIN_CONTROL) + ")";
        }
    }

    public String getSceneCaptureTypeDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_SCENE_CAPTURE_TYPE)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_SCENE_CAPTURE_TYPE)) {
            case 0:
                return "Standard";
            case 1:
                return "Landscape";
            case 2:
                return "Portrait";
            case 3:
                return "Night scene";
            default:
                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_SCENE_CAPTURE_TYPE) + ")";
        }
    }

    public String get35mmFilmEquivFocalLengthDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH)) return null;
        int equivalentFocalLength = _directory.getInt(ExifDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH);

        if (equivalentFocalLength==0)
            return "Unknown";
        else
            return SimpleDecimalFormatter.format(equivalentFocalLength) + "mm";
    }

    public String getDigitalZoomRatioDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_DIGITAL_ZOOM_RATIO)) return null;
        Rational rational = _directory.getRational(ExifDirectory.TAG_DIGITAL_ZOOM_RATIO);
        if (rational.getNumerator()==0)
            return "Digital zoom not used.";

        return SimpleDecimalFormatter.format(rational.doubleValue());
    }

    public String getWhiteBalanceModeDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_WHITE_BALANCE_MODE)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_WHITE_BALANCE_MODE)) {
            case 0:
                return "Auto white balance";
            case 1:
                return "Manual white balance";
            default:
                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_WHITE_BALANCE_MODE) + ")";
        }
    }

    public String getExposureModeDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_EXPOSURE_MODE)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_EXPOSURE_MODE)) {
            case 0:
                return "Auto exposure";
            case 1:
                return "Manual exposure";
            case 2:
                return "Auto bracket";
            default:
                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_EXPOSURE_MODE) + ")";
        }
    }

    public String getCustomRenderedDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_CUSTOM_RENDERED)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_CUSTOM_RENDERED)) {
            case 0:
                return "Normal process";
            case 1:
                return "Custom process";
            default:
                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_CUSTOM_RENDERED) + ")";
        }
    }

    public String getUserCommentDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_USER_COMMENT)) return null;

        byte[] commentBytes = _directory.getByteArray(ExifDirectory.TAG_USER_COMMENT);

        if (commentBytes.length==0)
            return "";

        final String[] encodingNames = new String[] { "ASCII", "UNICODE", "JIS" };

        if (commentBytes.length>=10)
        {
            String encodingRegion = new String(commentBytes, 0, 10);

            // try each encoding name
            for (int i = 0; i<encodingNames.length; i++) {
                String encodingName = encodingNames[i];
                if (encodingRegion.startsWith(encodingName))
                {
                    // remove the null characters (and any spaces) commonly present after the encoding name
                    for (int j = encodingName.length(); j<10; j++) {
                        byte b = commentBytes[j];
                        if (b!='\0' && b!=' ') {
                           if (encodingName.equals("UNICODE")) {
                              try {
                                 return new String(commentBytes, j, commentBytes.length - j, "UTF-16LE").trim();
                              }
                              catch (UnsupportedEncodingException ex) {
                                 return null;
                              }
                           }
                           return new String(commentBytes, j, commentBytes.length - j).trim();
                        }
                    }
                    return new String(commentBytes, 10, commentBytes.length - 10).trim();
                }
            }
        }

        // special handling fell through, return a plain string representation
        return new String(commentBytes).trim();
    }

    public String getThumbnailDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_THUMBNAIL_DATA)) return null;
        int[] thumbnailBytes = _directory.getIntArray(ExifDirectory.TAG_THUMBNAIL_DATA);
        return "[" + thumbnailBytes.length + " bytes of thumbnail data]";
    }

    public String getIsoEquivalentDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_ISO_EQUIVALENT)) return null;
        int isoEquiv = _directory.getInt(ExifDirectory.TAG_ISO_EQUIVALENT);
        if (isoEquiv < 50) {
            isoEquiv *= 200;
        }
        return Integer.toString(isoEquiv);
    }

    public String getReferenceBlackWhiteDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_REFERENCE_BLACK_WHITE)) return null;
        int[] ints = _directory.getIntArray(ExifDirectory.TAG_REFERENCE_BLACK_WHITE);
        int blackR = ints[0];
        int whiteR = ints[1];
        int blackG = ints[2];
        int whiteG = ints[3];
        int blackB = ints[4];
        int whiteB = ints[5];
        String pos = "[" + blackR + "," + blackG + "," + blackB + "] " +
                "[" + whiteR + "," + whiteG + "," + whiteB + "]";
        return pos;
    }

    public String getExifVersionDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_EXIF_VERSION)) return null;
        int[] ints = _directory.getIntArray(ExifDirectory.TAG_EXIF_VERSION);
        return ExifDescriptor.convertBytesToVersionString(ints);
    }

    public String getFlashPixVersionDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_FLASHPIX_VERSION)) return null;
        int[] ints = _directory.getIntArray(ExifDirectory.TAG_FLASHPIX_VERSION);
        return ExifDescriptor.convertBytesToVersionString(ints);
    }

    public String getSceneTypeDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_SCENE_TYPE)) return null;
        int sceneType = _directory.getInt(ExifDirectory.TAG_SCENE_TYPE);
        if (sceneType == 1) {
            return "Directly photographed image";
        } else {
            return "Unknown (" + sceneType + ")";
        }
    }

    public String getFileSourceDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_FILE_SOURCE)) return null;
        int fileSource = _directory.getInt(ExifDirectory.TAG_FILE_SOURCE);
        if (fileSource == 3) {
            return "Digital Still Camera (DSC)";
        } else {
            return "Unknown (" + fileSource + ")";
        }
    }

    public String getExposureBiasDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_EXPOSURE_BIAS)) return null;
        Rational exposureBias = _directory.getRational(ExifDirectory.TAG_EXPOSURE_BIAS);
        return exposureBias.toSimpleString(true) + " EV";
    }

    public String getMaxApertureValueDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_MAX_APERTURE)) return null;
        double aperture = _directory.getDouble(ExifDirectory.TAG_MAX_APERTURE);
        double fStop = PhotographicConversions.apertureToFStop(aperture);
        return "F" + SimpleDecimalFormatter.format(fStop);
    }

    public String getApertureValueDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_APERTURE)) return null;
        double aperture = _directory.getDouble(ExifDirectory.TAG_APERTURE);
        double fStop = PhotographicConversions.apertureToFStop(aperture);
        return "F" + SimpleDecimalFormatter.format(fStop);
    }

    public String getExposureProgramDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_EXPOSURE_PROGRAM)) return null;
        // '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.
        switch (_directory.getInt(ExifDirectory.TAG_EXPOSURE_PROGRAM)) {
            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 (" + _directory.getInt(ExifDirectory.TAG_EXPOSURE_PROGRAM) + ")";
        }
    }

    public String getYCbCrSubsamplingDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_YCBCR_SUBSAMPLING)) return null;
        int[] positions = _directory.getIntArray(ExifDirectory.TAG_YCBCR_SUBSAMPLING);
        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)";
        }
    }

    public String getPlanarConfigurationDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_PLANAR_CONFIGURATION)) return null;
        // 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.

        switch (_directory.getInt(ExifDirectory.TAG_PLANAR_CONFIGURATION)) {
            case 1: return "Chunky (contiguous for each subsampling pixel)";
            case 2: return "Separate (Y-plane/Cb-plane/Cr-plane format)";
            default:
                return "Unknown configuration";
        }
    }

    public String getSamplesPerPixelDescription()
    {
        if (!_directory.containsTag(ExifDirectory.TAG_SAMPLES_PER_PIXEL)) return null;
        return _directory.getString(ExifDirectory.TAG_SAMPLES_PER_PIXEL) + " samples/pixel";
    }

    public String getRowsPerStripDescription()
    {
        if (!_directory.containsTag(ExifDirectory.TAG_ROWS_PER_STRIP)) return null;
        return _directory.getString(ExifDirectory.TAG_ROWS_PER_STRIP) + " rows/strip";
    }

    public String getStripByteCountsDescription()
    {
        if (!_directory.containsTag(ExifDirectory.TAG_STRIP_BYTE_COUNTS)) return null;
        return _directory.getString(ExifDirectory.TAG_STRIP_BYTE_COUNTS) + " bytes";
    }

    public String getPhotometricInterpretationDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_PHOTOMETRIC_INTERPRETATION)) return null;
        // Shows the color space of the image data components
        switch (_directory.getInt(ExifDirectory.TAG_PHOTOMETRIC_INTERPRETATION)) {
            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";
        }
    }

    public String getCompressionDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_COMPRESSION)) return null;
        switch (_directory.getInt(ExifDirectory.TAG_COMPRESSION)) {
            case 1: return "Uncompressed";
            case 2: return "CCITT 1D";
            case 3: return "T4/Group 3 Fax";
            case 4: return "T6/Group 4 Fax";
            case 5: return "LZW";
            case 6: return "JPEG (old-style)";
            case 7: return "JPEG";
            case 8: return "Adobe Deflate";
            case 9: return "JBIG B&W";
            case 10: return "JBIG Color";
            case 32766: return "Next";
            case 32771: return "CCIRLEW";
            case 32773: return "PackBits";
            case 32809: return "Thunderscan";
            case 32895: return "IT8CTPAD";
            case 32896: return "IT8LW";
            case 32897: return "IT8MP";
            case 32898: return "IT8BL";
            case 32908: return "PixarFilm";
            case 32909: return "PixarLog";
            case 32946: return "Deflate";
            case 32947: return "DCS";
            case 32661: return "JBIG";
            case 32676: return "SGILog";
            case 32677: return "SGILog24";
            case 32712: return "JPEG 2000";
            case 32713: return "Nikon NEF Compressed";
            default:
                return "Unknown compression";
        }
    }

    public String getBitsPerSampleDescription()
    {
        if (!_directory.containsTag(ExifDirectory.TAG_BITS_PER_SAMPLE)) return null;
        return _directory.getString(ExifDirectory.TAG_BITS_PER_SAMPLE) + " bits/component/pixel";
    }

    public String getThumbnailImageWidthDescription()
    {
        if (!_directory.containsTag(ExifDirectory.TAG_THUMBNAIL_IMAGE_WIDTH)) return null;
        return _directory.getString(ExifDirectory.TAG_THUMBNAIL_IMAGE_WIDTH) + " pixels";
    }

    public String getThumbnailImageHeightDescription()
    {
        if (!_directory.containsTag(ExifDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT)) return null;
        return _directory.getString(ExifDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT) + " pixels";
    }

    public String getFocalPlaneXResolutionDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_FOCAL_PLANE_X_RES)) return null;
        Rational rational = _directory.getRational(ExifDirectory.TAG_FOCAL_PLANE_X_RES);
        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) + " " +
                getFocalPlaneResolutionUnitDescription().toLowerCase();
    }

    public String getFocalPlaneYResolutionDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_FOCAL_PLANE_Y_RES)) return null;
        Rational rational = _directory.getRational(ExifDirectory.TAG_FOCAL_PLANE_Y_RES);
        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) + " " +
                getFocalPlaneResolutionUnitDescription().toLowerCase();
    }

    public String getFocalPlaneResolutionUnitDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_FOCAL_PLANE_UNIT)) return null;
        // Unit of FocalPlaneXResoluton/FocalPlaneYResolution. '1' means no-unit,
        // '2' inch, '3' centimeter.
        switch (_directory.getInt(ExifDirectory.TAG_FOCAL_PLANE_UNIT)) {
            case 1:
                return "(No unit)";
            case 2:
                return "Inches";
            case 3:
                return "cm";
            default:
                return "";
        }
    }

    public String getExifImageWidthDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_EXIF_IMAGE_WIDTH)) return null;
        return _directory.getInt(ExifDirectory.TAG_EXIF_IMAGE_WIDTH) + " pixels";
    }

    public String getExifImageHeightDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_EXIF_IMAGE_HEIGHT)) return null;
        return _directory.getInt(ExifDirectory.TAG_EXIF_IMAGE_HEIGHT) + " pixels";
    }

    public String getColorSpaceDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_COLOR_SPACE)) return null;
        int colorSpace = _directory.getInt(ExifDirectory.TAG_COLOR_SPACE);
        if (colorSpace == 1) {
            return "sRGB";
        } else if (colorSpace == 65535) {
            return "Undefined";
        } else {
            return "Unknown";
        }
    }

    public String getFocalLengthDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_FOCAL_LENGTH)) return null;
        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
        Rational focalLength = _directory.getRational(ExifDirectory.TAG_FOCAL_LENGTH);
        return formatter.format(focalLength.doubleValue()) + " mm";
    }

    public String getFlashDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_FLASH)) return null;

        /*
         * 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
         */

        int val = _directory.getInt(ExifDirectory.TAG_FLASH);

        StringBuffer sb = new StringBuffer();

        if ((val & 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 ((val & 0x4)!=0)
        {
            if ((val & 0x2)!=0)
                sb.append(", return detected");
            else
                sb.append(", return not detected");
        }

        if ((val & 0x10)!=0)
            sb.append(", auto");

        if ((val & 0x40)!=0)
            sb.append(", red-eye reduction");

        return sb.toString();
    }

    public String getWhiteBalanceDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_WHITE_BALANCE)) return null;
        // '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.
        switch (_directory.getInt(ExifDirectory.TAG_WHITE_BALANCE)) {
            case 0:
                return "Unknown";
            case 1:
                return "Daylight";
            case 2:
                return "Flourescent";
            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 (" + _directory.getInt(ExifDirectory.TAG_WHITE_BALANCE) + ")";
        }
    }

    public String getMeteringModeDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_METERING_MODE)) return null;
        // '0' means unknown, '1' average, '2' center weighted average, '3' spot
        // '4' multi-spot, '5' multi-segment, '6' partial, '255' other
        int meteringMode = _directory.getInt(ExifDirectory.TAG_METERING_MODE);
        switch (meteringMode) {
            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 "";
        }
    }

    public String getSubjectDistanceDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_SUBJECT_DISTANCE)) return null;
        Rational distance = _directory.getRational(ExifDirectory.TAG_SUBJECT_DISTANCE);
        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
        return formatter.format(distance.doubleValue()) + " metres";
    }

    public String getCompressionLevelDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_COMPRESSION_LEVEL)) return null;
        Rational compressionRatio = _directory.getRational(ExifDirectory.TAG_COMPRESSION_LEVEL);
        String ratio = compressionRatio.toSimpleString(_allowDecimalRepresentationOfRationals);
        if (compressionRatio.isInteger() && compressionRatio.intValue() == 1) {
            return ratio + " bit/pixel";
        } else {
            return ratio + " bits/pixel";
        }
    }

    public String getThumbnailLengthDescription()
    {
        if (!_directory.containsTag(ExifDirectory.TAG_THUMBNAIL_LENGTH)) return null;
        return _directory.getString(ExifDirectory.TAG_THUMBNAIL_LENGTH) + " bytes";
    }

    public String getThumbnailOffsetDescription()
    {
        if (!_directory.containsTag(ExifDirectory.TAG_THUMBNAIL_OFFSET)) return null;
        return _directory.getString(ExifDirectory.TAG_THUMBNAIL_OFFSET) + " bytes";
    }

    public String getYResolutionDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_Y_RESOLUTION)) return null;
        Rational resolution = _directory.getRational(ExifDirectory.TAG_Y_RESOLUTION);
        return resolution.toSimpleString(_allowDecimalRepresentationOfRationals) +
                " dots per " +
                getResolutionDescription().toLowerCase();
    }

    public String getXResolutionDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_X_RESOLUTION)) return null;
        Rational resolution = _directory.getRational(ExifDirectory.TAG_X_RESOLUTION);
        return resolution.toSimpleString(_allowDecimalRepresentationOfRationals) +
                " dots per " +
                getResolutionDescription().toLowerCase();
    }

    public String getExposureTimeDescription()
    {
        if (!_directory.containsTag(ExifDirectory.TAG_EXPOSURE_TIME)) return null;
        return _directory.getString(ExifDirectory.TAG_EXPOSURE_TIME) + " sec";
    }

    public String getShutterSpeedDescription() throws MetadataException
    {
        // 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).

        if (!_directory.containsTag(ExifDirectory.TAG_SHUTTER_SPEED)) return null;
//        float apexValue = _directory.getFloat(ExifDirectory.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.getFloat(ExifDirectory.TAG_SHUTTER_SPEED);
        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(ExifDirectory.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();
*/

    }

    public String getFNumberDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_FNUMBER)) return null;
        Rational fNumber = _directory.getRational(ExifDirectory.TAG_FNUMBER);
        return "F" + SimpleDecimalFormatter.format(fNumber.doubleValue());
    }

    public String getYCbCrPositioningDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_YCBCR_POSITIONING)) return null;
        int yCbCrPosition = _directory.getInt(ExifDirectory.TAG_YCBCR_POSITIONING);
        switch (yCbCrPosition) {
            case 1: return "Center of pixel array";
            case 2: return "Datum point";
            default:
                return String.valueOf(yCbCrPosition);
        }
    }

    public String getOrientationDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_ORIENTATION)) return null;
        int orientation = _directory.getInt(ExifDirectory.TAG_ORIENTATION);
        switch (orientation) {
            case 1: return "Top, left side (Horizontal / normal)";
            case 2: return "Top, right side (Mirror horizontal)";
            case 3: return "Bottom, right side (Rotate 180)";
            case 4: return "Bottom, left side (Mirror vertical)";
            case 5: return "Left side, top (Mirror horizontal and rotate 270 CW)";
            case 6: return "Right side, top (Rotate 90 CW)";
            case 7: return "Right side, bottom (Mirror horizontal and rotate 90 CW)";
            case 8: return "Left side, bottom (Rotate 270 CW)";
            default:
                return String.valueOf(orientation);
        }
    }

    public String getResolutionDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_RESOLUTION_UNIT)) return "";
        // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
        int resolutionUnit = _directory.getInt(ExifDirectory.TAG_RESOLUTION_UNIT);
        switch (resolutionUnit) {
            case 1: return "(No unit)";
            case 2: return "Inch";
            case 3: return "cm";
            default:
                return "";
        }
    }

    public String getSensingMethodDescription() throws MetadataException
    {
        if (!_directory.containsTag(ExifDirectory.TAG_SENSING_METHOD)) return null;
        // '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
        int sensingMethod = _directory.getInt(ExifDirectory.TAG_SENSING_METHOD);
        switch (sensingMethod) {
            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 "";
        }
    }

    public String getComponentConfigurationDescription() throws MetadataException
    {
        int[] components = _directory.getIntArray(ExifDirectory.TAG_COMPONENTS_CONFIGURATION);
        String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"};
        StringBuffer componentConfig = new StringBuffer();
        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();
    }

    /**
     * Takes a series of 4 bytes from the specified offset, and converts these to a
     * well-known version number, where possible.  For example, (hex) 30 32 31 30 == 2.10).
     * @param components the four version values
     * @return the version as a string of form 2.10
     */
    public static String convertBytesToVersionString(int[] components)
    {
        StringBuffer version = new StringBuffer();
        for (int i = 0; i < 4 && i < components.length; i++) {
            if (i == 2) version.append('.');
            String digit = String.valueOf((char)components[i]);
            if (i == 0 && "0".equals(digit)) continue;
            version.append(digit);
        }
        return version.toString();
    }

    /**
     * The Windows specific tags uses plain Unicode
     */
    private String getUnicodeDescription(int tag) throws MetadataException
    {
         if (!_directory.containsTag(tag)) return null;
         byte[] commentBytes = _directory.getByteArray(tag);
         try {
             // decode the unicode string
             // trim it, as i'm seeing a junk character on the end
            return new String(commentBytes, "UTF-16LE").trim();
         }
         catch (UnsupportedEncodingException ex) {
            return null;
         }
    }

    public String getWindowsAuthorDescription() throws MetadataException
    {
       return getUnicodeDescription(ExifDirectory.TAG_WIN_AUTHOR);
    }

    public String getWindowsCommentDescription() throws MetadataException
    {
       return getUnicodeDescription(ExifDirectory.TAG_WIN_COMMENT);
    }

    public String getWindowsKeywordsDescription() throws MetadataException
    {
       return getUnicodeDescription(ExifDirectory.TAG_WIN_KEYWORDS);
    }

    public String getWindowsTitleDescription() throws MetadataException
    {
       return getUnicodeDescription(ExifDirectory.TAG_WIN_TITLE);
    }

    public String getWindowsSubjectDescription() throws MetadataException
    {
       return getUnicodeDescription(ExifDirectory.TAG_WIN_SUBJECT);
    }
}
