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