it.geosolutions.imageio.plugins.exif.EXIFUtilities.java Source code

Java tutorial

Introduction

Here is the source code for it.geosolutions.imageio.plugins.exif.EXIFUtilities.java

Source

/*
 *    ImageI/O-Ext - OpenSource Java Image translation Library
 *    http://www.geo-solutions.it/
 *    http://java.net/projects/imageio-ext/
 *    (C) 2007 - 2011, GeoSolutions
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    either version 3 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package it.geosolutions.imageio.plugins.exif;

import it.geosolutions.imageio.plugins.exif.EXIFTags.Type;
import it.geosolutions.imageio.stream.input.FileImageInputStreamExt;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.commons.io.FileUtils;

import com.sun.media.imageio.plugins.tiff.BaselineTIFFTagSet;
import com.sun.media.imageio.plugins.tiff.EXIFParentTIFFTagSet;
import com.sun.media.imageio.plugins.tiff.EXIFTIFFTagSet;
import com.sun.media.imageio.plugins.tiff.TIFFTag;

/**
 * @author Daniele Romagnoli, GeoSolutions SAS
 * 
 * Utility class providing methods to setup/parse EXIF, write it to stream, retrieve from stream. 
 */
public class EXIFUtilities {

    /** @deprecated use {@link EXIFTags#COPYRIGHT} */
    public static final int TAG_COPYRIGHT = BaselineTIFFTagSet.TAG_COPYRIGHT;

    /** @deprecated use {@link EXIFTags#EXIF_IFD_POINTER} */
    public static final int TAG_EXIF_IFD_POINTER = EXIFParentTIFFTagSet.TAG_EXIF_IFD_POINTER;

    /** @deprecated use {@link EXIFTags#USER_COMMENT} */
    public static final int TAG_USER_COMMENT = EXIFTIFFTagSet.TAG_USER_COMMENT;

    /** @deprecated use {@link EXIFTags#Type} */
    public enum EXIFTagType {
        BASELINE, EXIF
        //TODO more may be added in the future, like GPS, ...
    }

    /**
     * Simple dummy class to wrap an EXIFMetadata instance as well as the length
     * of the APP1 marker. 
     */
    static class EXIFMetadataWrapper {
        public EXIFMetadata getExif() {
            return exif;
        }

        public void setExif(EXIFMetadata exif) {
            this.exif = exif;
        }

        public int getLength() {
            return length;
        }

        public void setLength(int length) {
            this.length = length;
        }

        /**
         * @param exif
         * @param length
         */
        public EXIFMetadataWrapper(EXIFMetadata exif, int length) {
            super();
            this.exif = exif;
            this.length = length;
        }

        EXIFMetadata exif;

        int length;
    }

    /** Utility buffer size */
    final static int DEFAULT_BUFFER_SIZE = 4096;

    final static int EXIF_SCAN_BUFFER_SIZE = 32768;

    final static byte _0 = 0x00;

    final static byte FF = (byte) 0xFF;

    /** EXIF Marker identifier */
    final static byte[] EXIF_MARKER = new byte[] { 'E', 'x', 'i', 'f', _0, _0 };

    /** Offset to be appended before starting IFD1 content */
    final static byte[] NEXT_IFD = new byte[] { _0, _0, _0, _0 };

    /** BIG Endian TIFF HEADER */
    final static byte[] TIFF_HEADER = new byte[] { 'M', 'M', _0, 0x2A, _0, _0, _0, 8 };

    final static int TIFF_HEADER_LENGTH = TIFF_HEADER.length;

    /** 
     * UserComment ASCII character code prefix to be inserted before any UserComment String when writing it 
     * into the EXIF marker
     */
    final static byte[] USER_COMMENT_ASCII_CHAR_CODE = new byte[] { 0x41, 0x53, 0x43, 0x49, 0x49, _0, _0, _0 };

    /** The APP1 Marker bytes, (APP1 is the one containing EXIF) */
    final static byte[] APP1_MARKER = new byte[] { FF, (byte) 0xE1 };

    /** 
     * The Data Quantization Table Marker. 
     * It is contained within a JPEG encoded data stream before the data image.
     * EXIF marker should be put before this marker
     */
    final static byte[] DQT_MARKER = new byte[] { FF, (byte) 0xDB };

