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