content: /*
   * 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.
   * <p>
   * Below is an example of writing EXIF data into a file
   *
   * <pre>
   * 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();
   *             }
   *         }
   *     }
   * }
   * </pre>
   */
  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<ExifTag> 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<ExifTag> stripNullValueTags(ExifData data) {
          ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>();
          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;
          }
      }
  }
  