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