diff options
author | Earl Ou <shunhsingou@google.com> | 2012-08-28 18:39:49 +0800 |
---|---|---|
committer | Earl Ou <shunhsingou@google.com> | 2012-08-30 14:50:19 +0800 |
commit | 4729f2992f820d5c03011253cd5f528bf0b40129 (patch) | |
tree | cdcfa9ea9dce8ab49c4ac2a618ab95ef7fb12179 | |
parent | 1d3dfbf55bb320dc4d1d4379c385b629c107f713 (diff) | |
download | android_packages_apps_Snap-4729f2992f820d5c03011253cd5f528bf0b40129.tar.gz android_packages_apps_Snap-4729f2992f820d5c03011253cd5f528bf0b40129.tar.bz2 android_packages_apps_Snap-4729f2992f820d5c03011253cd5f528bf0b40129.zip |
ExifOutputStream
Change-Id: I9f2de77e87a502ccdadba0b18658621028c538c9
6 files changed, 518 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/exif/ExifData.java b/src/com/android/gallery3d/exif/ExifData.java index eca819ea0..776f77a65 100644 --- a/src/com/android/gallery3d/exif/ExifData.java +++ b/src/com/android/gallery3d/exif/ExifData.java @@ -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 index 000000000..1c0baf251 --- /dev/null +++ b/src/com/android/gallery3d/exif/ExifOutputStream.java @@ -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 index 000000000..9244be329 --- /dev/null +++ b/src/com/android/gallery3d/exif/OrderedDataOutputStream.java @@ -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()); + } +} diff --git a/src/com/android/gallery3d/exif/Util.java b/src/com/android/gallery3d/exif/Util.java index c7d9ce842..594d6fc7f 100644 --- a/src/com/android/gallery3d/exif/Util.java +++ b/src/com/android/gallery3d/exif/Util.java @@ -16,8 +16,19 @@ 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 index 000000000..fb85b2c91 --- /dev/null +++ b/tests/src/com/android/gallery3d/exif/ExifOutputStreamTest.java @@ -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 diff --git a/tests/src/com/android/gallery3d/exif/ExifTestRunner.java b/tests/src/com/android/gallery3d/exif/ExifTestRunner.java index 022597d73..bcbc9f57b 100644 --- a/tests/src/com/android/gallery3d/exif/ExifTestRunner.java +++ b/tests/src/com/android/gallery3d/exif/ExifTestRunner.java @@ -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; } |