/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.gallery3d.exif; import android.util.Log; import java.io.BufferedOutputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; /** * This class provides a way to replace the Exif header of a JPEG image. *

* Below is an example of writing EXIF data into a file * *

 * public static void writeExif(byte[] jpeg, ExifData exif, String path) {
 *     OutputStream os = null;
 *     try {
 *         os = new FileOutputStream(path);
 *         ExifOutputStream eos = new ExifOutputStream(os);
 *         // Set the exif header
 *         eos.setExifData(exif);
 *         // Write the original jpeg out, the header will be add into the file.
 *         eos.write(jpeg);
 *     } catch (FileNotFoundException e) {
 *         e.printStackTrace();
 *     } catch (IOException e) {
 *         e.printStackTrace();
 *     } finally {
 *         if (os != null) {
 *             try {
 *                 os.close();
 *             } catch (IOException e) {
 *                 e.printStackTrace();
 *             }
 *         }
 *     }
 * }
 * 
*/ class ExifOutputStream extends FilterOutputStream { private static final String TAG = "ExifOutputStream"; private static final boolean DEBUG = false; private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb private static final int STATE_SOI = 0; private static final int STATE_FRAME_HEADER = 1; private static final int STATE_JPEG_DATA = 2; private static final int EXIF_HEADER = 0x45786966; private static final short TIFF_HEADER = 0x002A; private static final short TIFF_BIG_ENDIAN = 0x4d4d; private static final short TIFF_LITTLE_ENDIAN = 0x4949; private static final short TAG_SIZE = 12; private static final short TIFF_HEADER_SIZE = 8; private static final int MAX_EXIF_SIZE = 65535; private ExifData mExifData; private int mState = STATE_SOI; private int mByteToSkip; private int mByteToCopy; private byte[] mSingleByteArray = new byte[1]; private ByteBuffer mBuffer = ByteBuffer.allocate(4); private final ExifInterface mInterface; protected ExifOutputStream(OutputStream ou, ExifInterface iRef) { super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE)); mInterface = iRef; } /** * Sets the ExifData to be written into the JPEG file. Should be called * before writing image data. */ protected void setExifData(ExifData exifData) { mExifData = exifData; } /** * Gets the Exif header to be written into the JPEF file. */ protected ExifData getExifData() { return mExifData; } private int requestByteToBuffer(int requestByteCount, byte[] buffer , int offset, int length) { int byteNeeded = requestByteCount - mBuffer.position(); int byteToRead = length > byteNeeded ? byteNeeded : length; mBuffer.put(buffer, offset, byteToRead); return byteToRead; } /** * Writes the image out. The input data should be a valid JPEG format. After * writing, it's Exif header will be replaced by the given header. */ @Override public void write(byte[] buffer, int offset, int length) throws IOException { while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA) && length > 0) { if (mByteToSkip > 0) { int byteToProcess = length > mByteToSkip ? mByteToSkip : length; length -= byteToProcess; mByteToSkip -= byteToProcess; offset += byteToProcess; } if (mByteToCopy > 0) { int byteToProcess = length > mByteToCopy ? mByteToCopy : length; out.write(buffer, offset, byteToProcess); length -= byteToProcess; mByteToCopy -= byteToProcess; offset += byteToProcess; } if (length == 0) { return; } switch (mState) { case STATE_SOI: int byteRead = requestByteToBuffer(2, buffer, offset, length); offset += byteRead; length -= byteRead; if (mBuffer.position() < 2) { return; } mBuffer.rewind(); if (mBuffer.getShort() != JpegHeader.SOI) { throw new IOException("Not a valid jpeg image, cannot write exif"); } out.write(mBuffer.array(), 0, 2); mState = STATE_FRAME_HEADER; mBuffer.rewind(); writeExifData(); break; case STATE_FRAME_HEADER: // We ignore the APP1 segment and copy all other segments // until SOF tag. byteRead = requestByteToBuffer(4, buffer, offset, length); offset += byteRead; length -= byteRead; // Check if this image data doesn't contain SOF. if (mBuffer.position() == 2) { short tag = mBuffer.getShort(); if (tag == JpegHeader.EOI) { out.write(mBuffer.array(), 0, 2); mBuffer.rewind(); } } if (mBuffer.position() < 4) { return; } mBuffer.rewind(); short marker = mBuffer.getShort(); if (marker == JpegHeader.APP1) { mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2; mState = STATE_JPEG_DATA; } else if (!JpegHeader.isSofMarker(marker)) { out.write(mBuffer.array(), 0, 4); mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2; } else { out.write(mBuffer.array(), 0, 4); mState = STATE_JPEG_DATA; } mBuffer.rewind(); } } if (length > 0) { out.write(buffer, offset, length); } } /** * Writes the one bytes out. The input data should be a valid JPEG format. * After writing, it's Exif header will be replaced by the given header. */ @Override public void write(int oneByte) throws IOException { mSingleByteArray[0] = (byte) (0xff & oneByte); write(mSingleByteArray); } /** * Equivalent to calling write(buffer, 0, buffer.length). */ @Override public void write(byte[] buffer) throws IOException { write(buffer, 0, buffer.length); } private void writeExifData() throws IOException { if (mExifData == null) { return; } if (DEBUG) { Log.v(TAG, "Writing exif data..."); } ArrayList nullTags = stripNullValueTags(mExifData); createRequiredIfdAndTag(); int exifSize = calculateAllOffset(); if (exifSize + 8 > MAX_EXIF_SIZE) { throw new IOException("Exif header is too large (>64Kb)"); } OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out); dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN); dataOutputStream.writeShort(JpegHeader.APP1); dataOutputStream.writeShort((short) (exifSize + 8)); dataOutputStream.writeInt(EXIF_HEADER); dataOutputStream.writeShort((short) 0x0000); if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) { dataOutputStream.writeShort(TIFF_BIG_ENDIAN); } else { dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN); } dataOutputStream.setByteOrder(mExifData.getByteOrder()); dataOutputStream.writeShort(TIFF_HEADER); dataOutputStream.writeInt(8); writeAllTags(dataOutputStream); writeThumbnail(dataOutputStream); for (ExifTag t : nullTags) { mExifData.addTag(t); } } private ArrayList stripNullValueTags(ExifData data) { ArrayList nullTags = new ArrayList(); for(ExifTag t : data.getAllTags()) { if (t.getValue() == null && !ExifInterface.isOffsetTag(t.getTagId())) { data.removeTag(t.getTagId(), t.getIfd()); nullTags.add(t); } } return nullTags; } private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException { if (mExifData.hasCompressedThumbnail()) { dataOutputStream.write(mExifData.getCompressedThumbnail()); } else if (mExifData.hasUncompressedStrip()) { for (int i = 0; i < mExifData.getStripCount(); i++) { dataOutputStream.write(mExifData.getStrip(i)); } } } private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException { writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream); writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream); IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY); if (interoperabilityIfd != null) { writeIfd(interoperabilityIfd, dataOutputStream); } IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS); if (gpsIfd != null) { writeIfd(gpsIfd, dataOutputStream); } IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1); if (ifd1 != null) { writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream); } } private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream) throws IOException { ExifTag[] tags = ifd.getAllTags(); dataOutputStream.writeShort((short) tags.length); for (ExifTag tag : tags) { dataOutputStream.writeShort(tag.getTagId()); dataOutputStream.writeShort(tag.getDataType()); dataOutputStream.writeInt(tag.getComponentCount()); if (DEBUG) { Log.v(TAG, "\n" + tag.toString()); } if (tag.getDataSize() > 4) { dataOutputStream.writeInt(tag.getOffset()); } else { ExifOutputStream.writeTagValue(tag, dataOutputStream); for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) { dataOutputStream.write(0); } } } dataOutputStream.writeInt(ifd.getOffsetToNextIfd()); for (ExifTag tag : tags) { if (tag.getDataSize() > 4) { ExifOutputStream.writeTagValue(tag, dataOutputStream); } } } private int calculateOffsetOfIfd(IfdData ifd, int offset) { offset += 2 + ifd.getTagCount() * TAG_SIZE + 4; ExifTag[] tags = ifd.getAllTags(); for (ExifTag tag : tags) { if (tag.getDataSize() > 4) { tag.setOffset(offset); offset += tag.getDataSize(); } } return offset; } private void createRequiredIfdAndTag() throws IOException { // IFD0 is required for all file IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0); if (ifd0 == null) { ifd0 = new IfdData(IfdId.TYPE_IFD_0); mExifData.addIfdData(ifd0); } ExifTag exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD); if (exifOffsetTag == null) { throw new IOException("No definition for crucial exif tag: " + ExifInterface.TAG_EXIF_IFD); } ifd0.setTag(exifOffsetTag); // Exif IFD is required for all files. IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF); if (exifIfd == null) { exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF); mExifData.addIfdData(exifIfd); } // GPS IFD IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS); if (gpsIfd != null) { ExifTag gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD); if (gpsOffsetTag == null) { throw new IOException("No definition for crucial exif tag: " + ExifInterface.TAG_GPS_IFD); } ifd0.setTag(gpsOffsetTag); } // Interoperability IFD IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY); if (interIfd != null) { ExifTag interOffsetTag = mInterface .buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD); if (interOffsetTag == null) { throw new IOException("No definition for crucial exif tag: " + ExifInterface.TAG_INTEROPERABILITY_IFD); } exifIfd.setTag(interOffsetTag); } IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1); // thumbnail if (mExifData.hasCompressedThumbnail()) { if (ifd1 == null) { ifd1 = new IfdData(IfdId.TYPE_IFD_1); mExifData.addIfdData(ifd1); } ExifTag offsetTag = mInterface .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT); if (offsetTag == null) { throw new IOException("No definition for crucial exif tag: " + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT); } ifd1.setTag(offsetTag); ExifTag lengthTag = mInterface .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); if (lengthTag == null) { throw new IOException("No definition for crucial exif tag: " + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); } lengthTag.setValue(mExifData.getCompressedThumbnail().length); ifd1.setTag(lengthTag); // Get rid of tags for uncompressed if they exist. ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)); ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS)); } else if (mExifData.hasUncompressedStrip()) { if (ifd1 == null) { ifd1 = new IfdData(IfdId.TYPE_IFD_1); mExifData.addIfdData(ifd1); } int stripCount = mExifData.getStripCount(); ExifTag offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS); if (offsetTag == null) { throw new IOException("No definition for crucial exif tag: " + ExifInterface.TAG_STRIP_OFFSETS); } ExifTag lengthTag = mInterface .buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS); if (lengthTag == null) { throw new IOException("No definition for crucial exif tag: " + ExifInterface.TAG_STRIP_BYTE_COUNTS); } long[] lengths = new long[stripCount]; for (int i = 0; i < mExifData.getStripCount(); i++) { lengths[i] = mExifData.getStrip(i).length; } lengthTag.setValue(lengths); ifd1.setTag(offsetTag); ifd1.setTag(lengthTag); // Get rid of tags for compressed if they exist. ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)); ifd1.removeTag(ExifInterface .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)); } else if (ifd1 != null) { // Get rid of offset and length tags if there is no thumbnail. ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)); ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS)); ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)); ifd1.removeTag(ExifInterface .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)); } } private int calculateAllOffset() { int offset = TIFF_HEADER_SIZE; IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0); offset = calculateOffsetOfIfd(ifd0, offset); ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)).setValue(offset); IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF); offset = calculateOffsetOfIfd(exifIfd, offset); IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY); if (interIfd != null) { exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD)) .setValue(offset); offset = calculateOffsetOfIfd(interIfd, offset); } IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS); if (gpsIfd != null) { ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset); offset = calculateOffsetOfIfd(gpsIfd, offset); } IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1); if (ifd1 != null) { ifd0.setOffsetToNextIfd(offset); offset = calculateOffsetOfIfd(ifd1, offset); } // thumbnail if (mExifData.hasCompressedThumbnail()) { ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) .setValue(offset); offset += mExifData.getCompressedThumbnail().length; } else if (mExifData.hasUncompressedStrip()) { int stripCount = mExifData.getStripCount(); long[] offsets = new long[stripCount]; for (int i = 0; i < mExifData.getStripCount(); i++) { offsets[i] = offset; offset += mExifData.getStrip(i).length; } ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue( offsets); } return offset; } static void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream) throws IOException { switch (tag.getDataType()) { case ExifTag.TYPE_ASCII: byte buf[] = tag.getStringByte(); if (buf.length == tag.getComponentCount()) { buf[buf.length - 1] = 0; dataOutputStream.write(buf); } else { dataOutputStream.write(buf); dataOutputStream.write(0); } break; case ExifTag.TYPE_LONG: case ExifTag.TYPE_UNSIGNED_LONG: for (int i = 0, n = tag.getComponentCount(); i < n; i++) { dataOutputStream.writeInt((int) tag.getValueAt(i)); } break; case ExifTag.TYPE_RATIONAL: case ExifTag.TYPE_UNSIGNED_RATIONAL: for (int i = 0, n = tag.getComponentCount(); i < n; i++) { dataOutputStream.writeRational(tag.getRational(i)); } break; case ExifTag.TYPE_UNDEFINED: case ExifTag.TYPE_UNSIGNED_BYTE: buf = new byte[tag.getComponentCount()]; tag.getBytes(buf); dataOutputStream.write(buf); break; case ExifTag.TYPE_UNSIGNED_SHORT: for (int i = 0, n = tag.getComponentCount(); i < n; i++) { dataOutputStream.writeShort((short) tag.getValueAt(i)); } break; } } }