Home | History | Annotate | Download | only in exif
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.mms.exif;
     18 
     19 import com.android.mms.LogTag;
     20 
     21 import android.util.Log;
     22 
     23 import java.io.BufferedOutputStream;
     24 import java.io.FilterOutputStream;
     25 import java.io.IOException;
     26 import java.io.OutputStream;
     27 import java.nio.ByteBuffer;
     28 import java.nio.ByteOrder;
     29 import java.util.ArrayList;
     30 
     31 /**
     32  * This class provides a way to replace the Exif header of a JPEG image.
     33  * <p>
     34  * Below is an example of writing EXIF data into a file
     35  *
     36  * <pre>
     37  * public static void writeExif(byte[] jpeg, ExifData exif, String path) {
     38  *     OutputStream os = null;
     39  *     try {
     40  *         os = new FileOutputStream(path);
     41  *         ExifOutputStream eos = new ExifOutputStream(os);
     42  *         // Set the exif header
     43  *         eos.setExifData(exif);
     44  *         // Write the original jpeg out, the header will be add into the file.
     45  *         eos.write(jpeg);
     46  *     } catch (FileNotFoundException e) {
     47  *         e.printStackTrace();
     48  *     } catch (IOException e) {
     49  *         e.printStackTrace();
     50  *     } finally {
     51  *         if (os != null) {
     52  *             try {
     53  *                 os.close();
     54  *             } catch (IOException e) {
     55  *                 e.printStackTrace();
     56  *             }
     57  *         }
     58  *     }
     59  * }
     60  * </pre>
     61  */
     62 class ExifOutputStream extends FilterOutputStream {
     63     private static final String TAG = LogTag.TAG;
     64     private static final boolean DEBUG = false;
     65     private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
     66 
     67     private static final int STATE_SOI = 0;
     68     private static final int STATE_FRAME_HEADER = 1;
     69     private static final int STATE_JPEG_DATA = 2;
     70 
     71     private static final int EXIF_HEADER = 0x45786966;
     72     private static final short TIFF_HEADER = 0x002A;
     73     private static final short TIFF_BIG_ENDIAN = 0x4d4d;
     74     private static final short TIFF_LITTLE_ENDIAN = 0x4949;
     75     private static final short TAG_SIZE = 12;
     76     private static final short TIFF_HEADER_SIZE = 8;
     77     private static final int MAX_EXIF_SIZE = 65535;
     78 
     79     private ExifData mExifData;
     80     private int mState = STATE_SOI;
     81     private int mByteToSkip;
     82     private int mByteToCopy;
     83     private final byte[] mSingleByteArray = new byte[1];
     84     private final ByteBuffer mBuffer = ByteBuffer.allocate(4);
     85     private final ExifInterface mInterface;
     86 
     87     protected ExifOutputStream(OutputStream ou, ExifInterface iRef) {
     88         super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
     89         mInterface = iRef;
     90     }
     91 
     92     /**
     93      * Sets the ExifData to be written into the JPEG file. Should be called
     94      * before writing image data.
     95      */
     96     protected void setExifData(ExifData exifData) {
     97         mExifData = exifData;
     98     }
     99 
    100     /**
    101      * Gets the Exif header to be written into the JPEF file.
    102      */
    103     protected ExifData getExifData() {
    104         return mExifData;
    105     }
    106 
    107     private int requestByteToBuffer(int requestByteCount, byte[] buffer
    108             , int offset, int length) {
    109         int byteNeeded = requestByteCount - mBuffer.position();
    110         int byteToRead = length > byteNeeded ? byteNeeded : length;
    111         mBuffer.put(buffer, offset, byteToRead);
    112         return byteToRead;
    113     }
    114 
    115     /**
    116      * Writes the image out. The input data should be a valid JPEG format. After
    117      * writing, it's Exif header will be replaced by the given header.
    118      */
    119     @Override
    120     public void write(byte[] buffer, int offset, int length) throws IOException {
    121         while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
    122                 && length > 0) {
    123             if (mByteToSkip > 0) {
    124                 int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
    125                 length -= byteToProcess;
    126                 mByteToSkip -= byteToProcess;
    127                 offset += byteToProcess;
    128             }
    129             if (mByteToCopy > 0) {
    130                 int byteToProcess = length > mByteToCopy ? mByteToCopy : length;
    131                 out.write(buffer, offset, byteToProcess);
    132                 length -= byteToProcess;
    133                 mByteToCopy -= byteToProcess;
    134                 offset += byteToProcess;
    135             }
    136             if (length == 0) {
    137                 return;
    138             }
    139             switch (mState) {
    140                 case STATE_SOI:
    141                     int byteRead = requestByteToBuffer(2, buffer, offset, length);
    142                     offset += byteRead;
    143                     length -= byteRead;
    144                     if (mBuffer.position() < 2) {
    145                         return;
    146                     }
    147                     mBuffer.rewind();
    148                     if (mBuffer.getShort() != JpegHeader.SOI) {
    149                         throw new IOException("Not a valid jpeg image, cannot write exif");
    150                     }
    151                     out.write(mBuffer.array(), 0, 2);
    152                     mState = STATE_FRAME_HEADER;
    153                     mBuffer.rewind();
    154                     writeExifData();
    155                     break;
    156                 case STATE_FRAME_HEADER:
    157                     // We ignore the APP1 segment and copy all other segments
    158                     // until SOF tag.
    159                     byteRead = requestByteToBuffer(4, buffer, offset, length);
    160                     offset += byteRead;
    161                     length -= byteRead;
    162                     // Check if this image data doesn't contain SOF.
    163                     if (mBuffer.position() == 2) {
    164                         short tag = mBuffer.getShort();
    165                         if (tag == JpegHeader.EOI) {
    166                             out.write(mBuffer.array(), 0, 2);
    167                             mBuffer.rewind();
    168                         }
    169                     }
    170                     if (mBuffer.position() < 4) {
    171                         return;
    172                     }
    173                     mBuffer.rewind();
    174                     short marker = mBuffer.getShort();
    175                     if (marker == JpegHeader.APP1) {
    176                         mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
    177                         mState = STATE_JPEG_DATA;
    178                     } else if (!JpegHeader.isSofMarker(marker)) {
    179                         out.write(mBuffer.array(), 0, 4);
    180                         mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
    181                     } else {
    182                         out.write(mBuffer.array(), 0, 4);
    183                         mState = STATE_JPEG_DATA;
    184                     }
    185                     mBuffer.rewind();
    186             }
    187         }
    188         if (length > 0) {
    189             out.write(buffer, offset, length);
    190         }
    191     }
    192 
    193     /**
    194      * Writes the one bytes out. The input data should be a valid JPEG format.
    195      * After writing, it's Exif header will be replaced by the given header.
    196      */
    197     @Override
    198     public void write(int oneByte) throws IOException {
    199         mSingleByteArray[0] = (byte) (0xff & oneByte);
    200         write(mSingleByteArray);
    201     }
    202 
    203     /**
    204      * Equivalent to calling write(buffer, 0, buffer.length).
    205      */
    206     @Override
    207     public void write(byte[] buffer) throws IOException {
    208         write(buffer, 0, buffer.length);
    209     }
    210 
    211     private void writeExifData() throws IOException {
    212         if (mExifData == null) {
    213             return;
    214         }
    215         if (DEBUG) {
    216             Log.v(TAG, "Writing exif data...");
    217         }
    218         ArrayList<ExifTag> nullTags = stripNullValueTags(mExifData);
    219         createRequiredIfdAndTag();
    220         int exifSize = calculateAllOffset();
    221         if (exifSize + 8 > MAX_EXIF_SIZE) {
    222             throw new IOException("Exif header is too large (>64Kb)");
    223         }
    224         OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out);
    225         dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
    226         dataOutputStream.writeShort(JpegHeader.APP1);
    227         dataOutputStream.writeShort((short) (exifSize + 8));
    228         dataOutputStream.writeInt(EXIF_HEADER);
    229         dataOutputStream.writeShort((short) 0x0000);
    230         if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) {
    231             dataOutputStream.writeShort(TIFF_BIG_ENDIAN);
    232         } else {
    233             dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN);
    234         }
    235         dataOutputStream.setByteOrder(mExifData.getByteOrder());
    236         dataOutputStream.writeShort(TIFF_HEADER);
    237         dataOutputStream.writeInt(8);
    238         writeAllTags(dataOutputStream);
    239         writeThumbnail(dataOutputStream);
    240         for (ExifTag t : nullTags) {
    241             mExifData.addTag(t);
    242         }
    243     }
    244 
    245     private ArrayList<ExifTag> stripNullValueTags(ExifData data) {
    246         ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>();
    247         for(ExifTag t : data.getAllTags()) {
    248             if (t.getValue() == null && !ExifInterface.isOffsetTag(t.getTagId())) {
    249                 data.removeTag(t.getTagId(), t.getIfd());
    250                 nullTags.add(t);
    251             }
    252         }
    253         return nullTags;
    254     }
    255 
    256     private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException {
    257         if (mExifData.hasCompressedThumbnail()) {
    258             dataOutputStream.write(mExifData.getCompressedThumbnail());
    259         } else if (mExifData.hasUncompressedStrip()) {
    260             for (int i = 0; i < mExifData.getStripCount(); i++) {
    261                 dataOutputStream.write(mExifData.getStrip(i));
    262             }
    263         }
    264     }
    265 
    266     private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException {
    267         writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream);
    268         writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream);
    269         IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
    270         if (interoperabilityIfd != null) {
    271             writeIfd(interoperabilityIfd, dataOutputStream);
    272         }
    273         IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
    274         if (gpsIfd != null) {
    275             writeIfd(gpsIfd, dataOutputStream);
    276         }
    277         IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
    278         if (ifd1 != null) {
    279             writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream);
    280         }
    281     }
    282 
    283     private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream)
    284             throws IOException {
    285         ExifTag[] tags = ifd.getAllTags();
    286         dataOutputStream.writeShort((short) tags.length);
    287         for (ExifTag tag : tags) {
    288             dataOutputStream.writeShort(tag.getTagId());
    289             dataOutputStream.writeShort(tag.getDataType());
    290             dataOutputStream.writeInt(tag.getComponentCount());
    291             if (DEBUG) {
    292                 Log.v(TAG, "\n" + tag.toString());
    293             }
    294             if (tag.getDataSize() > 4) {
    295                 dataOutputStream.writeInt(tag.getOffset());
    296             } else {
    297                 ExifOutputStream.writeTagValue(tag, dataOutputStream);
    298                 for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) {
    299                     dataOutputStream.write(0);
    300                 }
    301             }
    302         }
    303         dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
    304         for (ExifTag tag : tags) {
    305             if (tag.getDataSize() > 4) {
    306                 ExifOutputStream.writeTagValue(tag, dataOutputStream);
    307             }
    308         }
    309     }
    310 
    311     private int calculateOffsetOfIfd(IfdData ifd, int offset) {
    312         offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
    313         ExifTag[] tags = ifd.getAllTags();
    314         for (ExifTag tag : tags) {
    315             if (tag.getDataSize() > 4) {
    316                 tag.setOffset(offset);
    317                 offset += tag.getDataSize();
    318             }
    319         }
    320         return offset;
    321     }
    322 
    323     private void createRequiredIfdAndTag() throws IOException {
    324         // IFD0 is required for all file
    325         IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
    326         if (ifd0 == null) {
    327             ifd0 = new IfdData(IfdId.TYPE_IFD_0);
    328             mExifData.addIfdData(ifd0);
    329         }
    330         ExifTag exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD);
    331         if (exifOffsetTag == null) {
    332             throw new IOException("No definition for crucial exif tag: "
    333                     + ExifInterface.TAG_EXIF_IFD);
    334         }
    335         ifd0.setTag(exifOffsetTag);
    336 
    337         // Exif IFD is required for all files.
    338         IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
    339         if (exifIfd == null) {
    340             exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF);
    341             mExifData.addIfdData(exifIfd);
    342         }
    343 
    344         // GPS IFD
    345         IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
    346         if (gpsIfd != null) {
    347             ExifTag gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD);
    348             if (gpsOffsetTag == null) {
    349                 throw new IOException("No definition for crucial exif tag: "
    350                         + ExifInterface.TAG_GPS_IFD);
    351             }
    352             ifd0.setTag(gpsOffsetTag);
    353         }
    354 
    355         // Interoperability IFD
    356         IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
    357         if (interIfd != null) {
    358             ExifTag interOffsetTag = mInterface
    359                     .buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD);
    360             if (interOffsetTag == null) {
    361                 throw new IOException("No definition for crucial exif tag: "
    362                         + ExifInterface.TAG_INTEROPERABILITY_IFD);
    363             }
    364             exifIfd.setTag(interOffsetTag);
    365         }
    366 
    367         IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
    368 
    369         // thumbnail
    370         if (mExifData.hasCompressedThumbnail()) {
    371 
    372             if (ifd1 == null) {
    373                 ifd1 = new IfdData(IfdId.TYPE_IFD_1);
    374                 mExifData.addIfdData(ifd1);
    375             }
    376 
    377             ExifTag offsetTag = mInterface
    378                     .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
    379             if (offsetTag == null) {
    380                 throw new IOException("No definition for crucial exif tag: "
    381                         + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
    382             }
    383 
    384             ifd1.setTag(offsetTag);
    385             ExifTag lengthTag = mInterface
    386                     .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
    387             if (lengthTag == null) {
    388                 throw new IOException("No definition for crucial exif tag: "
    389                         + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
    390             }
    391 
    392             lengthTag.setValue(mExifData.getCompressedThumbnail().length);
    393             ifd1.setTag(lengthTag);
    394 
    395             // Get rid of tags for uncompressed if they exist.
    396             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
    397             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
    398         } else if (mExifData.hasUncompressedStrip()) {
    399             if (ifd1 == null) {
    400                 ifd1 = new IfdData(IfdId.TYPE_IFD_1);
    401                 mExifData.addIfdData(ifd1);
    402             }
    403             int stripCount = mExifData.getStripCount();
    404             ExifTag offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS);
    405             if (offsetTag == null) {
    406                 throw new IOException("No definition for crucial exif tag: "
    407                         + ExifInterface.TAG_STRIP_OFFSETS);
    408             }
    409             ExifTag lengthTag = mInterface
    410                     .buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS);
    411             if (lengthTag == null) {
    412                 throw new IOException("No definition for crucial exif tag: "
    413                         + ExifInterface.TAG_STRIP_BYTE_COUNTS);
    414             }
    415             long[] lengths = new long[stripCount];
    416             for (int i = 0; i < mExifData.getStripCount(); i++) {
    417                 lengths[i] = mExifData.getStrip(i).length;
    418             }
    419             lengthTag.setValue(lengths);
    420             ifd1.setTag(offsetTag);
    421             ifd1.setTag(lengthTag);
    422             // Get rid of tags for compressed if they exist.
    423             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
    424             ifd1.removeTag(ExifInterface
    425                     .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
    426         } else if (ifd1 != null) {
    427             // Get rid of offset and length tags if there is no thumbnail.
    428             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
    429             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
    430             ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
    431             ifd1.removeTag(ExifInterface
    432                     .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
    433         }
    434     }
    435 
    436     private int calculateAllOffset() {
    437         int offset = TIFF_HEADER_SIZE;
    438         IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
    439         offset = calculateOffsetOfIfd(ifd0, offset);
    440         ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)).setValue(offset);
    441 
    442         IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
    443         offset = calculateOffsetOfIfd(exifIfd, offset);
    444 
    445         IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
    446         if (interIfd != null) {
    447             exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD))
    448                     .setValue(offset);
    449             offset = calculateOffsetOfIfd(interIfd, offset);
    450         }
    451 
    452         IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
    453         if (gpsIfd != null) {
    454             ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset);
    455             offset = calculateOffsetOfIfd(gpsIfd, offset);
    456         }
    457 
    458         IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
    459         if (ifd1 != null) {
    460             ifd0.setOffsetToNextIfd(offset);
    461             offset = calculateOffsetOfIfd(ifd1, offset);
    462         }
    463 
    464         // thumbnail
    465         if (mExifData.hasCompressedThumbnail()) {
    466             ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
    467                     .setValue(offset);
    468             offset += mExifData.getCompressedThumbnail().length;
    469         } else if (mExifData.hasUncompressedStrip()) {
    470             int stripCount = mExifData.getStripCount();
    471             long[] offsets = new long[stripCount];
    472             for (int i = 0; i < mExifData.getStripCount(); i++) {
    473                 offsets[i] = offset;
    474                 offset += mExifData.getStrip(i).length;
    475             }
    476             ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue(
    477                     offsets);
    478         }
    479         return offset;
    480     }
    481 
    482     static void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
    483             throws IOException {
    484         switch (tag.getDataType()) {
    485             case ExifTag.TYPE_ASCII:
    486                 byte buf[] = tag.getStringByte();
    487                 if (buf.length == tag.getComponentCount()) {
    488                     buf[buf.length - 1] = 0;
    489                     dataOutputStream.write(buf);
    490                 } else {
    491                     dataOutputStream.write(buf);
    492                     dataOutputStream.write(0);
    493                 }
    494                 break;
    495             case ExifTag.TYPE_LONG:
    496             case ExifTag.TYPE_UNSIGNED_LONG:
    497                 for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
    498                     dataOutputStream.writeInt((int) tag.getValueAt(i));
    499                 }
    500                 break;
    501             case ExifTag.TYPE_RATIONAL:
    502             case ExifTag.TYPE_UNSIGNED_RATIONAL:
    503                 for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
    504                     dataOutputStream.writeRational(tag.getRational(i));
    505                 }
    506                 break;
    507             case ExifTag.TYPE_UNDEFINED:
    508             case ExifTag.TYPE_UNSIGNED_BYTE:
    509                 buf = new byte[tag.getComponentCount()];
    510                 tag.getBytes(buf);
    511                 dataOutputStream.write(buf);
    512                 break;
    513             case ExifTag.TYPE_UNSIGNED_SHORT:
    514                 for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
    515                     dataOutputStream.writeShort((short) tag.getValueAt(i));
    516                 }
    517                 break;
    518         }
    519     }
    520 }
    521