    /** The specification requires 2 bytes to identify the number of tags */
    final static int BYTES_FOR_TAGS_NUMBER = 2;

    /** A byte array representing NULL String terminator, to be appended after any ASCII byte array */
    final static byte[] NULL_STRING = new byte[] { _0 };

    /** 
     * The fixed length of each IFD, 
     * made of Number (2 bytes) + Type (2 bytes) + Count (4 bytes) + Value/Offset (4 Bytes)
     */
    final static int IFD_LENGTH = 12;

    /**
     * This method will update the image referred by the specified inputStream, by replacing
     * the underlying EXIF with the one represented by the specified {@link EXIFMetadata} instance.
     * Write the result to the specified {@link OutputStream}.
     * 
     * @param outputStream the stream where to write the result
     * @param inputStream the input stream referring to the original image to be copied back to the
     *   output
     * @param exif the {@link EXIFMetadata} instance containing updated exif to replace the one
     *   contained into the input image. 
     * @param previousEXIFLength 
     *   the length of the previous EXIF marker. 
     *   It is needed in order to understand the portion of the input image to be copied back to 
     *   the output
     * @throws IOException
     */
    private static void updateStream(final OutputStream outputStream, final FileImageInputStreamExt inputStream,
            final EXIFMetadata exif, final int previousEXIFLength) throws IOException {
        ByteArrayOutputStream baos = null;
        BufferedOutputStream bos = null;
        try {

            // Setup a new byteArrayOutputStream on top of the Exif object 
            baos = initializeExifStream(exif, null);

            // Update this outputStream by copying bytes from the original image 
            // referred by the inputStream, but inserting updated EXIF 
            // instead of copying it from the original image
            bos = new BufferedOutputStream(outputStream, DEFAULT_BUFFER_SIZE);
            updateFromStream(bos, baos, inputStream, previousEXIFLength);
        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (Throwable t) {
                    // Eat exception on close
                }
            }
            if (baos != null) {
                try {
                    baos.close();
                } catch (Throwable t) {
                    // Eat exception on close
                }
            }
        }

    }

    /**
     * This method allows to parse the provided {@link EXIFMetadata} object and put it into 
     * the specified outputStream while copying back the JPEG encoded image referred by
     * the imageData argument.
     * 
     * @param outputStream the stream where to write
     * @param imageData the bytes containing JPEG encoded image data 
     * @param imageDataSize the number of bytes to be used from the data array
     * @param exif the {@link EXIFMetadata} object holding EXIF.
     * @throws IOException
     */
    public static void insertEXIFintoStream(final OutputStream outputStream, final byte[] imageData,
            final int imageDataSize, final EXIFMetadata exif) throws IOException {
        ByteArrayOutputStream baos = null;
        if (outputStream instanceof ByteArrayOutputStream) {
            baos = (ByteArrayOutputStream) outputStream;
            writeToByteStream(baos, imageData, imageDataSize, exif);
        } else {
            writeBuffered(outputStream, imageData, imageDataSize, exif);
        }

    }

    /**
     * This method write the provided {@link EXIFMetadata} into the specified outputStream 
     * while copying back the JPEG encoded image referred by the imageData argument.
     * 
     * @param outputStream the stream where to write
     * @param imageData the bytes containing JPEG encoded image data 
     * @param imageDataSize the number of bytes to be used from the data array
     * @param exif the {@link EXIFMetadata} object holding EXIF.
     * @throws IOException
     */
    private static void writeBuffered(final OutputStream outputStream, final byte[] imageData,
            final int imageDataSize, final EXIFMetadata exif) throws IOException {
        ByteArrayOutputStream baos = null;
        try {
            baos = initializeExifStream(exif, null);
            updateFromBytes(outputStream, baos, imageData, imageDataSize);
        } finally {
            if (baos != null) {
                try {
                    baos.close();
                } catch (Throwable t) {
                    // Eat exception on close
                }
            }
        }
    }

    /**
     * Write the specified {@link EXIFMetadata} object as well as the specified image data bytes to
     * the specified outputStream. It will take care of inserting the EXIF marker in the proper 
     * location within the outputStream when writing image data bytes.  
     * 
     * @param outputStream the outputStream where to write
     * @param imageData the bytes containing JPEG encoded image data 
     * @param imageDataSize the number of bytes to be used from the data array
     * @param exif the {@link EXIFMetadata} object holding EXIF.
     * @throws IOException
     */
    private static void writeToByteStream(ByteArrayOutputStream outputStream, final byte[] imageData,
            final int imageDataSize, final EXIFMetadata exif) throws IOException {

        // locate the DQT marker in the input imageData bytes
        final int dqtMarkerPos = locateFirst(imageData, DQT_MARKER);
        if (dqtMarkerPos != -1) {

            // write to stream the initial part of imageData before appending EXIF marker
            // at the proper position
            outputStream.write(imageData, 0, dqtMarkerPos);
            outputStream.flush();

            // Append the EXIF content
            outputStream = initializeExifStream(exif, outputStream);
            outputStream.write(_0);

            // Proceed with writing the remaining part of image data bytes.
            outputStream.write(imageData, dqtMarkerPos, imageDataSize - dqtMarkerPos);
        }
    }

    /**
     * Initialize a ByteArrayOutputStream on top of an {@link EXIFMetadata} entity.
     * 
     * @param exif an {@link EXIFMetadata} instance representing EXIF tags to be put to the stream
     * @param outputStream an optional {@link ByteArrayOutputStream} where to write the exif marker.
     * If null, a new {@link ByteArrayOutputStream} will be created and returned
     * 
     * @return the {@link ByteArrayOutputStream} containing the written EXIF bytes.
     * @throws IOException
     */
    private static ByteArrayOutputStream initializeExifStream(final EXIFMetadata exif,
            final ByteArrayOutputStream outputStream) throws IOException {

        // Preliminar check. Write on: the provided ByteArrayOutputStream VS a newly created one
        final ByteArrayOutputStream baos = outputStream == null ? new ByteArrayOutputStream() : outputStream;

        // Get exif tags from the specified EXIF object.
        List<TIFFTagWrapper> baselineTags = exif.getList(Type.BASELINE);
        List<TIFFTagWrapper> exifTags = exif.getList(Type.EXIF);
        final int baseLength = TIFF_HEADER_LENGTH + BYTES_FOR_TAGS_NUMBER + NEXT_IFD.length;

        // Initialize number of fields and tags
        final int numBaselineTags = baselineTags.size();
        final int numExifTags = exifTags.size();
        final byte[] numFieldsB = intToBytes(numBaselineTags);
        final byte[] numSpecificFieldsB = intToBytes(numExifTags);

        // Initialize tags sizes and offsets
        final int baseLineTagsOffsets[] = new int[numBaselineTags];
        final int baseLineTagsContentSizes[] = new int[numBaselineTags];
        computeOffsetsAndSizes(baselineTags, baseLength, baseLineTagsOffsets, baseLineTagsContentSizes);
        final int baselineContentLength = sum(baseLineTagsContentSizes);

        final int exifTagsOffsets[] = new int[numExifTags];
        final int exifTagsContentSizes[] = new int[numExifTags];
        computeOffsetsAndSizes(exifTags, baseLineTagsOffsets[numBaselineTags - 1] + BYTES_FOR_TAGS_NUMBER,
                exifTagsOffsets, exifTagsContentSizes);
        final int exifTagsContentLength = sum(exifTagsContentSizes);

        // Compute total marker length (which is the first entry to be written out after the marker)
        int app1Lenght = APP1_MARKER.length - 1 // -1 due to the 0xFF part
                + EXIF_MARKER.length + baseLength + BYTES_FOR_TAGS_NUMBER // Num Fields (2 bytes)
                + BYTES_FOR_TAGS_NUMBER // Num EXIF Fields (2 bytes)
                + numBaselineTags * IFD_LENGTH + numExifTags * IFD_LENGTH // Bytes needed to represent all IFD
                + baselineContentLength + exifTagsContentLength; // Bytes used for tags contents

        // Write headers
        baos.write(APP1_MARKER);
        baos.write(intToBytes(app1Lenght));
        baos.write(EXIF_MARKER);
        baos.write(TIFF_HEADER);
        baos.write(numFieldsB);

        // Write BaseLine IFDs and their content
        writeIFDs(baos, baselineTags);
        baos.write(NEXT_IFD);
        writeTagsContent(baos, baselineTags);
        baos.write(numSpecificFieldsB);

        // Write EXIF Specific IFDs and their content
        writeIFDs(baos, exifTags);
        writeTagsContent(baos, exifTags);
        baos.flush();
        return baos;
    }

    /**
     * Update the EXIF content referred by a {@link FileImageInputStreamExt} using the 
     * content available in the {@link ByteArrayOutputStream}. Store the result
     * to the specified {@link OutputStream}.
     * 
     * @param outputStream a {@link OutputStream} where to write
     * @param byteStream a {@link ByteArrayOutputStream} previously populated with the content
     *   of an EXIF marker.   
     * @param inputStream a {@link FileImageInputStreamExt} referring to a file containing 
     *   EXIF metadata to be updated
     * @param originalAPP1MarkerLength is the length of the original APP1 marker 
     *          (the one containing EXIF content), before the update
     * @throws IOException
     */
    private static void updateFromStream(final OutputStream outputStream, final ByteArrayOutputStream byteStream,
            final FileImageInputStreamExt inputStream, final int originalAPP1MarkerLength) throws IOException {
        final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int readlength = 0;
        boolean replacedExif = false;

        // Read from the input stream  
        while ((readlength = inputStream.read(buffer)) != -1) {

            //Look for the EXIF marker 
            int app1MarkerPos = replacedExif ? -1 : locateFirst(buffer, APP1_MARKER);

            if (app1MarkerPos != -1) {
                replacedExif = true;

                // Copy back all the previous part before putting the updated EXIF
                outputStream.write(buffer, 0, app1MarkerPos);
                outputStream.write(byteStream.toByteArray());
                outputStream.write(_0);

                // Copy back the remaining part of the image by skipping the original EXIF marker
                // TODO: make sure we still work within the buffer boundaries 
                outputStream.write(buffer, app1MarkerPos + originalAPP1MarkerLength + 2,
                        readlength - (app1MarkerPos + originalAPP1MarkerLength + 2));
            } else {
                outputStream.write(buffer, 0, readlength);
            }
        }
    }

    /**
     * Update the EXIF content of the image stored in the specified byte array, using the 
     * content available in the input {@link ByteArrayOutputStream}. Store the result
     * to the specified {@link OutputStream}
     * 
     * @param outputStream The stream where to write
     * @param byteStream the byte stream containing exif metadata to be written
     * @param imageData the original image bytes
     * @param imageDataSize the portion of the image bytes array to be used
     * @throws IOException
     */
    private static void updateFromBytes(final OutputStream outputStream, final ByteArrayOutputStream byteStream,
            final byte[] imageData, final int imageDataSize) throws IOException {
        int dqtMarkerPos = locateFirst(imageData, DQT_MARKER);

        // Look for the DQT marker
        if (dqtMarkerPos != -1) {

            // copy back the first part of the image bytes
            outputStream.write(imageData, 0, dqtMarkerPos);

            // insert the EXIF content
            outputStream.write(byteStream.toByteArray());
            outputStream.write(_0);

            // continue copying back the remaining part of data
            outputStream.write(imageData, dqtMarkerPos, imageDataSize - dqtMarkerPos);
            outputStream.flush();
        }
    }

    /**
     * Write the IFDs referred by the tags list argument, to the specified stream.
     * No content referred by a offset, is written.  
     * 
     * @param stream the {@link ByteArrayOutputStream} where to append the IFDs.
     * @param tags the IFDs to be written. In order to respect the TIFF specification, make
     * sure the element of this list are sorted in ascending order.
     * @throws IOException
     */
    private static void writeIFDs(final ByteArrayOutputStream stream, final List<TIFFTagWrapper> tags)
            throws IOException {
        if (stream == null) {
            throw new IllegalArgumentException("Null stream has been provided");
        }
        for (TIFFTagWrapper tag : tags) {
            stream.write(tagAsBytes(tag, true));
        }

    }

    /**
     * Write the content of the IFDs referred by the tags list argument, to the specified stream.
     * Note that only the content of the IFDs having an offset will be written.
     * 
     * @param stream the {@link ByteArrayOutputStream} where to append the IFDs content.
     * @param tags the Tags list to be written. In order to respect the TIFF specification, make
     * sure the element of this list are sorted in ascending order.
     * @throws IOException
     */
    private static void writeTagsContent(final ByteArrayOutputStream stream, final List<TIFFTagWrapper> tags)
            throws IOException {

        // Scan tags looking for entries which has a not null content to be written
        for (TIFFTagWrapper tag : tags) {
            if (tag.getContent() != null) {

                //Make sure to write prefix and suffix bytes if present
                if (tag.getPrefix() != null) {
                    stream.write(tag.getPrefix());
                }
                stream.write((byte[]) tag.getContent());
                if (tag.getSuffix() != null) {
                    stream.write(tag.getSuffix());
                }
            }
        }
    }

    /**
     * Simply computes the sum of the value provided in the array.
     * @param values
     * @return
     */
    final static int sum(final int values[]) {
        int sum = 0;
        for (int s : values) {
            sum += s;
        }
        return sum;
    }

    /**
     * Scan the IFDs contained in the input tags collection.
     * Store the offsetValue and size of each entry in the proper array.
     * 
     * @param tags the tags to be scan
     * @param baseOffset the baseOffset to be taken into account when computing 
     *          the tags offsets
     * @param valueOffsets the array where to put found valueOffsets
     * @param sizes the array where to put found sizes
     */
    private static void computeOffsetsAndSizes(final List<TIFFTagWrapper> tags, final int baseOffset,
            final int[] valueOffsets, final int[] sizes) {
        final int elementsSize = tags.size();
        int i = 0;
        int previousOffset = 0;

        // Scan the tags list
        for (TIFFTagWrapper tag : tags) {

            // Offsets refer to position in the stream after the IFD, where the content will be written
            valueOffsets[i] = baseOffset + elementsSize * IFD_LENGTH + previousOffset;
            if (tag.getNumber() == EXIFParentTIFFTagSet.TAG_EXIF_IFD_POINTER) {
                // Special case, being a pointer, make sure to set the offset as value,
                // without byte counts increment
                tag.setValue(valueOffsets[i]);
            } else if (tag.getContent() != null) {
                tag.setValue(valueOffsets[i]);
                previousOffset += tag.getCount();
                sizes[i] = tag.getCount();
            } else {
                sizes[i] = 0;
            }
            i++;
        }
    }

    /**
     * Simple utility method returning a 2 bytes representation of a value, in compliance with the
     * specified endianness 
     * 
     * @param value the value to be represented through 2 bytes
     * @param isBigEndian {@code true} in case of bigEndian
     * @return
     */
    public final static byte[] intToBytes(final int value, final boolean isBigEndian) {
        if (isBigEndian) {
            return new byte[] { (byte) ((value >> 8) & 0xFF), (byte) (value & 0xFF) };
        } else {
            return new byte[] { (byte) (value & 0xFF), (byte) ((value >> 8) & 0xFF) };
        }
    }

    /**
     * Simple utility method returning a 2 bytes representation of a value as bigEndian
     * 
     * @param value the value to be represented through 2 bytes
     * @return
     */
    public final static byte[] intToBytes(final int value) {
        return intToBytes(value, true);
    }

    /**
     * Simple utility methods returning an int built on top of 2 bytes, in compliance
     * with the specified endianness
     * @param buff the buffer containing 2 bytes to be transformed
     * @param start the position within the buffer of the first byte to be transformed 
     * @param isBigEndian {@code true} in case we need to encode it as bigEndian
     * @return
     */
    public final static int bytes2ToInt(byte[] buff, int start, final boolean isBigEndian) {
        int intValue = 0;
        intValue |= buff[start + (isBigEndian ? 0 : 1)] & 0xFF;
        intValue <<= 8;
        intValue |= buff[start + (isBigEndian ? 1 : 0)] & 0xFF;
        return intValue;
    }

    /**
     * Simple utility methods returning an int built on top of 4 bytes, in compliance
     * with the specified endianness
     * @param buff the buffer containing 4 bytes to be transformed
     * @param start the position within the buffer of the first byte to be transformed 
     * @param isBigEndian {@code true} in case we need to encode it as bigEndian
     * @return
     */
    public final static int bytes4ToInt(byte[] buff, int start, final boolean isBigEndian) {
        int intValue = 0;
        intValue |= buff[start + (isBigEndian ? 0 : 3)] & 0xFF;
        intValue <<= 8;
        intValue |= buff[start + (isBigEndian ? 1 : 2)] & 0xFF;
        intValue <<= 8;
        intValue |= buff[start + (isBigEndian ? 2 : 1)] & 0xFF;
        intValue <<= 8;
        intValue |= buff[start + (isBigEndian ? 3 : 0)] & 0xFF;
        return intValue;
    }

    /**
     * Return the specified {@link TIFFTagWrapper} as a byte array, by taking endianness into
     * account
     * 
     * @param tag
     * @param isBigEndian
     * @return
     */
    private static byte[] tagAsBytes(final TIFFTagWrapper tag, final boolean isBigEndian) {
        final byte[] output = new byte[IFD_LENGTH];
        final int number = tag.getNumber();
        final int type = tag.getType();
        final int count = tag.getCount();
        final int offset = tag.getValue();
        output[isBigEndian ? 1 : 0] = (byte) (number & 0xFF);
        output[isBigEndian ? 0 : 1] = (byte) ((number >> 8) & 0xFF);
        output[isBigEndian ? 3 : 2] = (byte) (type & 0xFF);
        output[isBigEndian ? 2 : 3] = (byte) ((type >> 8) & 0xFF);
        output[isBigEndian ? 7 : 4] = (byte) (count & 0xFF);
        output[isBigEndian ? 6 : 5] = (byte) ((count >> 8) & 0xFF);
        output[isBigEndian ? 5 : 6] = (byte) ((count >> 16) & 0xFF);
        output[isBigEndian ? 4 : 7] = (byte) ((count >> 24) & 0xFF);
        output[isBigEndian ? 11 : 8] = (byte) (offset & 0xFF);
        output[isBigEndian ? 10 : 9] = (byte) ((offset >> 8) & 0xFF);
        output[isBigEndian ? 9 : 10] = (byte) ((offset >> 16) & 0xFF);
        output[isBigEndian ? 8 : 11] = (byte) ((offset >> 24) & 0xFF);
        return output;

    }

    /**
     * Scan the input byte buffer, looking for a candidate bytes sequence and return
     * the index of the first occurrence within the input buffer, or -1 in case nothing
     * is found.   
     */
    public static int locateFirst(final byte[] buffer, final byte[] candidate) {
        if (IsEmptyLocate(buffer, candidate)) {
            return -1;
        }
        for (int i = 0; i < buffer.length; i++) {
            if (!IsMatch(buffer, i, candidate)) {
                continue;
            }
            return i;
        }
        return -1;
    }

    /**
     * Scan the input buffer, starting from the specified position, and check whether it matches the
     * candidate byte sequence.
     * 
     * @param buffer the byte array to be scanned
     * @param start the starting index 
     * @param candidate the byte array to be matched
     * @return {@code true} in case the buffer match the candidate sequence
     */
    final static boolean IsMatch(final byte[] buffer, final int start, final byte[] candidate) {
        if (candidate.length > (buffer.length - start)) {
            return false;
        }

        for (int i = 0; i < candidate.length; i++) {
            if (buffer[start + i] != candidate[i]) {
                return false;
            }
        }

        return true;
    }

    /**
     * Simple utility method which check for conditions which won't allow to get a match
     * from the scan. As an instance, a null candidate byte array or a candidate array longer
     * than the array to be scan won't allow to get a match.  
     * @param toBeScan
     * @param candidate
     * @return {@code false} in case the needed conditions for matching aren't satisfied.
     */
    static boolean IsEmptyLocate(byte[] toBeScan, byte[] candidate) {
        return (toBeScan == null) || (candidate == null) || (toBeScan.length == 0) || (candidate.length == 0)
                || (candidate.length > toBeScan.length);
    }

    /** 
     * Replace the EXIF contained within a file referred by a {@link FileImageInputStreamExt} instance
     * with the EXIF represented by the specified {@link EXIFMetadata} instance. The original file
     * will be overwritten by the new one containing updated EXIF.
     * 
     * It is worth to point out that this replacing method won't currently perform any fields delete, 
     * but simply content update. Therefore, tags in the original EXIF which are missing in the 
     * updated EXIF parameter, won't be modified. 
     * 
     * @param inputStream a {@link FileImageInputStreamExt} referring to a JPEG containing EXIF 
     * @param exif the {@link EXIFMetadata} instance containing tags to be updated
     */
    public static void replaceEXIFs(final FileImageInputStreamExt inputStream, final EXIFMetadata exif)
            throws IOException {

        EXIFMetadataWrapper exifMarker = parseExifMetadata(inputStream, exif);
        EXIFMetadata updatedExif = exifMarker.getExif();

        final int app1Length = exifMarker.getLength();
        if (updatedExif != null) {
            // Create a temp file where to store the updated EXIF
            final File file = File.createTempFile("replacingExif", ".exif");
            final OutputStream fos = new FileOutputStream(file);
            updateStream(fos, inputStream, updatedExif, app1Length);
            final File previousFile = inputStream.getFile();
            FileUtils.deleteQuietly(previousFile);
            FileUtils.moveFile(file, previousFile);
        }
    }

    /**
     * Return an {@link EXIFMetadataWrapper} made of an {@link EXIFMetadata} setup on top of the 
     * EXIF found in the inputStream, merged with the EXIF found on the specified 
     * exif parameter (if any). The EXIF tags contained in the specified parameter will override
     * the ones found within the inputStream. 
     * The returned wrapper will also contain the length of the APP1 marker of the original EXIF.
     * 
     * @param inputStream a {@link FileImageInputStreamExt} referring to a JPEG containing EXIF 
     * @param exif the optional {@link EXIFMetadata} instance containing tags to be updated
     * @return a {@link EXIFMetadataWrapper} containing the merged/updated EXIF as well as the 
     * length of the original APP1 marker 
     * @throws IOException
     */
    private static EXIFMetadataWrapper parseExifMetadata(final FileImageInputStreamExt inputStream,
            final EXIFMetadata exif) throws IOException {

        List<TIFFTagWrapper> baselineTags = null;
        List<TIFFTagWrapper> exifTags = null;
        int app1Length = -1;
        if (exif != null) {
            // Get the updated Tags
            baselineTags = exif.getList(Type.BASELINE);
            exifTags = exif.getList(Type.EXIF);
        }

        inputStream.mark();
        final Map<Integer, TIFFTagWrapper> foundBaseLineTags = new TreeMap<Integer, TIFFTagWrapper>();
        final Map<Integer, TIFFTagWrapper> foundExifTags = new TreeMap<Integer, TIFFTagWrapper>();

        EXIFMetadata updatedExif = null;
        final byte[] buff = new byte[EXIF_SCAN_BUFFER_SIZE];
        boolean contains_EXIF_IFD = false;
        boolean found = false;

        // Scan the stream looking for exif tags
        while ((inputStream.read(buff)) != -1) {

            // Look for the EXIF section (referred by the APP1 marker)
            int exifTagPos = found ? -1 : locateFirst(buff, APP1_MARKER);
            int pos = 0;
            if (exifTagPos != -1) {
                found = true;
                pos = exifTagPos;

                // Get the original EXIF length
                app1Length = bytes2ToInt(buff, pos + APP1_MARKER.length, true);
                final boolean isBigEndian = buff[pos + APP1_MARKER.length + 2 + EXIF_MARKER.length] == 'M'
                        && buff[pos + APP1_MARKER.length + 2 + 1 + EXIF_MARKER.length] == 'M';

                // Initialize number of tags
                final int numBaselineTags = bytes2ToInt(buff,
                        pos + APP1_MARKER.length + 2 + EXIF_MARKER.length + TIFF_HEADER.length, isBigEndian);
                int skip = 0;
                int start = pos + APP1_MARKER.length + 2 + EXIF_MARKER.length;
                int globalContentLength = 0;

                for (int i = 0; i < numBaselineTags; i++) {
                    skip = start + TIFF_HEADER.length + 2 + i * IFD_LENGTH;

                    // Retrieve TIFF Tag information by scanning the buffer (Tag Number, type, count, offsetValue) 
                    int number = bytes2ToInt(buff, skip, isBigEndian);
                    int type = bytes2ToInt(buff, skip + 2, isBigEndian);
                    int count = bytes4ToInt(buff, skip + 4, isBigEndian);
                    int offsetValue = bytes4ToInt(buff, skip + 8, isBigEndian);
                    Object content = null;

                    // In case we find an IFDPointer during the scan, we need to take note of this
                    if (number == EXIFParentTIFFTagSet.TAG_EXIF_IFD_POINTER) {
                        contains_EXIF_IFD = true;
                    }
                    // Increase the globalContentLength when finding specific fields
                    if (type == TIFFTag.TIFF_ASCII || type == TIFFTag.TIFF_UNDEFINED) {
                        content = new byte[count];
                        System.arraycopy(buff, offsetValue + start, content, 0, count);
                        globalContentLength += count;
                    }

                    // Check whether the TAG found in the original EXIF is also contained
                    // in the EXIF to be updated
                    TIFFTagWrapper updatedTag = null;
                    for (TIFFTagWrapper tag : baselineTags) {
                        if (tag.getNumber() == number) {
                            updatedTag = tag;
                            break;
                        }
                    }

                    // Update the Map of merged EXIFs 
                    if (updatedTag == null) {

                        // In case the list of EXIF to be merged doesn't contain this tag id,
                        // take the one from the original EXIF to preserve its value
                        updatedTag = new TIFFTagWrapper(number, type, content, offsetValue, count, null, null);
                    }
                    foundBaseLineTags.put(number, updatedTag);
                }

                // Handle the EXIF Specific tags
                if (contains_EXIF_IFD) {
                    start = skip + globalContentLength + IFD_LENGTH + 4; // 4 due to 4 0x00 to start
                                                                         // the IFD values
                    int numExifFields = bytes2ToInt(buff, start, isBigEndian);

                    for (int i = 0; i < numExifFields; i++) {
                        skip = start + 2 + i * IFD_LENGTH;

                        // Retrieve TIFF Tag information by scanning the buffer (Tag Number, type, count, offsetValue)
                        int number = bytes2ToInt(buff, skip, isBigEndian);
                        int type = bytes2ToInt(buff, skip + 2, isBigEndian);
                        int count = bytes4ToInt(buff, skip + 4, isBigEndian);
                        int offsetValue = bytes4ToInt(buff, skip + 8, isBigEndian);
                        Object content = null;

                        // Increase the globalContentLength when finding specific fields
                        if (type == TIFFTag.TIFF_ASCII || type == TIFFTag.TIFF_UNDEFINED) {
                            content = new byte[count];
                            System.arraycopy(buff, offsetValue + start, content, 0, count);
                            globalContentLength += count;
                        }

                        // Check whether the TAG found in the original EXIF is also contained
                        // in the EXIF to be updated
                        TIFFTagWrapper updatedTag = null;
                        for (TIFFTagWrapper tag : exifTags) {
                            if (tag.getNumber() == number) {
                                updatedTag = tag;
                                break;
                            }
                        }

                        // Update the Map of merged EXIFs  
                        if (updatedTag == null) {

                            // In case the list of EXIF to be merged doesn't contain this tag id,
                            // take the one from the original EXIF to preserve its value
                            updatedTag = new TIFFTagWrapper(number, type, content, offsetValue, count, null, null);
                        }
                        foundExifTags.put(number, updatedTag);
                    }
                }

                List<TIFFTagWrapper> mergedBaselineTags = null;
                List<TIFFTagWrapper> mergedExifTags = null;
                if (!foundBaseLineTags.isEmpty()) {
                    mergedBaselineTags = new ArrayList<TIFFTagWrapper>(foundBaseLineTags.values());

                }
                if (!foundExifTags.isEmpty()) {
                    mergedExifTags = new ArrayList<TIFFTagWrapper>(foundExifTags.values());
                }

                // Setup a new EXIF Metadata object containing all the EXIFs to be put
                updatedExif = new EXIFMetadata(mergedBaselineTags, mergedExifTags);
            }
        }
        inputStream.reset();
        return new EXIFMetadataWrapper(updatedExif, app1Length);
    }

    /**
     * @param tagNumber
     * @return
     */
    public static TIFFTagWrapper createTag(int tagNumber) {
        switch (tagNumber) {
        case EXIFTags.USER_COMMENT:
            return new TIFFTagWrapper(EXIFTags.USER_COMMENT, TIFFTag.TIFF_UNDEFINED, null, -1, 0,
                    EXIFUtilities.USER_COMMENT_ASCII_CHAR_CODE, null);
        case EXIFTags.COPYRIGHT:
            return new TIFFTagWrapper(EXIFTags.COPYRIGHT, TIFFTag.TIFF_ASCII, null, 0, 0, null,
                    EXIFUtilities.NULL_STRING);
        case EXIFTags.EXIF_IFD_POINTER:
            return new TIFFTagWrapper(EXIFTags.EXIF_IFD_POINTER, TIFFTag.TIFF_LONG, null, -1, 1);
        }
        return null;
    }
}