OSDN Git Service

ExifOutputStream
authorEarl Ou <shunhsingou@google.com>
Tue, 28 Aug 2012 10:39:49 +0000 (18:39 +0800)
committerEarl Ou <shunhsingou@google.com>
Thu, 30 Aug 2012 06:50:19 +0000 (14:50 +0800)
Change-Id: I9f2de77e87a502ccdadba0b18658621028c538c9

src/com/android/gallery3d/exif/ExifData.java
src/com/android/gallery3d/exif/ExifOutputStream.java [new file with mode: 0644]
src/com/android/gallery3d/exif/OrderedDataOutputStream.java [new file with mode: 0644]
src/com/android/gallery3d/exif/Util.java
tests/src/com/android/gallery3d/exif/ExifOutputStreamTest.java [new file with mode: 0644]
tests/src/com/android/gallery3d/exif/ExifTestRunner.java

index eca819e..776f77a 100644 (file)
@@ -116,6 +116,13 @@ public class ExifData {
         return mByteOrder;
     }
 
+    /**
+     * Returns true if this header contains compressed strip of thumbnail.
+     */
+    public boolean hasUncompressedStrip() {
+        return mStripBytes.size() != 0;
+    }
+
     @Override
     public boolean equals(Object obj) {
         if (obj instanceof ExifData) {
diff --git a/src/com/android/gallery3d/exif/ExifOutputStream.java b/src/com/android/gallery3d/exif/ExifOutputStream.java
new file mode 100644 (file)
index 0000000..1c0baf2
--- /dev/null
@@ -0,0 +1,370 @@
+/*
+ * 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 java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class ExifOutputStream extends FilterOutputStream {
+    private static final String TAG = "ExifOutputStream";
+
+    private static final int STATE_SOI = 0;
+    private static final int STATE_APP1 = 1;
+    private static final int STATE_JPEG_DATA = 2;
+
+    private static final short SOI = (short) 0xFFD8;
+    private static final short APP0 = (short) 0xFFE0;
+    private static final short APP1 = (short) 0xFFE1;
+    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 ExifData mExifData;
+    private int mState;
+    private int mByteToSkip;
+    private int mByteToCopy;
+    private ByteBuffer mBuffer = ByteBuffer.allocate(4);
+
+    public ExifOutputStream(OutputStream ou) {
+        super(ou);
+    }
+
+    public void setExifData(ExifData exifData) {
+        mExifData = exifData;
+    }
+
+    public 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;
+    }
+
+    @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;
+            }
+            switch (mState) {
+                case STATE_SOI:
+                    int byteRead = requestByteToBuffer(2, buffer, offset, length);
+                    offset += byteRead;
+                    length -= byteRead;
+                    if (mBuffer.position() < 2) return;
+                    mBuffer.rewind();
+                    assert(mBuffer.getShort() == SOI);
+                    out.write(mBuffer.array(), 0 ,2);
+                    mState = STATE_APP1;
+                    mBuffer.rewind();
+                    break;
+                case STATE_APP1:
+                    byteRead = requestByteToBuffer(4, buffer, offset, length);
+                    offset += byteRead;
+                    length -= byteRead;
+                    if (mBuffer.position() < 4) return;
+                    mBuffer.rewind();
+                    if (mBuffer.getShort() == APP0) {
+                        out.write(mBuffer.array(), 0 ,4);
+                        mByteToCopy = (mBuffer.getShort() & 0xff) - 2;
+                    } else if (mBuffer.getShort() == APP1) {
+                        writeExifData();
+                        mByteToSkip = (mBuffer.getShort() & 0xff) - 2;
+                        mState = STATE_JPEG_DATA;
+                    } else {
+                        writeExifData();
+                        out.write(mBuffer.array(), 0, 4);
+                        mState = STATE_JPEG_DATA;
+                    }
+                    mBuffer.rewind();
+                    break;
+            }
+        }
+        if (length > 0) {
+            out.write(buffer, offset, length);
+        }
+    }
+
+    @Override
+    public void write(int oneByte) throws IOException {
+        byte[] buf = new byte[] {(byte) (0xff & oneByte)};
+        write(buf);
+    }
+
+    @Override
+    public void write(byte[] buffer) throws IOException {
+        write(buffer, 0, buffer.length);
+    }
+
+    private void writeExifData() throws IOException {
+        createRequiredIfdAndTag();
+        int exifSize = calculateAllOffset();
+        OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out);
+        dataOutputStream.writeShort(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);
+        writeAllTag(dataOutputStream);
+        writeThumbnail(dataOutputStream);
+    }
+
+    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 writeAllTag(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(new ExifTag[] {});
+        dataOutputStream.writeShort((short) tags.length);
+        for (ExifTag tag: tags) {
+            dataOutputStream.writeShort(tag.getTagId());
+            dataOutputStream.writeShort(tag.getDataType());
+            dataOutputStream.writeInt(tag.getComponentCount());
+            if (tag.getDataSize() > 4) {
+                dataOutputStream.writeInt(tag.getOffset());
+            } else {
+                writeTagValue(tag, dataOutputStream);
+                for (int i = 0; i < 4 - tag.getDataSize(); i++) {
+                    dataOutputStream.write(0);
+                }
+            }
+        }
+        dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
+        for (ExifTag tag: tags) {
+            if (tag.getDataSize() > 4) {
+                writeTagValue(tag, dataOutputStream);
+            }
+        }
+    }
+
+    private void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
+            throws IOException {
+        switch (tag.getDataType()) {
+            case ExifTag.TYPE_ASCII:
+                dataOutputStream.write(tag.getString().getBytes());
+                int remain = tag.getComponentCount() - tag.getString().getBytes().length;
+                for (int i = 0; i < remain; i++) {
+                    dataOutputStream.write(0);
+                }
+                break;
+            case ExifTag.TYPE_INT:
+                for (int i = 0; i < tag.getComponentCount(); i++) {
+                    dataOutputStream.writeInt(tag.getInt(i));
+                }
+                break;
+            case ExifTag.TYPE_RATIONAL:
+            case ExifTag.TYPE_UNSIGNED_RATIONAL:
+                for (int i = 0; i < tag.getComponentCount(); i++) {
+                    dataOutputStream.writeRational(tag.getRational(i));
+                }
+                break;
+            case ExifTag.TYPE_UNDEFINED:
+            case ExifTag.TYPE_UNSIGNED_BYTE:
+                byte[] buf = new byte[tag.getComponentCount()];
+                tag.getBytes(buf);
+                dataOutputStream.write(buf);
+                break;
+            case ExifTag.TYPE_UNSIGNED_INT:
+                for (int i = 0; i < tag.getComponentCount(); i++) {
+                    dataOutputStream.writeInt((int) tag.getUnsignedInt(i));
+                }
+                break;
+            case ExifTag.TYPE_UNSIGNED_SHORT:
+                for (int i = 0; i < tag.getComponentCount(); i++) {
+                    dataOutputStream.writeShort((short) tag.getUnsignedShort(i));
+                }
+                break;
+        }
+    }
+
+    private int calculateOffsetOfIfd(IfdData ifd, int offset) {
+        offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
+        ExifTag[] tags = ifd.getAllTags(new ExifTag[] {});
+        for(ExifTag tag: tags) {
+            if (tag.getDataSize() > 4) {
+                tag.setOffset(offset);
+                offset += tag.getDataSize();
+            }
+        }
+        return offset;
+    }
+
+    private void createRequiredIfdAndTag() {
+        // 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 = new ExifTag(ExifTag.TIFF_TAG.TAG_EXIF_IFD
+                , ExifTag.TYPE_UNSIGNED_INT, 1, IfdId.TYPE_IFD_0);
+        ifd0.setTag(exifOffsetTag);
+
+        // Exif IFD is required for all file.
+        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 = new ExifTag(ExifTag.TIFF_TAG.TAG_GPS_IFD,
+                    ExifTag.TYPE_UNSIGNED_INT, 1, IfdId.TYPE_IFD_0);
+            ifd0.setTag(gpsOffsetTag);
+        }
+
+        // Interoperability IFD
+        IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
+        if (interIfd != null) {
+            ExifTag interOffsetTag = new ExifTag(ExifTag.EXIF_TAG.TAG_INTEROPERABILITY_IFD,
+                    ExifTag.TYPE_UNSIGNED_INT, 1, IfdId.TYPE_IFD_EXIF);
+            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 = new ExifTag(ExifTag.TIFF_TAG.TAG_JPEG_INTERCHANGE_FORMAT,
+                    ExifTag.TYPE_UNSIGNED_INT, 1, IfdId.TYPE_IFD_1);
+            ifd1.setTag(offsetTag);
+            ExifTag lengthTag = new ExifTag(ExifTag.TIFF_TAG.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+                    ExifTag.TYPE_UNSIGNED_INT, 1, IfdId.TYPE_IFD_1);
+            lengthTag.setValue(mExifData.getCompressedThumbnail().length);
+            ifd1.setTag(lengthTag);
+        } else if (mExifData.hasUncompressedStrip()){
+            if (ifd1 == null) {
+                ifd1 = new IfdData(IfdId.TYPE_IFD_1);
+                mExifData.addIfdData(ifd1);
+            }
+            int stripCount = mExifData.getStripCount();
+            ExifTag offsetTag = new ExifTag(ExifTag.TIFF_TAG.TAG_STRIP_OFFSETS,
+                    ExifTag.TYPE_UNSIGNED_INT, stripCount, IfdId.TYPE_IFD_1);
+            ExifTag lengthTag = new ExifTag(ExifTag.TIFF_TAG.TAG_STRIP_BYTE_COUNTS,
+                    ExifTag.TYPE_UNSIGNED_INT, stripCount, IfdId.TYPE_IFD_1);
+            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);
+        }
+    }
+
+    private int calculateAllOffset() {
+        int offset = TIFF_HEADER_SIZE;
+        IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
+        offset = calculateOffsetOfIfd(ifd0, offset);
+        ifd0.getTag(ExifTag.TIFF_TAG.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(ExifTag.EXIF_TAG.TAG_INTEROPERABILITY_IFD).setValue(offset);
+            offset = calculateOffsetOfIfd(interIfd, offset);
+        }
+
+        IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
+        if (gpsIfd != null) {
+            ifd0.getTag(ExifTag.TIFF_TAG.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(ExifTag.TIFF_TAG.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(ExifTag.TIFF_TAG.TAG_STRIP_OFFSETS).setValue(offsets);
+        }
+        return offset;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/exif/OrderedDataOutputStream.java b/src/com/android/gallery3d/exif/OrderedDataOutputStream.java
new file mode 100644 (file)
index 0000000..9244be3
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class OrderedDataOutputStream extends FilterOutputStream {
+    private final ByteBuffer mByteBuffer = ByteBuffer.allocate(4);
+
+    public OrderedDataOutputStream(OutputStream out) {
+        super(out);
+    }
+
+    public void setByteOrder(ByteOrder order) {
+        mByteBuffer.order(order);
+    }
+
+    public void writeShort(short value) throws IOException {
+        mByteBuffer.rewind();
+        mByteBuffer.putShort(value);
+        out.write(mByteBuffer.array(), 0, 2);
+     }
+
+    public void writeInt(int value) throws IOException {
+        mByteBuffer.rewind();
+        mByteBuffer.putInt(value);
+        out.write(mByteBuffer.array());
+    }
+
+    public void writeRational(Rational rational) throws IOException {
+        writeInt((int) rational.getNominator());
+        writeInt((int) rational.getDenominator());
+    }
+}
index c7d9ce8..594d6fc 100644 (file)
 
 package com.android.gallery3d.exif;
 
+import java.io.Closeable;
+
 class Util {
     public static boolean equals(Object a, Object b) {
         return (a == b) || (a == null ? false : a.equals(b));
     }
+
+    public static void closeSilently(Closeable c) {
+        if (c == null) return;
+        try {
+            c.close();
+        } catch (Throwable t) {
+            // do nothing
+        }
+    }
 }
diff --git a/tests/src/com/android/gallery3d/exif/ExifOutputStreamTest.java b/tests/src/com/android/gallery3d/exif/ExifOutputStreamTest.java
new file mode 100644 (file)
index 0000000..fb85b2c
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.test.InstrumentationTestCase;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ExifOutputStreamTest extends InstrumentationTestCase {
+
+    private final int mImageResourceId;
+
+    public ExifOutputStreamTest(int imageResourceId, int xmlReourceId) {
+        mImageResourceId = imageResourceId;
+    }
+
+    public void testExifOutputStream() throws IOException, ExifInvalidFormatException {
+        File file = File.createTempFile("exif_test", ".jpg");
+        InputStream imageInputStream = null;
+        InputStream exifInputStream = null;
+        FileInputStream reDecodeInputStream = null;
+        FileInputStream reParseInputStream = null;
+        try {
+            // Read the image
+            imageInputStream = getInstrumentation()
+                    .getContext().getResources().openRawResource(mImageResourceId);
+            Bitmap bmp = BitmapFactory.decodeStream(imageInputStream);
+
+            // Read exif data
+            exifInputStream = getInstrumentation()
+                    .getContext().getResources().openRawResource(mImageResourceId);
+            ExifData exifData = new ExifReader().read(exifInputStream);
+
+            // Encode the image with the exif data
+            FileOutputStream outputStream = new FileOutputStream(file);
+            ExifOutputStream exifOutputStream = new ExifOutputStream(outputStream);
+            exifOutputStream.setExifData(exifData);
+            bmp.compress(Bitmap.CompressFormat.JPEG, 100, exifOutputStream);
+            exifOutputStream.close();
+
+            // Re-decode the temp file and check the data.
+            reDecodeInputStream = new FileInputStream(file);
+            Bitmap decodedBmp = BitmapFactory.decodeStream(reDecodeInputStream);
+            assertNotNull(decodedBmp);
+
+            // Re-parse the temp file the check EXIF tag
+            reParseInputStream = new FileInputStream(file);
+            ExifData reExifData = new ExifReader().read(reParseInputStream);
+            assertEquals(exifData, reExifData);
+        } finally {
+            Util.closeSilently(imageInputStream);
+            Util.closeSilently(exifInputStream);
+            Util.closeSilently(reDecodeInputStream);
+            Util.closeSilently(reParseInputStream);
+        }
+    }
+}
\ No newline at end of file
index 022597d..bcbc9f5 100644 (file)
@@ -44,6 +44,7 @@ public class ExifTestRunner extends InstrumentationTestRunner {
         TestSuite suite = new InstrumentationTestSuite(this);
         getAllTestFromTestCase(ExifParserTest.class, suite);
         getAllTestFromTestCase(ExifReaderTest.class, suite);
+        getAllTestFromTestCase(ExifOutputStreamTest.class, suite);
         return suite;
     }