From 2523f4344661b1e6a734d1ba20e92308c87a7c54 Mon Sep 17 00:00:00 2001 From: Hung-ying Tyan Date: Fri, 19 Oct 2012 20:50:50 +0800 Subject: Move new exif lib to gallerycommon. https://googleplex-android-review.googlesource.com/#/c/242047 depends on this. Bug: 6667860 Change-Id: Iac937e2eea94f0f5ffde876235b1c2ee748e7570 --- .../gallery3d/exif/CountedDataInputStream.java | 136 ++ .../src/com/android/gallery3d/exif/ExifData.java | 261 ++++ .../gallery3d/exif/ExifInvalidFormatException.java | 23 + .../android/gallery3d/exif/ExifOutputStream.java | 377 +++++ .../src/com/android/gallery3d/exif/ExifParser.java | 752 ++++++++++ .../src/com/android/gallery3d/exif/ExifReader.java | 74 + .../src/com/android/gallery3d/exif/ExifTag.java | 1441 ++++++++++++++++++++ .../src/com/android/gallery3d/exif/IfdData.java | 122 ++ .../src/com/android/gallery3d/exif/IfdId.java | 26 + .../src/com/android/gallery3d/exif/JpegHeader.java | 39 + .../gallery3d/exif/OrderedDataOutputStream.java | 52 + .../src/com/android/gallery3d/exif/Rational.java | 45 + .../src/com/android/gallery3d/exif/Util.java | 34 + 13 files changed, 3382 insertions(+) create mode 100644 gallerycommon/src/com/android/gallery3d/exif/CountedDataInputStream.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/ExifData.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/ExifInvalidFormatException.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/ExifOutputStream.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/ExifParser.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/ExifReader.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/ExifTag.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/IfdData.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/IfdId.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/JpegHeader.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/OrderedDataOutputStream.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/Rational.java create mode 100644 gallerycommon/src/com/android/gallery3d/exif/Util.java (limited to 'gallerycommon/src/com/android/gallery3d') diff --git a/gallerycommon/src/com/android/gallery3d/exif/CountedDataInputStream.java b/gallerycommon/src/com/android/gallery3d/exif/CountedDataInputStream.java new file mode 100644 index 000000000..dfd4a1a10 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/CountedDataInputStream.java @@ -0,0 +1,136 @@ +/* + * 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.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; + +class CountedDataInputStream extends FilterInputStream { + + private int mCount = 0; + + // allocate a byte buffer for a long value; + private final byte mByteArray[] = new byte[8]; + private final ByteBuffer mByteBuffer = ByteBuffer.wrap(mByteArray); + + protected CountedDataInputStream(InputStream in) { + super(in); + } + + public int getReadByteCount() { + return mCount; + } + + @Override + public int read(byte[] b) throws IOException { + int r = in.read(b); + mCount += (r >= 0) ? r : 0; + return r; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int r = in.read(b, off, len); + mCount += (r >= 0) ? r : 0; + return r; + } + + @Override + public int read() throws IOException { + int r = in.read(); + mCount += (r >= 0) ? 1 : 0; + return r; + } + + @Override + public long skip(long length) throws IOException { + long skip = in.skip(length); + mCount += skip; + return skip; + } + + public void skipOrThrow(long length) throws IOException { + if (skip(length) != length) throw new EOFException(); + } + + public void skipTo(long target) throws IOException { + long cur = mCount; + long diff = target - cur; + assert(diff >= 0); + skipOrThrow(diff); + } + + public void readOrThrow(byte[] b, int off, int len) throws IOException { + int r = read(b, off, len); + if (r != len) throw new EOFException(); + } + + public void readOrThrow(byte[] b) throws IOException { + readOrThrow(b, 0, b.length); + } + + public void setByteOrder(ByteOrder order) { + mByteBuffer.order(order); + } + + public ByteOrder getByteOrder() { + return mByteBuffer.order(); + } + + public short readShort() throws IOException { + readOrThrow(mByteArray, 0 ,2); + mByteBuffer.rewind(); + return mByteBuffer.getShort(); + } + + public int readUnsignedShort() throws IOException { + return readShort() & 0xffff; + } + + public int readInt() throws IOException { + readOrThrow(mByteArray, 0 , 4); + mByteBuffer.rewind(); + return mByteBuffer.getInt(); + } + + public long readUnsignedInt() throws IOException { + return readInt() & 0xffffffffL; + } + + public long readLong() throws IOException { + readOrThrow(mByteArray, 0 , 8); + mByteBuffer.rewind(); + return mByteBuffer.getLong(); + } + + public String readString(int n) throws IOException { + byte buf[] = new byte[n]; + readOrThrow(buf); + return new String(buf, "UTF8"); + } + + public String readString(int n, Charset charset) throws IOException { + byte buf[] = new byte[n]; + readOrThrow(buf); + return new String(buf, charset); + } +} \ No newline at end of file diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifData.java b/gallerycommon/src/com/android/gallery3d/exif/ExifData.java new file mode 100644 index 000000000..39eb57455 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/ExifData.java @@ -0,0 +1,261 @@ +/* + * 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.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * This class stores the EXIF header in IFDs according to the JPEG specification. + * It is the result produced by {@link ExifReader}. + * @see ExifReader + * @see IfdData + */ +public class ExifData { + private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT]; + private byte[] mThumbnail; + private ArrayList mStripBytes = new ArrayList(); + private final ByteOrder mByteOrder; + + public ExifData(ByteOrder order) { + mByteOrder = order; + } + + IfdData getIfdData(int ifdId) { + return mIfdDatas[ifdId]; + } + + /** + * Adds IFD data. If IFD data of the same type already exists, + * it will be replaced by the new data. + */ + void addIfdData(IfdData data) { + mIfdDatas[data.getId()] = data; + } + + /** + * Gets the compressed thumbnail. Returns null if there is no compressed thumbnail. + * + * @see #hasCompressedThumbnail() + */ + public byte[] getCompressedThumbnail() { + return mThumbnail; + } + + /** + * Sets the compressed thumbnail. + */ + public void setCompressedThumbnail(byte[] thumbnail) { + mThumbnail = thumbnail; + } + + /** + * Returns true it this header contains a compressed thumbnail. + */ + public boolean hasCompressedThumbnail() { + return mThumbnail != null; + } + + /** + * Adds an uncompressed strip. + */ + public void setStripBytes(int index, byte[] strip) { + if (index < mStripBytes.size()) { + mStripBytes.set(index, strip); + } else { + for (int i = mStripBytes.size(); i < index; i++) { + mStripBytes.add(null); + } + mStripBytes.add(strip); + } + } + + /** + * Gets the strip count. + */ + public int getStripCount() { + return mStripBytes.size(); + } + + /** + * Gets the strip at the specified index. + * @exceptions #IndexOutOfBoundException + */ + public byte[] getStrip(int index) { + return mStripBytes.get(index); + } + + /** + * Gets the byte order. + */ + public ByteOrder getByteOrder() { + return mByteOrder; + } + + /** + * Returns true if this header contains uncompressed strip of thumbnail. + */ + public boolean hasUncompressedStrip() { + return mStripBytes.size() != 0; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ExifData) { + ExifData data = (ExifData) obj; + if (data.mByteOrder != mByteOrder + || !Arrays.equals(data.mThumbnail, mThumbnail) + || data.mStripBytes.size() != mStripBytes.size()) return false; + + for (int i = 0; i < mStripBytes.size(); i++) { + if (!Arrays.equals(data.mStripBytes.get(i), mStripBytes.get(i))) return false; + } + + for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) { + if (!Util.equals(data.getIfdData(i), getIfdData(i))) return false; + } + return true; + } + return false; + } + + /** + * Adds {@link ExifTag#TAG_GPS_LATITUDE}, {@link ExifTag#TAG_GPS_LONGITUDE}, + * {@link ExifTag#TAG_GPS_LATITUDE_REF} and {@link ExifTag#TAG_GPS_LONGITUDE_REF} with the + * given latitude and longitude. + */ + public void addGpsTags(double latitude, double longitude) { + IfdData gpsIfd = getIfdData(IfdId.TYPE_IFD_GPS); + if (gpsIfd == null) { + gpsIfd = new IfdData(IfdId.TYPE_IFD_GPS); + addIfdData(gpsIfd); + } + ExifTag latTag = new ExifTag(ExifTag.TAG_GPS_LATITUDE, ExifTag.TYPE_RATIONAL, + 3, IfdId.TYPE_IFD_GPS); + ExifTag longTag = new ExifTag(ExifTag.TAG_GPS_LONGITUDE, ExifTag.TYPE_RATIONAL, + 3, IfdId.TYPE_IFD_GPS); + ExifTag latRefTag = new ExifTag(ExifTag.TAG_GPS_LATITUDE_REF, + ExifTag.TYPE_ASCII, 2, IfdId.TYPE_IFD_GPS); + ExifTag longRefTag = new ExifTag(ExifTag.TAG_GPS_LONGITUDE_REF, + ExifTag.TYPE_ASCII, 2, IfdId.TYPE_IFD_GPS); + latTag.setValue(toExifLatLong(latitude)); + longTag.setValue(toExifLatLong(longitude)); + latRefTag.setValue(latitude >= 0 + ? ExifTag.GpsLatitudeRef.NORTH + : ExifTag.GpsLatitudeRef.SOUTH); + longRefTag.setValue(longitude >= 0 + ? ExifTag.GpsLongitudeRef.EAST + : ExifTag.GpsLongitudeRef.WEST); + gpsIfd.setTag(latTag); + gpsIfd.setTag(longTag); + gpsIfd.setTag(latRefTag); + gpsIfd.setTag(longRefTag); + } + + private static Rational[] toExifLatLong(double value) { + // convert to the format dd/1 mm/1 ssss/100 + value = Math.abs(value); + int degrees = (int) value; + value = (value - degrees) * 60; + int minutes = (int) value; + value = (value - minutes) * 6000; + int seconds = (int) value; + return new Rational[] { + new Rational(degrees, 1), new Rational(minutes, 1), new Rational(seconds, 100)}; + } + + private IfdData getOrCreateIfdData(int ifdId) { + IfdData ifdData = mIfdDatas[ifdId]; + if (ifdData == null) { + ifdData = new IfdData(ifdId); + mIfdDatas[ifdId] = ifdData; + } + return ifdData; + } + + /** + * Gets the tag with the given tag ID. Returns null if the tag does not exist. For tags + * related to interoperability or thumbnail, call {@link #getInteroperabilityTag(short)} and + * {@link #getThumbnailTag(short)} respectively. + */ + public ExifTag getTag(short tagId) { + int ifdId = ExifTag.getIfdIdFromTagId(tagId); + IfdData ifdData = mIfdDatas[ifdId]; + return (ifdData == null) ? null : ifdData.getTag(tagId); + } + + /** + * Gets the thumbnail-related tag with the given tag ID. + */ + public ExifTag getThumbnailTag(short tagId) { + IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_1]; + return (ifdData == null) ? null : ifdData.getTag(tagId); + } + + /** + * Gets the interoperability-related tag with the given tag ID. + */ + public ExifTag getInteroperabilityTag(short tagId) { + IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_INTEROPERABILITY]; + return (ifdData == null) ? null : ifdData.getTag(tagId); + } + + /** + * Adds a tag with the given tag ID. The original tag will be replaced by the new tag. For tags + * related to interoperability or thumbnail, call {@link #addInteroperabilityTag(short)} or + * {@link #addThumbnailTag(short)} respectively. + * @exception IllegalArgumentException if the tag ID is invalid. + */ + public ExifTag addTag(short tagId) { + int ifdId = ExifTag.getIfdIdFromTagId(tagId); + IfdData ifdData = getOrCreateIfdData(ifdId); + ExifTag tag = ExifTag.buildTag(tagId); + ifdData.setTag(tag); + return tag; + } + + /** + * Adds a thumbnail-related tag with the given tag ID. The original tag will be replaced + * by the new tag. + * @exception IllegalArgumentException if the tag ID is invalid. + */ + public ExifTag addThumbnailTag(short tagId) { + IfdData ifdData = getOrCreateIfdData(IfdId.TYPE_IFD_1); + ExifTag tag = ExifTag.buildThumbnailTag(tagId); + ifdData.setTag(tag); + return tag; + } + + /** + * Adds an interoperability-related tag with the given tag ID. The original tag will be + * replaced by the new tag. + * @exception IllegalArgumentException if the tag ID is invalid. + */ + public ExifTag addInteroperabilityTag(short tagId) { + IfdData ifdData = getOrCreateIfdData(IfdId.TYPE_IFD_INTEROPERABILITY); + ExifTag tag = ExifTag.buildInteroperabilityTag(tagId); + ifdData.setTag(tag); + return tag; + } + + public void removeThumbnailData() { + mThumbnail = null; + mStripBytes.clear(); + mIfdDatas[IfdId.TYPE_IFD_1] = null; + } +} \ No newline at end of file diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifInvalidFormatException.java b/gallerycommon/src/com/android/gallery3d/exif/ExifInvalidFormatException.java new file mode 100644 index 000000000..bf923ec26 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/ExifInvalidFormatException.java @@ -0,0 +1,23 @@ +/* + * 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; + +public class ExifInvalidFormatException extends Exception { + public ExifInvalidFormatException(String meg) { + super(meg); + } +} \ No newline at end of file diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifOutputStream.java b/gallerycommon/src/com/android/gallery3d/exif/ExifOutputStream.java new file mode 100644 index 000000000..b8db8e34c --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/ExifOutputStream.java @@ -0,0 +1,377 @@ +/* + * 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_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 ExifData mExifData; + private int mState = STATE_SOI; + 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; + } + 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(); + assert(mBuffer.getShort() == JpegHeader.SOI); + 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() & 0xff) - 2; + mState = STATE_JPEG_DATA; + } else if (!JpegHeader.isSofMarker(marker)) { + out.write(mBuffer.array(), 0, 4); + mByteToCopy = (mBuffer.getShort() & 0xff) - 2; + } else { + out.write(mBuffer.array(), 0, 4); + mState = STATE_JPEG_DATA; + } + mBuffer.rewind(); + } + } + 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.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); + } + + 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 (tag.getDataSize() > 4) { + dataOutputStream.writeInt(tag.getOffset()); + } else { + 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) { + 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().length(); + for (int i = 0; i < remain; i++) { + dataOutputStream.write(0); + } + break; + case ExifTag.TYPE_LONG: + for (int i = 0, n = tag.getComponentCount(); i < n; i++) { + dataOutputStream.writeInt(tag.getLong(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: + byte[] buf = new byte[tag.getComponentCount()]; + tag.getBytes(buf); + dataOutputStream.write(buf); + break; + case ExifTag.TYPE_UNSIGNED_LONG: + for (int i = 0, n = tag.getComponentCount(); i < n; i++) { + dataOutputStream.writeInt((int) tag.getUnsignedLong(i)); + } + break; + case ExifTag.TYPE_UNSIGNED_SHORT: + for (int i = 0, n = tag.getComponentCount(); i < n; 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(); + 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.TAG_EXIF_IFD, + ExifTag.TYPE_UNSIGNED_LONG, 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.TAG_GPS_IFD, + ExifTag.TYPE_UNSIGNED_LONG, 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.TAG_INTEROPERABILITY_IFD, + ExifTag.TYPE_UNSIGNED_LONG, 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.TAG_JPEG_INTERCHANGE_FORMAT, + ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_1); + ifd1.setTag(offsetTag); + ExifTag lengthTag = new ExifTag(ExifTag.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, + ExifTag.TYPE_UNSIGNED_LONG, 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.TAG_STRIP_OFFSETS, + ExifTag.TYPE_UNSIGNED_LONG, stripCount, IfdId.TYPE_IFD_1); + ExifTag lengthTag = new ExifTag(ExifTag.TAG_STRIP_BYTE_COUNTS, + ExifTag.TYPE_UNSIGNED_LONG, 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.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.TAG_INTEROPERABILITY_IFD).setValue(offset); + offset = calculateOffsetOfIfd(interIfd, offset); + } + + IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS); + if (gpsIfd != null) { + ifd0.getTag(ExifTag.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.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.TAG_STRIP_OFFSETS).setValue(offsets); + } + return offset; + } +} \ No newline at end of file diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifParser.java b/gallerycommon/src/com/android/gallery3d/exif/ExifParser.java new file mode 100644 index 000000000..f1e52c5b3 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/ExifParser.java @@ -0,0 +1,752 @@ +/* + * 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.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.util.Map.Entry; +import java.util.TreeMap; + +/** + * This class provides a low-level EXIF parsing API. Given a JPEG format InputStream, the caller + * can request which IFD's to read via {@link #parse(InputStream, int)} with given options. + *

+ * Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the parser. + *

+ * void parse() {
+ *     ExifParser parser = ExifParser.parse(mImageInputStream,
+ *             ExifParser.OPTION_IFD_0 | ExifParser.OPTIONS_IFD_EXIF);
+ *     int event = parser.next();
+ *     while (event != ExifParser.EVENT_END) {
+ *         switch (event) {
+ *             case ExifParser.EVENT_START_OF_IFD:
+ *                 break;
+ *             case ExifParser.EVENT_NEW_TAG:
+ *                 ExifTag tag = parser.getTag();
+ *                 if (!tag.hasValue()) {
+ *                     parser.registerForTagValue(tag);
+ *                 } else {
+ *                     processTag(tag);
+ *                 }
+ *                 break;
+ *             case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+ *                 tag = parser.getTag();
+ *                 if (tag.getDataType() != ExifTag.TYPE_UNDEFINED) {
+ *                     processTag(tag);
+ *                 }
+ *                 break;
+ *         }
+ *         event = parser.next();
+ *     }
+ * }
+ *
+ * void processTag(ExifTag tag) {
+ *     // process the tag as you like.
+ * }
+ * 
+ */ +public class ExifParser { + /** + * When the parser reaches a new IFD area. Call + * {@link #getCurrentIfd()} to know which IFD we are in. + */ + public static final int EVENT_START_OF_IFD = 0; + /** + * When the parser reaches a new tag. Call {@link #getTag()}to get the + * corresponding tag. + */ + public static final int EVENT_NEW_TAG = 1; + /** + * When the parser reaches the value area of tag that is registered by + * {@link #registerForTagValue(ExifTag)} previously. Call + * {@link #getTag()} to get the corresponding tag. + */ + public static final int EVENT_VALUE_OF_REGISTERED_TAG = 2; + + /** + * When the parser reaches the compressed image area. + */ + public static final int EVENT_COMPRESSED_IMAGE = 3; + /** + * When the parser reaches the uncompressed image strip. + * Call {@link #getStripIndex()} to get the index of the strip. + * @see #getStripIndex() + * @see #getStripCount() + */ + public static final int EVENT_UNCOMPRESSED_STRIP = 4; + /** + * When there is nothing more to parse. + */ + public static final int EVENT_END = 5; + + /** + * Option bit to request to parse IFD0. + */ + public static final int OPTION_IFD_0 = 1 << 0; + /** + * Option bit to request to parse IFD1. + */ + public static final int OPTION_IFD_1 = 1 << 1; + /** + * Option bit to request to parse Exif-IFD. + */ + public static final int OPTION_IFD_EXIF = 1 << 2; + /** + * Option bit to request to parse GPS-IFD. + */ + public static final int OPTION_IFD_GPS = 1 << 3; + /** + * Option bit to request to parse Interoperability-IFD. + */ + public static final int OPTION_IFD_INTEROPERABILITY = 1 << 4; + /** + * Option bit to request to parse thumbnail. + */ + public static final int OPTION_THUMBNAIL = 1 << 5; + + private static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif" + private static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1 + + // TIFF header + private static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II" + private static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM" + private static final short TIFF_HEADER_TAIL = 0x002A; + + private static final int TAG_SIZE = 12; + private static final int OFFSET_SIZE = 2; + + private final CountedDataInputStream mTiffStream; + private final int mOptions; + private int mIfdStartOffset = 0; + private int mNumOfTagInIfd = 0; + private int mIfdType; + private ExifTag mTag; + private ImageEvent mImageEvent; + private int mStripCount; + private ExifTag mStripSizeTag; + private ExifTag mJpegSizeTag; + private boolean mNeedToParseOffsetsInCurrentIfd; + private boolean mContainExifData = false; + + private final TreeMap mCorrespondingEvent = new TreeMap(); + + private boolean isIfdRequested(int ifdType) { + switch (ifdType) { + case IfdId.TYPE_IFD_0: + return (mOptions & OPTION_IFD_0) != 0; + case IfdId.TYPE_IFD_1: + return (mOptions & OPTION_IFD_1) != 0; + case IfdId.TYPE_IFD_EXIF: + return (mOptions & OPTION_IFD_EXIF) != 0; + case IfdId.TYPE_IFD_GPS: + return (mOptions & OPTION_IFD_GPS) != 0; + case IfdId.TYPE_IFD_INTEROPERABILITY: + return (mOptions & OPTION_IFD_INTEROPERABILITY) != 0; + } + return false; + } + + private boolean isThumbnailRequested() { + return (mOptions & OPTION_THUMBNAIL) != 0; + } + + private ExifParser(InputStream inputStream, int options) + throws IOException, ExifInvalidFormatException { + mContainExifData = seekTiffData(inputStream); + mTiffStream = new CountedDataInputStream(inputStream); + mOptions = options; + if (!mContainExifData) return; + if (mTiffStream.getReadByteCount() == 0) { + parseTiffHeader(); + long offset = mTiffStream.readUnsignedInt(); + registerIfd(IfdId.TYPE_IFD_0, offset); + } + } + + /** + * Parses the the given InputStream with the given options + * @exception IOException + * @exception ExifInvalidFormatException + */ + public static ExifParser parse(InputStream inputStream, int options) + throws IOException, ExifInvalidFormatException { + return new ExifParser(inputStream, options); + } + + /** + * Parses the the given InputStream with default options; that is, every IFD and thumbnaill + * will be parsed. + * @exception IOException + * @exception ExifInvalidFormatException + * @see #parse(InputStream, int) + */ + public static ExifParser parse(InputStream inputStream) + throws IOException, ExifInvalidFormatException { + return new ExifParser(inputStream, OPTION_IFD_0 | OPTION_IFD_1 + | OPTION_IFD_EXIF | OPTION_IFD_GPS | OPTION_IFD_INTEROPERABILITY + | OPTION_THUMBNAIL); + } + + /** + * Moves the parser forward and returns the next parsing event + * + * @exception IOException + * @exception ExifInvalidFormatException + * @see #EVENT_START_OF_IFD + * @see #EVENT_NEW_TAG + * @see #EVENT_VALUE_OF_REGISTERED_TAG + * @see #EVENT_COMPRESSED_IMAGE + * @see #EVENT_UNCOMPRESSED_STRIP + * @see #EVENT_END + */ + public int next() throws IOException, ExifInvalidFormatException { + if (!mContainExifData) { + return EVENT_END; + } + int offset = mTiffStream.getReadByteCount(); + int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd; + if (offset < endOfTags) { + mTag = readTag(); + if (mNeedToParseOffsetsInCurrentIfd) { + checkOffsetOrImageTag(mTag); + } + return EVENT_NEW_TAG; + } else if (offset == endOfTags) { + long ifdOffset = readUnsignedLong(); + // There is a link to ifd1 at the end of ifd0 + if (mIfdType == IfdId.TYPE_IFD_0) { + if (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested()) { + if (ifdOffset != 0) { + registerIfd(IfdId.TYPE_IFD_1, ifdOffset); + } + } + } else { + if (ifdOffset != 0) { + throw new ExifInvalidFormatException("Invalid link to next IFD"); + } + } + } + while(mCorrespondingEvent.size() != 0) { + Entry entry = mCorrespondingEvent.pollFirstEntry(); + Object event = entry.getValue(); + skipTo(entry.getKey()); + if (event instanceof IfdEvent) { + mIfdType = ((IfdEvent) event).ifd; + mNumOfTagInIfd = mTiffStream.readUnsignedShort(); + mIfdStartOffset = entry.getKey(); + mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd(); + if (((IfdEvent) event).isRequested) { + return EVENT_START_OF_IFD; + } else { + skipRemainingTagsInCurrentIfd(); + } + } else if (event instanceof ImageEvent) { + mImageEvent = (ImageEvent) event; + return mImageEvent.type; + } else { + ExifTagEvent tagEvent = (ExifTagEvent) event; + mTag = tagEvent.tag; + if (mTag.getDataType() != ExifTag.TYPE_UNDEFINED) { + readFullTagValue(mTag); + checkOffsetOrImageTag(mTag); + } + if (tagEvent.isRequested) { + return EVENT_VALUE_OF_REGISTERED_TAG; + } + } + } + return EVENT_END; + } + + /** + * Skips the tags area of current IFD, if the parser is not in the tag area, nothing will + * happen. + * + * @throws IOException + * @throws ExifInvalidFormatException + */ + public void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException { + int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd; + int offset = mTiffStream.getReadByteCount(); + if (offset > endOfTags) return; + if (mNeedToParseOffsetsInCurrentIfd) { + while (offset < endOfTags) { + mTag = readTag(); + checkOffsetOrImageTag(mTag); + offset += TAG_SIZE; + } + } else { + skipTo(endOfTags); + } + long ifdOffset = readUnsignedLong(); + // For ifd0, there is a link to ifd1 in the end of all tags + if (mIfdType == IfdId.TYPE_IFD_0 + && (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested())) { + if (ifdOffset > 0) { + registerIfd(IfdId.TYPE_IFD_1, ifdOffset); + } + } + } + + private boolean needToParseOffsetsInCurrentIfd() { + switch (mIfdType) { + case IfdId.TYPE_IFD_0: + return isIfdRequested(IfdId.TYPE_IFD_EXIF) || isIfdRequested(IfdId.TYPE_IFD_GPS) + || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY); + case IfdId.TYPE_IFD_1: + return isThumbnailRequested(); + case IfdId.TYPE_IFD_EXIF: + // The offset to interoperability IFD is located in Exif IFD + return isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY); + default: + return false; + } + } + + /** + * If {@link #next()} return {@link #EVENT_NEW_TAG} or {@link #EVENT_VALUE_OF_REGISTERED_TAG}, + * call this function to get the corresponding tag. + *

+ * + * For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size of the value is + * greater than 4 bytes. One should call {@link ExifTag#hasValue()} to check if the tag + * contains value. + * If there is no value,call {@link #registerForTagValue(ExifTag)} to have the parser emit + * {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area pointed by the offset. + * + *

+ * When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the tag will have + * already been read except for tags of undefined type. For tags of undefined type, call + * one of the read methods to get the value. + * + * @see #registerForTagValue(ExifTag) + * @see #read(byte[]) + * @see #read(byte[], int, int) + * @see #readLong() + * @see #readRational() + * @see #readShort() + * @see #readString(int) + * @see #readString(int, Charset) + */ + public ExifTag getTag() { + return mTag; + } + + /** + * Gets number of tags in the current IFD area. + */ + public int getTagCountInCurrentIfd() { + return mNumOfTagInIfd; + } + + /** + * Gets the ID of current IFD. + * + * @see IfdId#TYPE_IFD_0 + * @see IfdId#TYPE_IFD_1 + * @see IfdId#TYPE_IFD_GPS + * @see IfdId#TYPE_IFD_INTEROPERABILITY + * @see IfdId#TYPE_IFD_EXIF + */ + public int getCurrentIfd() { + return mIfdType; + } + + /** + * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, + * call this function to get the index of this strip. + * @see #getStripCount() + */ + public int getStripIndex() { + return mImageEvent.stripIndex; + } + + /** + * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to get the number + * of strip data. + * @see #getStripIndex() + */ + public int getStripCount() { + return mStripCount; + } + + /** + * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to get the strip size. + */ + public int getStripSize() { + if (mStripSizeTag == null) return 0; + if (mStripSizeTag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) { + return mStripSizeTag.getUnsignedShort(mImageEvent.stripIndex); + } else { + // Cast unsigned int to int since the strip size is always smaller + // than the size of APP1 (65536) + return (int) mStripSizeTag.getUnsignedLong(mImageEvent.stripIndex); + } + } + + /** + * When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get the image data + * size. + */ + public int getCompressedImageSize() { + if (mJpegSizeTag == null) return 0; + // Cast unsigned int to int since the thumbnail is always smaller + // than the size of APP1 (65536) + return (int) mJpegSizeTag.getUnsignedLong(0); + } + + private void skipTo(int offset) throws IOException { + mTiffStream.skipTo(offset); + while (!mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) { + mCorrespondingEvent.pollFirstEntry(); + } + } + + /** + * When getting {@link #EVENT_NEW_TAG} in the tag area of IFD, + * the tag may not contain the value if the size of the value is greater than 4 bytes. + * When the value is not available here, call this method so that the parser will emit + * {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area where the value is located. + + * @see #EVENT_VALUE_OF_REGISTERED_TAG + */ + public void registerForTagValue(ExifTag tag) { + mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true)); + } + + private void registerIfd(int ifdType, long offset) { + // Cast unsigned int to int since the offset is always smaller + // than the size of APP1 (65536) + mCorrespondingEvent.put((int) offset, new IfdEvent(ifdType, isIfdRequested(ifdType))); + } + + private void registerCompressedImage(long offset) { + mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_COMPRESSED_IMAGE)); + } + + private void registerUncompressedStrip(int stripIndex, long offset) { + mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_UNCOMPRESSED_STRIP + , stripIndex)); + } + + private ExifTag readTag() throws IOException, ExifInvalidFormatException { + short tagId = mTiffStream.readShort(); + short dataFormat = mTiffStream.readShort(); + long numOfComp = mTiffStream.readUnsignedInt(); + if (numOfComp > Integer.MAX_VALUE) { + throw new ExifInvalidFormatException( + "Number of component is larger then Integer.MAX_VALUE"); + } + ExifTag tag = new ExifTag(tagId, dataFormat, (int) numOfComp, mIfdType); + int dataSize = tag.getDataSize(); + if (dataSize > 4) { + long offset = mTiffStream.readUnsignedInt(); + if (offset > Integer.MAX_VALUE) { + throw new ExifInvalidFormatException( + "offset is larger then Integer.MAX_VALUE"); + } + tag.setOffset((int) offset); + } else { + readFullTagValue(tag); + mTiffStream.skip(4 - dataSize); + } + return tag; + } + + /** + * Check the tag, if the tag is one of the offset tag that points to the IFD or image the + * caller is interested in, register the IFD or image. + */ + private void checkOffsetOrImageTag(ExifTag tag) { + switch (tag.getTagId()) { + case ExifTag.TAG_EXIF_IFD: + if (isIfdRequested(IfdId.TYPE_IFD_EXIF) + || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) { + registerIfd(IfdId.TYPE_IFD_EXIF, tag.getUnsignedLong(0)); + } + break; + case ExifTag.TAG_GPS_IFD: + if (isIfdRequested(IfdId.TYPE_IFD_GPS)) { + registerIfd(IfdId.TYPE_IFD_GPS, tag.getUnsignedLong(0)); + } + break; + case ExifTag.TAG_INTEROPERABILITY_IFD: + if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) { + registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getUnsignedLong(0)); + } + break; + case ExifTag.TAG_JPEG_INTERCHANGE_FORMAT: + if (isThumbnailRequested()) { + registerCompressedImage(tag.getUnsignedLong(0)); + } + break; + case ExifTag.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH: + if (isThumbnailRequested()) { + mJpegSizeTag = tag; + } + break; + case ExifTag.TAG_STRIP_OFFSETS: + if (isThumbnailRequested()) { + if (tag.hasValue()) { + for (int i = 0; i < tag.getComponentCount(); i++) { + if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) { + registerUncompressedStrip(i, tag.getUnsignedShort(i)); + } else { + registerUncompressedStrip(i, tag.getUnsignedLong(i)); + } + } + } else { + mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false)); + } + } + break; + case ExifTag.TAG_STRIP_BYTE_COUNTS: + if (isThumbnailRequested()) { + if (tag.hasValue()) { + mStripSizeTag = tag; + } + } + break; + } + } + + private void readFullTagValue(ExifTag tag) throws IOException { + switch(tag.getDataType()) { + case ExifTag.TYPE_UNSIGNED_BYTE: + case ExifTag.TYPE_UNDEFINED: + { + byte buf[] = new byte[tag.getComponentCount()]; + read(buf); + tag.setValue(buf); + } + break; + case ExifTag.TYPE_ASCII: + tag.setValue(readString(tag.getComponentCount())); + break; + case ExifTag.TYPE_UNSIGNED_LONG: + { + long value[] = new long[tag.getComponentCount()]; + for (int i = 0, n = value.length; i < n; i++) { + value[i] = readUnsignedLong(); + } + tag.setValue(value); + } + break; + case ExifTag.TYPE_UNSIGNED_RATIONAL: + { + Rational value[] = new Rational[tag.getComponentCount()]; + for (int i = 0, n = value.length; i < n; i++) { + value[i] = readUnsignedRational(); + } + tag.setValue(value); + } + break; + case ExifTag.TYPE_UNSIGNED_SHORT: + { + int value[] = new int[tag.getComponentCount()]; + for (int i = 0, n = value.length; i < n; i++) { + value[i] = readUnsignedShort(); + } + tag.setValue(value); + } + break; + case ExifTag.TYPE_LONG: + { + int value[] = new int[tag.getComponentCount()]; + for (int i = 0, n = value.length; i < n; i++) { + value[i] = readLong(); + } + tag.setValue(value); + } + break; + case ExifTag.TYPE_RATIONAL: + { + Rational value[] = new Rational[tag.getComponentCount()]; + for (int i = 0, n = value.length; i < n; i++) { + value[i] = readRational(); + } + tag.setValue(value); + } + break; + } + } + + private void parseTiffHeader() throws IOException, + ExifInvalidFormatException { + short byteOrder = mTiffStream.readShort(); + ByteOrder order; + if (LITTLE_ENDIAN_TAG == byteOrder) { + mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } else if (BIG_ENDIAN_TAG == byteOrder) { + mTiffStream.setByteOrder(ByteOrder.BIG_ENDIAN); + } else { + throw new ExifInvalidFormatException("Invalid TIFF header"); + } + + if (mTiffStream.readShort() != TIFF_HEADER_TAIL) { + throw new ExifInvalidFormatException("Invalid TIFF header"); + } + } + + private boolean seekTiffData(InputStream inputStream) throws IOException, + ExifInvalidFormatException { + DataInputStream dataStream = new DataInputStream(inputStream); + + // SOI and APP1 + if (dataStream.readShort() != JpegHeader.SOI) { + throw new ExifInvalidFormatException("Invalid JPEG format"); + } + + short marker = dataStream.readShort(); + while(marker != JpegHeader.APP1 && marker != JpegHeader.EOI + && !JpegHeader.isSofMarker(marker)) { + int length = dataStream.readUnsignedShort(); + if ((length - 2) != dataStream.skip(length - 2)) { + throw new EOFException(); + } + marker = dataStream.readShort(); + } + + if (marker != JpegHeader.APP1) return false; // No APP1 segment + + // APP1 length, it's not used for us + dataStream.readShort(); + + // Exif header + return (dataStream.readInt() == EXIF_HEADER + && dataStream.readShort() == EXIF_HEADER_TAIL); + } + + /** + * Reads bytes from the InputStream. + */ + public int read(byte[] buffer, int offset, int length) throws IOException { + return mTiffStream.read(buffer, offset, length); + } + + /** + * Equivalent to read(buffer, 0, buffer.length). + */ + public int read(byte[] buffer) throws IOException { + return mTiffStream.read(buffer); + } + + /** + * Reads a String from the InputStream with UTF8 charset. + * This is used for reading values of type {@link ExifTag#TYPE_ASCII}. + */ + public String readString(int n) throws IOException { + if (n > 0) { + byte[] buf = new byte[n]; + mTiffStream.readOrThrow(buf); + return new String(buf, 0, n - 1, "UTF8"); + } else { + return ""; + } + } + + /** + * Reads a String from the InputStream with the given charset. + * This is used for reading values of type {@link ExifTag#TYPE_ASCII}. + */ + public String readString(int n, Charset charset) throws IOException { + byte[] buf = new byte[n]; + mTiffStream.readOrThrow(buf); + return new String(buf, 0, n - 1, charset); + } + + /** + * Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the InputStream. + */ + public int readUnsignedShort() throws IOException { + return mTiffStream.readShort() & 0xffff; + } + + /** + * Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the InputStream. + */ + public long readUnsignedLong() throws IOException { + return readLong() & 0xffffffffL; + } + + /** + * Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the InputStream. + */ + public Rational readUnsignedRational() throws IOException { + long nomi = readUnsignedLong(); + long denomi = readUnsignedLong(); + return new Rational(nomi, denomi); + } + + /** + * Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream. + */ + public int readLong() throws IOException { + return mTiffStream.readInt(); + } + + /** + * Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream. + */ + public Rational readRational() throws IOException { + int nomi = readLong(); + int denomi = readLong(); + return new Rational(nomi, denomi); + } + + private static class ImageEvent { + int stripIndex; + int type; + ImageEvent(int type) { + this.stripIndex = 0; + this.type = type; + } + ImageEvent(int type, int stripIndex) { + this.type = type; + this.stripIndex = stripIndex; + } + } + + private static class IfdEvent { + int ifd; + boolean isRequested; + IfdEvent(int ifd, boolean isInterestedIfd) { + this.ifd = ifd; + this.isRequested = isInterestedIfd; + } + } + + private static class ExifTagEvent { + ExifTag tag; + boolean isRequested; + ExifTagEvent(ExifTag tag, boolean isRequireByUser) { + this.tag = tag; + this.isRequested = isRequireByUser; + } + } + + /** + * Gets the byte order of the current InputStream. + */ + public ByteOrder getByteOrder() { + return mTiffStream.getByteOrder(); + } +} \ No newline at end of file diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifReader.java b/gallerycommon/src/com/android/gallery3d/exif/ExifReader.java new file mode 100644 index 000000000..d8083b2dd --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/ExifReader.java @@ -0,0 +1,74 @@ +/* + * 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.IOException; +import java.io.InputStream; + +/** + * This class reads the EXIF header of a JPEG file and stores it in {@link ExifData}. + */ +public class ExifReader { + /** + * Parses the inputStream and and returns the EXIF data in an {@link ExifData}. + * @throws ExifInvalidFormatException + * @throws IOException + */ + public ExifData read(InputStream inputStream) throws ExifInvalidFormatException, + IOException { + ExifParser parser = ExifParser.parse(inputStream); + ExifData exifData = new ExifData(parser.getByteOrder()); + + int event = parser.next(); + while (event != ExifParser.EVENT_END) { + switch (event) { + case ExifParser.EVENT_START_OF_IFD: + exifData.addIfdData(new IfdData(parser.getCurrentIfd())); + break; + case ExifParser.EVENT_NEW_TAG: + ExifTag tag = parser.getTag(); + if (!tag.hasValue()) { + parser.registerForTagValue(tag); + } else { + exifData.getIfdData(tag.getIfd()).setTag(tag); + } + break; + case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG: + tag = parser.getTag(); + if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) { + byte[] buf = new byte[tag.getComponentCount()]; + parser.read(buf); + tag.setValue(buf); + } + exifData.getIfdData(tag.getIfd()).setTag(tag); + break; + case ExifParser.EVENT_COMPRESSED_IMAGE: + byte buf[] = new byte[parser.getCompressedImageSize()]; + parser.read(buf); + exifData.setCompressedThumbnail(buf); + break; + case ExifParser.EVENT_UNCOMPRESSED_STRIP: + buf = new byte[parser.getStripSize()]; + parser.read(buf); + exifData.setStripBytes(parser.getStripIndex(), buf); + break; + } + event = parser.next(); + } + return exifData; + } +} \ No newline at end of file diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifTag.java b/gallerycommon/src/com/android/gallery3d/exif/ExifTag.java new file mode 100644 index 000000000..49cb6edbc --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/ExifTag.java @@ -0,0 +1,1441 @@ +/* + * 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.SparseArray; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; + +/** + * This class stores information of an EXIF tag. + * @see ExifParser + * @see ExifReader + * @see IfdData + * @see ExifData + */ +public class ExifTag { + // Tiff Tags + public static final short TAG_IMAGE_WIDTH = 0x100; + /* + * The height of the image. + */ + public static final short TAG_IMAGE_LENGTH = 0x101; + public static final short TAG_BITS_PER_SAMPLE = 0x102; + public static final short TAG_COMPRESSION = 0x103; + public static final short TAG_PHOTOMETRIC_INTERPRETATION = 0x106; + public static final short TAG_IMAGE_DESCRIPTION = 0x10E; + public static final short TAG_MAKE = 0x10F; + public static final short TAG_MODEL = 0x110; + public static final short TAG_STRIP_OFFSETS = 0x111; + public static final short TAG_ORIENTATION = 0x112; + public static final short TAG_SAMPLES_PER_PIXEL = 0x115; + public static final short TAG_ROWS_PER_STRIP = 0x116; + public static final short TAG_STRIP_BYTE_COUNTS = 0x117; + public static final short TAG_X_RESOLUTION = 0x11A; + public static final short TAG_Y_RESOLUTION = 0x11B; + public static final short TAG_PLANAR_CONFIGURATION = 0x11C; + public static final short TAG_RESOLUTION_UNIT = 0x128; + public static final short TAG_TRANSFER_FUNCTION = 0x12D; + public static final short TAG_SOFTWARE = 0x131; + public static final short TAG_DATE_TIME = 0x132; + public static final short TAG_ARTIST = 0x13B; + public static final short TAG_WHITE_POINT = 0x13E; + public static final short TAG_PRIMARY_CHROMATICITIES = 0x13F; + public static final short TAG_JPEG_INTERCHANGE_FORMAT = 0x201; + public static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 0x202; + public static final short TAG_Y_CB_CR_COEFFICIENTS = 0x211; + public static final short TAG_Y_CB_CR_SUB_SAMPLING = 0x212; + public static final short TAG_Y_CB_CR_POSITIONING = 0x213; + public static final short TAG_REFERENCE_BLACK_WHITE = 0x214; + public static final short TAG_COPYRIGHT = (short) 0x8298; + public static final short TAG_EXIF_IFD = (short) 0x8769; + public static final short TAG_GPS_IFD = (short) 0x8825; + + // Exif Tags + public static final short TAG_EXPOSURE_TIME = (short) 0x829A; + public static final short TAG_F_NUMBER = (short) 0x829D; + public static final short TAG_EXPOSURE_PROGRAM = (short) 0x8822; + public static final short TAG_SPECTRAL_SENSITIVITY = (short) 0x8824; + public static final short TAG_ISO_SPEED_RATINGS = (short) 0x8827; + public static final short TAG_OECF = (short) 0x8828; + public static final short TAG_EXIF_VERSION = (short) 0x9000; + public static final short TAG_DATE_TIME_ORIGINAL = (short) 0x9003; + public static final short TAG_DATE_TIME_DIGITIZED = (short) 0x9004; + public static final short TAG_COMPONENTS_CONFIGURATION = (short) 0x9101; + public static final short TAG_COMPRESSED_BITS_PER_PIXEL = (short) 0x9102; + public static final short TAG_SHUTTER_SPEED_VALUE = (short) 0x9201; + public static final short TAG_APERTURE_VALUE = (short) 0x9202; + public static final short TAG_BRIGHTNESS_VALUE = (short) 0x9203; + public static final short TAG_EXPOSURE_BIAS_VALUE = (short) 0x9204; + public static final short TAG_MAX_APERTURE_VALUE = (short) 0x9205; + public static final short TAG_SUBJECT_DISTANCE = (short) 0x9206; + public static final short TAG_METERING_MODE = (short) 0x9207; + public static final short TAG_LIGHT_SOURCE = (short) 0x9208; + public static final short TAG_FLASH = (short) 0x9209; + public static final short TAG_FOCAL_LENGTH = (short) 0x920A; + public static final short TAG_SUBJECT_AREA = (short) 0x9214; + public static final short TAG_MAKER_NOTE = (short) 0x927C; + public static final short TAG_USER_COMMENT = (short) 0x9286; + public static final short TAG_SUB_SEC_TIME = (short) 0x9290; + public static final short TAG_SUB_SEC_TIME_ORIGINAL = (short) 0x9291; + public static final short TAG_SUB_SEC_TIME_DIGITIZED = (short) 0x9292; + public static final short TAG_FLASHPIX_VERSION = (short) 0xA000; + public static final short TAG_COLOR_SPACE = (short) 0xA001; + public static final short TAG_PIXEL_X_DIMENSION = (short) 0xA002; + public static final short TAG_PIXEL_Y_DIMENSION = (short) 0xA003; + public static final short TAG_RELATED_SOUND_FILE = (short) 0xA004; + public static final short TAG_INTEROPERABILITY_IFD = (short) 0xA005; + public static final short TAG_FLASH_ENERGY = (short) 0xA20B; + public static final short TAG_SPATIAL_FREQUENCY_RESPONSE = (short) 0xA20C; + public static final short TAG_FOCAL_PLANE_X_RESOLUTION = (short) 0xA20E; + public static final short TAG_FOCAL_PLANE_Y_RESOLUTION = (short) 0xA20F; + public static final short TAG_FOCAL_PLANE_RESOLUTION_UNIT = (short) 0xA210; + public static final short TAG_SUBJECT_LOCATION = (short) 0xA214; + public static final short TAG_EXPOSURE_INDEX = (short) 0xA215; + public static final short TAG_SENSING_METHOD = (short) 0xA217; + public static final short TAG_FILE_SOURCE = (short) 0xA300; + public static final short TAG_SCENE_TYPE = (short) 0xA301; + public static final short TAG_CFA_PATTERN = (short) 0xA302; + public static final short TAG_CUSTOM_RENDERED = (short) 0xA401; + public static final short TAG_EXPOSURE_MODE = (short) 0xA402; + public static final short TAG_WHITE_BALANCE = (short) 0xA403; + public static final short TAG_DIGITAL_ZOOM_RATIO = (short) 0xA404; + public static final short TAG_FOCAL_LENGTH_IN_35_MM_FILE = (short) 0xA405; + public static final short TAG_SCENE_CAPTURE_TYPE = (short) 0xA406; + public static final short TAG_GAIN_CONTROL = (short) 0xA407; + public static final short TAG_CONTRAST = (short) 0xA408; + public static final short TAG_SATURATION = (short) 0xA409; + public static final short TAG_SHARPNESS = (short) 0xA40A; + public static final short TAG_DEVICE_SETTING_DESCRIPTION = (short) 0xA40B; + public static final short TAG_SUBJECT_DISTANCE_RANGE = (short) 0xA40C; + public static final short TAG_IMAGE_UNIQUE_ID = (short) 0xA420; + + // GPS tags + public static final short TAG_GPS_VERSION_ID = 0; + public static final short TAG_GPS_LATITUDE_REF = 1; + public static final short TAG_GPS_LATITUDE = 2; + public static final short TAG_GPS_LONGITUDE_REF = 3; + public static final short TAG_GPS_LONGITUDE = 4; + public static final short TAG_GPS_ALTITUDE_REF = 5; + public static final short TAG_GPS_ALTITUDE = 6; + public static final short TAG_GPS_TIME_STAMP = 7; + public static final short TAG_GPS_SATTELLITES = 8; + public static final short TAG_GPS_STATUS = 9; + public static final short TAG_GPS_MEASURE_MODE = 10; + public static final short TAG_GPS_DOP = 11; + public static final short TAG_GPS_SPEED_REF = 12; + public static final short TAG_GPS_SPEED = 13; + public static final short TAG_GPS_TRACK_REF = 14; + public static final short TAG_GPS_TRACK = 15; + public static final short TAG_GPS_IMG_DIRECTION_REF = 16; + public static final short TAG_GPS_IMG_DIRECTION = 17; + public static final short TAG_GPS_MAP_DATUM = 18; + public static final short TAG_GPS_DEST_LATITUDE_REF = 19; + public static final short TAG_GPS_DEST_LATITUDE = 20; + public static final short TAG_GPS_DEST_LONGITUDE_REF = 21; + public static final short TAG_GPS_DEST_LONGITUDE = 22; + public static final short TAG_GPS_DEST_BEARING_REF = 23; + public static final short TAG_GPS_DEST_BEARING = 24; + public static final short TAG_GPS_DEST_DISTANCE_REF = 25; + public static final short TAG_GPS_DEST_DISTANCE = 26; + public static final short TAG_GPS_PROCESSING_METHOD = 27; + public static final short TAG_GPS_AREA_INFORMATION = 28; + public static final short TAG_GPS_DATA_STAMP = 29; + public static final short TAG_GPS_DIFFERENTIAL = 30; + + // Interoperability tag + public static final short TAG_INTEROPERABILITY_INDEX = 1; + + /** + * Constants for {@link #TAG_ORIENTATION} + */ + public static interface Orientation { + public static final short TOP_LEFT = 1; + public static final short TOP_RIGHT = 2; + public static final short BOTTOM_LEFT = 3; + public static final short BOTTOM_RIGHT = 4; + public static final short LEFT_TOP = 5; + public static final short RIGHT_TOP = 6; + public static final short LEFT_BOTTOM = 7; + public static final short RIGHT_BOTTOM = 8; + } + + /** + * Constants for {@link #TAG_Y_CB_CR_POSITIONING} + */ + public static interface YCbCrPositioning { + public static final short CENTERED = 1; + public static final short CO_SITED = 2; + } + + /** + * Constants for {@link #TAG_COMPRESSION} + */ + public static interface Compression { + public static final short UNCOMPRESSION = 1; + public static final short JPEG = 6; + } + + /** + * Constants for {@link #TAG_RESOLUTION_UNIT} + */ + public static interface ResolutionUnit { + public static final short INCHES = 2; + public static final short CENTIMETERS = 3; + } + + /** + * Constants for {@link #TAG_PHOTOMETRIC_INTERPRETATION} + */ + public static interface PhotometricInterpretation { + public static final short RGB = 2; + public static final short YCBCR = 6; + } + + /** + * Constants for {@link #TAG_PLANAR_CONFIGURATION} + */ + public static interface PlanarConfiguration { + public static final short CHUNKY = 1; + public static final short PLANAR = 2; + } + + /** + * Constants for {@link #TAG_EXPOSURE_PROGRAM} + */ + public static interface ExposureProgram { + public static final short NOT_DEFINED = 0; + public static final short MANUAL = 1; + public static final short NORMAL_PROGRAM = 2; + public static final short APERTURE_PRIORITY = 3; + public static final short SHUTTER_PRIORITY = 4; + public static final short CREATIVE_PROGRAM = 5; + public static final short ACTION_PROGRAM = 6; + public static final short PROTRAIT_MODE = 7; + public static final short LANDSCAPE_MODE = 8; + } + + /** + * Constants for {@link #TAG_METERING_MODE} + */ + public static interface MeteringMode { + public static final short UNKNOWN = 0; + public static final short AVERAGE = 1; + public static final short CENTER_WEIGHTED_AVERAGE = 2; + public static final short SPOT = 3; + public static final short MULTISPOT = 4; + public static final short PATTERN = 5; + public static final short PARTAIL = 6; + public static final short OTHER = 255; + } + + /** + * Constants for {@link #TAG_FLASH} As the definition in Jeita EXIF 2.2 standard, we can + * treat this constant as bitwise flag. + *

+ * e.g. + *

+ * short flash = FIRED | RETURN_STROBE_RETURN_LIGHT_DETECTED | MODE_AUTO_MODE + */ + public static interface Flash { + // LSB + public static final short DID_NOT_FIRED = 0; + public static final short FIRED = 1; + // 1st~2nd bits + public static final short RETURN_NO_STROBE_RETURN_DETECTION_FUNCTION = 0 << 1; + public static final short RETURN_STROBE_RETURN_LIGHT_NOT_DETECTED = 2 << 1; + public static final short RETURN_STROBE_RETURN_LIGHT_DETECTED = 3 << 1; + // 3rd~4th bits + public static final short MODE_UNKNOWN = 0 << 3; + public static final short MODE_COMPULSORY_FLASH_FIRING = 1 << 3; + public static final short MODE_COMPULSORY_FLASH_SUPPRESSION = 2 << 3; + public static final short MODE_AUTO_MODE = 3 << 3; + // 5th bit + public static final short FUNCTION_PRESENT = 0 << 5; + public static final short FUNCTION_NO_FUNCTION = 1 << 5; + // 6th bit + public static final short RED_EYE_REDUCTION_NO_OR_UNKNOWN = 0 << 6; + public static final short RED_EYE_REDUCTION_SUPPORT = 1 << 6; + } + + /** + * Constants for {@link #TAG_COLOR_SPACE} + */ + public static interface ColorSpace { + public static final short SRGB = 1; + public static final short UNCALIBRATED = (short) 0xFFFF; + } + + /** + * Constants for {@link #TAG_EXPOSURE_MODE} + */ + public static interface ExposureMode { + public static final short AUTO_EXPOSURE = 0; + public static final short MANUAL_EXPOSURE = 1; + public static final short AUTO_BRACKET = 2; + } + + /** + * Constants for {@link #TAG_WHITE_BALANCE} + */ + public static interface WhiteBalance { + public static final short AUTO = 0; + public static final short MANUAL = 1; + } + + /** + * Constants for {@link #TAG_SCENE_CAPTURE_TYPE} + */ + public static interface SceneCapture { + public static final short STANDARD = 0; + public static final short LANDSCAPE = 1; + public static final short PROTRAIT = 2; + public static final short NIGHT_SCENE = 3; + } + + /** + * Constants for {@link #TAG_COMPONENTS_CONFIGURATION} + */ + public static interface ComponentsConfiguration { + public static final short NOT_EXIST = 0; + public static final short Y = 1; + public static final short CB = 2; + public static final short CR = 3; + public static final short R = 4; + public static final short G = 5; + public static final short B = 6; + } + + /** + * Constants for {@link #TAG_LIGHT_SOURCE} + */ + public static interface LightSource { + public static final short UNKNOWN = 0; + public static final short DAYLIGHT = 1; + public static final short FLUORESCENT = 2; + public static final short TUNGSTEN = 3; + public static final short FLASH = 4; + public static final short FINE_WEATHER = 9; + public static final short CLOUDY_WEATHER = 10; + public static final short SHADE = 11; + public static final short DAYLIGHT_FLUORESCENT = 12; + public static final short DAY_WHITE_FLUORESCENT = 13; + public static final short COOL_WHITE_FLUORESCENT = 14; + public static final short WHITE_FLUORESCENT = 15; + public static final short STANDARD_LIGHT_A = 17; + public static final short STANDARD_LIGHT_B = 18; + public static final short STANDARD_LIGHT_C = 19; + public static final short D55 = 20; + public static final short D65 = 21; + public static final short D75 = 22; + public static final short D50 = 23; + public static final short ISO_STUDIO_TUNGSTEN = 24; + public static final short OTHER = 255; + } + + /** + * Constants for {@link #TAG_SENSING_METHOD} + */ + public static interface SensingMethod { + public static final short NOT_DEFINED = 1; + public static final short ONE_CHIP_COLOR = 2; + public static final short TWO_CHIP_COLOR = 3; + public static final short THREE_CHIP_COLOR = 4; + public static final short COLOR_SEQUENTIAL_AREA = 5; + public static final short TRILINEAR = 7; + public static final short COLOR_SEQUENTIAL_LINEAR = 8; + } + + /** + * Constants for {@link #TAG_FILE_SOURCE} + */ + public static interface FileSource { + public static final short DSC = 3; + } + + /** + * Constants for {@link #TAG_SCENE_TYPE} + */ + public static interface SceneType { + public static final short DIRECT_PHOTOGRAPHED = 1; + } + + /** + * Constants for {@link #TAG_GAIN_CONTROL} + */ + public static interface GainControl { + public static final short NONE = 0; + public static final short LOW_UP = 1; + public static final short HIGH_UP = 2; + public static final short LOW_DOWN = 3; + public static final short HIGH_DOWN = 4; + } + + /** + * Constants for {@link #TAG_CONTRAST} + */ + public static interface Contrast { + public static final short NORMAL = 0; + public static final short SOFT = 1; + public static final short HARD = 2; + } + + /** + * Constants for {@link #TAG_SATURATION} + */ + public static interface Saturation { + public static final short NORMAL = 0; + public static final short LOW = 1; + public static final short HIGH = 2; + } + + /** + * Constants for {@link #TAG_SHARPNESS} + */ + public static interface Sharpness { + public static final short NORMAL = 0; + public static final short SOFT = 1; + public static final short HARD = 2; + } + + /** + * Constants for {@link #TAG_SUBJECT_DISTANCE} + */ + public static interface SubjectDistance { + public static final short UNKNOWN = 0; + public static final short MACRO = 1; + public static final short CLOSE_VIEW = 2; + public static final short DISTANT_VIEW = 3; + } + + /** + * Constants for {@link #TAG_GPS_LATITUDE_REF}, {@link #TAG_GPS_DEST_LATITUDE_REF} + */ + public static interface GpsLatitudeRef { + public static final String NORTH = "N"; + public static final String SOUTH = "S"; + } + + /** + * Constants for {@link #TAG_GPS_LONGITUDE_REF}, {@link #TAG_GPS_DEST_LONGITUDE_REF} + */ + public static interface GpsLongitudeRef { + public static final String EAST = "E"; + public static final String WEST = "W"; + } + + /** + * Constants for {@link #TAG_GPS_ALTITUDE_REF} + */ + public static interface GpsAltitudeRef { + public static final short SEA_LEVEL = 0; + public static final short SEA_LEVEL_NEGATIVE = 1; + } + + /** + * Constants for {@link #TAG_GPS_STATUS} + */ + public static interface GpsStatus { + public static final String IN_PROGRESS = "A"; + public static final String INTEROPERABILITY = "V"; + } + + /** + * Constants for {@link #TAG_GPS_MEASURE_MODE} + */ + public static interface GpsMeasureMode { + public static final String MODE_2_DIMENSIONAL = "2"; + public static final String MODE_3_DIMENSIONAL = "3"; + } + + /** + * Constants for {@link #TAG_GPS_SPEED_REF}, {@link #TAG_GPS_DEST_DISTANCE_REF} + */ + public static interface GpsSpeedRef { + public static final String KILOMETERS = "K"; + public static final String MILES = "M"; + public static final String KNOTS = "N"; + } + + /** + * Constants for {@link #TAG_GPS_TRACK_REF}, {@link #TAG_GPS_IMG_DIRECTION_REF}, + * {@link #TAG_GPS_DEST_BEARING_REF} + */ + public static interface GpsTrackRef { + public static final String TRUE_DIRECTION = "T"; + public static final String MAGNETIC_DIRECTION = "M"; + } + + /** + * Constants for {@link #TAG_GPS_DIFFERENTIAL} + */ + public static interface GpsDifferential { + public static final short WITHOUT_DIFFERENTIAL_CORRECTION = 0; + public static final short DIFFERENTIAL_CORRECTION_APPLIED = 1; + } + + /** + * The BYTE type in the EXIF standard. An 8-bit unsigned integer. + */ + public static final short TYPE_UNSIGNED_BYTE = 1; + /** + * The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit ASCII code. + * The final byte is terminated with NULL. + */ + public static final short TYPE_ASCII = 2; + /** + * The SHORT type in the EXIF standard. A 16-bit (2-byte) unsigned integer + */ + public static final short TYPE_UNSIGNED_SHORT = 3; + /** + * The LONG type in the EXIF standard. A 32-bit (4-byte) unsigned integer + */ + public static final short TYPE_UNSIGNED_LONG = 4; + /** + * The RATIONAL type of EXIF standard. It consists of two LONGs. The first one is the numerator + * and the second one expresses the denominator. + */ + public static final short TYPE_UNSIGNED_RATIONAL = 5; + /** + * The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any value + * depending on the field definition. + */ + public static final short TYPE_UNDEFINED = 7; + /** + * The SLONG type in the EXIF standard. A 32-bit (4-byte) signed integer + * (2's complement notation). + */ + public static final short TYPE_LONG = 9; + /** + * The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first one is the + * numerator and the second one is the denominator. + */ + public static final short TYPE_RATIONAL = 10; + + private static final int TYPE_TO_SIZE_MAP[] = new int[11]; + static { + TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_BYTE] = 1; + TYPE_TO_SIZE_MAP[TYPE_ASCII] = 1; + TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_SHORT] = 2; + TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_LONG] = 4; + TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_RATIONAL] = 8; + TYPE_TO_SIZE_MAP[TYPE_UNDEFINED] = 1; + TYPE_TO_SIZE_MAP[TYPE_LONG] = 4; + TYPE_TO_SIZE_MAP[TYPE_RATIONAL] = 8; + } + + /** + * Gets the element size of the given data type. + * + * @see #TYPE_ASCII + * @see #TYPE_LONG + * @see #TYPE_RATIONAL + * @see #TYPE_UNDEFINED + * @see #TYPE_UNSIGNED_BYTE + * @see #TYPE_UNSIGNED_LONG + * @see #TYPE_UNSIGNED_RATIONAL + * @see #TYPE_UNSIGNED_SHORT + */ + public static int getElementSize(short type) { + return TYPE_TO_SIZE_MAP[type]; + } + + private static volatile SparseArray sTagInfo = null; + private static volatile SparseArray sInteroperTagInfo = null; + private static final int SIZE_UNDEFINED = 0; + + private static SparseArray getTagInfo() { + if (sTagInfo == null) { + synchronized(ExifTag.class) { + if (sTagInfo == null) { + sTagInfo = new SparseArray(); + initTagInfo(); + } + } + } + return sTagInfo; + } + + private static SparseArray getInteroperTagInfo() { + if (sInteroperTagInfo == null) { + synchronized(ExifTag.class) { + if (sInteroperTagInfo == null) { + sInteroperTagInfo = new SparseArray(); + sInteroperTagInfo.put(TAG_INTEROPERABILITY_INDEX, + (IfdId.TYPE_IFD_INTEROPERABILITY << 24) + | TYPE_ASCII << 16 | SIZE_UNDEFINED); + } + } + } + return sInteroperTagInfo; + } + + private static void initTagInfo() { + /** + * We put tag information in a 4-bytes integer. The first byte is the + * IFD of the tag, and the second byte is the default data type. The + * last two byte are a short value indicating the component count of this + * tag. + */ + sTagInfo.put(TAG_MAKE, + (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_IMAGE_WIDTH, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1); + sTagInfo.put(TAG_IMAGE_LENGTH, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1); + sTagInfo.put(TAG_BITS_PER_SAMPLE, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 3); + sTagInfo.put(TAG_COMPRESSION, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_PHOTOMETRIC_INTERPRETATION, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_ORIENTATION, (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_SAMPLES_PER_PIXEL, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_PLANAR_CONFIGURATION, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_Y_CB_CR_SUB_SAMPLING, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 2); + sTagInfo.put(TAG_Y_CB_CR_POSITIONING, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_X_RESOLUTION, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_Y_RESOLUTION, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_RESOLUTION_UNIT, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_STRIP_OFFSETS, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_ROWS_PER_STRIP, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1); + sTagInfo.put(TAG_STRIP_BYTE_COUNTS, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_JPEG_INTERCHANGE_FORMAT, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1); + sTagInfo.put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1); + sTagInfo.put(TAG_TRANSFER_FUNCTION, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 3 * 256); + sTagInfo.put(TAG_WHITE_POINT, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 2); + sTagInfo.put(TAG_PRIMARY_CHROMATICITIES, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 6); + sTagInfo.put(TAG_Y_CB_CR_COEFFICIENTS, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 3); + sTagInfo.put(TAG_REFERENCE_BLACK_WHITE, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 6); + sTagInfo.put(TAG_DATE_TIME, + (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | 20); + sTagInfo.put(TAG_IMAGE_DESCRIPTION, + (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_MAKE, + (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_MODEL, + (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_SOFTWARE, + (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_ARTIST, + (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_COPYRIGHT, + (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_EXIF_IFD, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1); + sTagInfo.put(TAG_GPS_IFD, + (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1); + + // EXIF TAG + sTagInfo.put(TAG_EXIF_VERSION, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | 4); + sTagInfo.put(TAG_FLASHPIX_VERSION, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | 4); + sTagInfo.put(TAG_COLOR_SPACE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_COMPONENTS_CONFIGURATION, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | 4); + sTagInfo.put(TAG_COMPRESSED_BITS_PER_PIXEL, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_PIXEL_X_DIMENSION, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_LONG << 16 | 1); + sTagInfo.put(TAG_PIXEL_Y_DIMENSION, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_LONG << 16 | 1); + sTagInfo.put(TAG_MAKER_NOTE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_USER_COMMENT, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_RELATED_SOUND_FILE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | 13); + sTagInfo.put(TAG_DATE_TIME_ORIGINAL, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | 20); + sTagInfo.put(TAG_DATE_TIME_DIGITIZED, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | 20); + sTagInfo.put(TAG_SUB_SEC_TIME, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_SUB_SEC_TIME_ORIGINAL, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_SUB_SEC_TIME_DIGITIZED, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_IMAGE_UNIQUE_ID, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | 33); + sTagInfo.put(TAG_EXPOSURE_TIME, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_F_NUMBER, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_EXPOSURE_PROGRAM, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_SPECTRAL_SENSITIVITY, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_ISO_SPEED_RATINGS, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_OECF, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_SHUTTER_SPEED_VALUE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_RATIONAL << 16 | 1); + sTagInfo.put(TAG_APERTURE_VALUE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_BRIGHTNESS_VALUE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_RATIONAL << 16 | 1); + sTagInfo.put(TAG_EXPOSURE_BIAS_VALUE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_RATIONAL << 16 | 1); + sTagInfo.put(TAG_MAX_APERTURE_VALUE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_SUBJECT_DISTANCE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_METERING_MODE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_LIGHT_SOURCE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_FLASH, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_FOCAL_LENGTH, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_SUBJECT_AREA, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_FLASH_ENERGY, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_SPATIAL_FREQUENCY_RESPONSE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_FOCAL_PLANE_X_RESOLUTION, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_FOCAL_PLANE_Y_RESOLUTION, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_FOCAL_PLANE_RESOLUTION_UNIT, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_SUBJECT_LOCATION, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 2); + sTagInfo.put(TAG_EXPOSURE_INDEX, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_SENSING_METHOD, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_FILE_SOURCE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | 1); + sTagInfo.put(TAG_SCENE_TYPE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | 1); + sTagInfo.put(TAG_CFA_PATTERN, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_CUSTOM_RENDERED, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_EXPOSURE_MODE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_WHITE_BALANCE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_DIGITAL_ZOOM_RATIO, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_FOCAL_LENGTH_IN_35_MM_FILE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_SCENE_CAPTURE_TYPE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_GAIN_CONTROL, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_CONTRAST, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_SATURATION, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_SHARPNESS, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + sTagInfo.put(TAG_DEVICE_SETTING_DESCRIPTION, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_SUBJECT_DISTANCE_RANGE, + (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1); + // GPS tag + sTagInfo.put(TAG_GPS_VERSION_ID, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_BYTE << 16 | 4); + sTagInfo.put(TAG_GPS_LATITUDE_REF, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2); + sTagInfo.put(TAG_GPS_LONGITUDE_REF, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2); + sTagInfo.put(TAG_GPS_LATITUDE, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_RATIONAL << 16 | 3); + sTagInfo.put(TAG_GPS_LONGITUDE, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_RATIONAL << 16 | 3); + sTagInfo.put(TAG_GPS_ALTITUDE_REF, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_BYTE << 16 | 1); + sTagInfo.put(TAG_GPS_ALTITUDE, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_GPS_TIME_STAMP, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 3); + sTagInfo.put(TAG_GPS_SATTELLITES, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_GPS_STATUS, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2); + sTagInfo.put(TAG_GPS_MEASURE_MODE, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2); + sTagInfo.put(TAG_GPS_DOP, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_GPS_SPEED_REF, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2); + sTagInfo.put(TAG_GPS_SPEED, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_GPS_TRACK_REF, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2); + sTagInfo.put(TAG_GPS_TRACK, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_GPS_IMG_DIRECTION_REF, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2); + sTagInfo.put(TAG_GPS_IMG_DIRECTION, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_GPS_MAP_DATUM, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_GPS_DEST_LATITUDE_REF, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2); + sTagInfo.put(TAG_GPS_DEST_LATITUDE, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_GPS_DEST_BEARING_REF, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2); + sTagInfo.put(TAG_GPS_DEST_BEARING, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_GPS_DEST_DISTANCE_REF, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2); + sTagInfo.put(TAG_GPS_DEST_DISTANCE, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1); + sTagInfo.put(TAG_GPS_PROCESSING_METHOD, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_GPS_AREA_INFORMATION, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED); + sTagInfo.put(TAG_GPS_DATA_STAMP, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 11); + sTagInfo.put(TAG_GPS_DIFFERENTIAL, + (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_SHORT << 16 | 11); + } + + private final short mTagId; + private final short mDataType; + private final int mIfd; + private final boolean mComponentCountDefined; + private int mComponentCount; + private Object mValue; + private int mOffset; + + static private short getTypeFromInfo(int info) { + return (short) ((info >> 16) & 0xff); + } + + static private int getComponentCountFromInfo(int info) { + return info & 0xffff; + } + + static private int getIfdIdFromInfo(int info) { + return (info >> 24) & 0xff; + } + + static private boolean getComponentCountDefined(short tagId, int ifd) { + Integer info = (ifd == IfdId.TYPE_IFD_INTEROPERABILITY) ? + getInteroperTagInfo().get(tagId) : getTagInfo().get(tagId); + if (info == null) return false; + return getComponentCountFromInfo(info) != SIZE_UNDEFINED; + } + + static int getIfdIdFromTagId(short tagId) { + Integer info = getTagInfo().get(tagId); + if (info == null) { + throw new IllegalArgumentException("Unknown Tag ID: " + tagId); + } + return getIfdIdFromInfo(info); + } + + /** + * Create a tag with given ID. For tags related to interoperability and thumbnail, call + * {@link #buildInteroperabilityTag(short)} and {@link #buildThumbnailTag(short)} respectively. + * @exception IllegalArgumentException If the ID is invalid. + */ + static public ExifTag buildTag(short tagId) { + Integer info = getTagInfo().get(tagId); + if (info == null) { + throw new IllegalArgumentException("Unknown Tag ID: " + tagId); + } + return new ExifTag(tagId, getTypeFromInfo(info), + getComponentCountFromInfo(info), + getIfdIdFromInfo(info)); + } + + /** + * Create a tag related to thumbnail with given ID. + * @exception IllegalArgumentException If the ID is invalid. + */ + static public ExifTag buildThumbnailTag(short tagId) { + Integer info = getTagInfo().get(tagId); + if (info == null || getIfdIdFromInfo(info) != IfdId.TYPE_IFD_0) { + throw new IllegalArgumentException("Unknown Thumnail Tag ID: " + tagId); + } + return new ExifTag(tagId, getTypeFromInfo(info), + getComponentCountFromInfo(info), + IfdId.TYPE_IFD_1); + } + + /** + * Create a tag related to interoperability with given ID. + * @exception IllegalArgumentException If the ID is invalid. + */ + static public ExifTag buildInteroperabilityTag(short tagId) { + Integer info = getInteroperTagInfo().get(tagId); + if (info == null || getIfdIdFromInfo(info) != IfdId.TYPE_IFD_INTEROPERABILITY) { + throw new RuntimeException("Unknown Interoperability Tag ID: " + tagId); + } + return new ExifTag(tagId, getTypeFromInfo(info), + getComponentCountFromInfo(info), + IfdId.TYPE_IFD_INTEROPERABILITY); + } + + ExifTag(short tagId, short type, int componentCount, int ifd) { + mTagId = tagId; + mDataType = type; + mComponentCount = componentCount; + mComponentCountDefined = getComponentCountDefined(tagId, ifd); + mIfd = ifd; + } + + /** + * Returns the ID of the IFD this tag belongs to. + * + * @see IfdId#TYPE_IFD_0 + * @see IfdId#TYPE_IFD_1 + * @see IfdId#TYPE_IFD_EXIF + * @see IfdId#TYPE_IFD_GPS + * @see IfdId#TYPE_IFD_INTEROPERABILITY + */ + public int getIfd() { + return mIfd; + } + + /** + * Gets the ID of this tag. + */ + public short getTagId() { + return mTagId; + } + + /** + * Gets the data type of this tag + * + * @see #TYPE_ASCII + * @see #TYPE_LONG + * @see #TYPE_RATIONAL + * @see #TYPE_UNDEFINED + * @see #TYPE_UNSIGNED_BYTE + * @see #TYPE_UNSIGNED_LONG + * @see #TYPE_UNSIGNED_RATIONAL + * @see #TYPE_UNSIGNED_SHORT + */ + public short getDataType() { + return mDataType; + } + + /** + * Gets the total data size in bytes of the value of this tag. + */ + public int getDataSize() { + return getComponentCount() * getElementSize(getDataType()); + } + + /** + * Gets the component count of this tag. + */ + public int getComponentCount() { + return mComponentCount; + } + + /** + * Returns true if this ExifTag contains value; otherwise, this tag will contain an offset value + * that links to the area where the actual value is located. + * + * @see #getOffset() + */ + public boolean hasValue() { + return mValue != null; + } + + /** + * Gets the offset of this tag. This is only valid if this data size > 4 and contains an offset + * to the location of the actual value. + */ + public int getOffset() { + return mOffset; + } + + /** + * Sets the offset of this tag. + */ + void setOffset(int offset) { + mOffset = offset; + } + + private void checkComponentCountOrThrow(int count) + throws IllegalArgumentException { + if (mComponentCountDefined && (mComponentCount != count)) { + throw new IllegalArgumentException("Tag " + mTagId + ": Required " + + mComponentCount + " components but was given " + count + + " component(s)"); + } + } + + private void throwTypeNotMatchedException(String className) + throws IllegalArgumentException { + throw new IllegalArgumentException("Tag " + mTagId + ": expect type " + + convertTypeToString(mDataType) + " but got " + className); + } + + private static String convertTypeToString(short type) { + switch (type) { + case TYPE_UNSIGNED_BYTE: + return "UNSIGNED_BYTE"; + case TYPE_ASCII: + return "ASCII"; + case TYPE_UNSIGNED_SHORT: + return "UNSIGNED_SHORT"; + case TYPE_UNSIGNED_LONG: + return "UNSIGNED_LONG"; + case TYPE_UNSIGNED_RATIONAL: + return "UNSIGNED_RATIONAL"; + case TYPE_UNDEFINED: + return "UNDEFINED"; + case TYPE_LONG: + return "LONG"; + case TYPE_RATIONAL: + return "RATIONAL"; + default: + return ""; + } + } + + private static final int UNSIGNED_SHORT_MAX = 65535; + private static final long UNSIGNED_LONG_MAX = 4294967295L; + private static final long LONG_MAX = Integer.MAX_VALUE; + private static final long LONG_MIN = Integer.MIN_VALUE; + + private void checkOverflowForUnsignedShort(int[] value) { + for (int v : value) { + if (v > UNSIGNED_SHORT_MAX || v < 0) { + throw new IllegalArgumentException( + "Tag " + mTagId+ ": Value" + v + + " is illegal for type UNSIGNED_SHORT"); + } + } + } + + private void checkOverflowForUnsignedLong(long[] value) { + for (long v: value) { + if (v < 0 || v > UNSIGNED_LONG_MAX) { + throw new IllegalArgumentException( + "Tag " + mTagId+ ": Value" + v + + " is illegal for type UNSIGNED_LONG"); + } + } + } + + private void checkOverflowForUnsignedLong(int[] value) { + for (int v: value) { + if (v < 0) { + throw new IllegalArgumentException( + "Tag " + mTagId+ ": Value" + v + + " is illegal for type UNSIGNED_LONG"); + } + } + } + + private void checkOverflowForUnsignedRational(Rational[] value) { + for (Rational v: value) { + if (v.getNominator() < 0 || v.getDenominator() < 0 + || v.getNominator() > UNSIGNED_LONG_MAX + || v.getDenominator() > UNSIGNED_LONG_MAX) { + throw new IllegalArgumentException( + "Tag " + mTagId+ ": Value" + v + + " is illegal for type UNSIGNED_RATIONAL"); + } + } + } + + private void checkOverflowForRational(Rational[] value) { + for (Rational v: value) { + if (v.getNominator() < LONG_MIN || v.getDenominator() < LONG_MIN + || v.getNominator() > LONG_MAX + || v.getDenominator() > LONG_MAX) { + throw new IllegalArgumentException( + "Tag " + mTagId+ ": Value" + v + + " is illegal for type RATIONAL"); + } + } + } + + /** + * Sets integer values into this tag. + * @exception IllegalArgumentException for the following situation: + *

    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT}, + * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.
  • + *
  • The value overflows.
  • + *
  • The value.length does NOT match the definition of component count in + * EXIF standard.
  • + *
+ */ + public void setValue(int[] value) { + checkComponentCountOrThrow(value.length); + if (mDataType != TYPE_UNSIGNED_SHORT && mDataType != TYPE_LONG && + mDataType != TYPE_UNSIGNED_LONG) { + throwTypeNotMatchedException("int"); + } + if (mDataType == TYPE_UNSIGNED_SHORT) { + checkOverflowForUnsignedShort(value); + } else if (mDataType == TYPE_UNSIGNED_LONG) { + checkOverflowForUnsignedLong(value); + } + + long[] data = new long[value.length]; + for (int i = 0; i < value.length; i++) { + data[i] = value[i]; + } + mValue = data; + mComponentCount = value.length; + } + + /** + * Sets integer values into this tag. + * @exception IllegalArgumentException For the following situation: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT}, + * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.
  • + *
  • The value overflows.
  • + *
  • The component count in the definition of EXIF standard is not 1.
  • + *
+ */ + public void setValue(int value) { + checkComponentCountOrThrow(1); + setValue(new int[] {value}); + } + + /** + * Sets long values into this tag. + * @exception IllegalArgumentException For the following situation: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.
  • + *
  • The value overflows.
  • + *
  • The value.length does NOT match the definition of component count in + * EXIF standard.
  • + *
+ */ + public void setValue(long[] value) { + checkComponentCountOrThrow(value.length); + if (mDataType != TYPE_UNSIGNED_LONG) { + throwTypeNotMatchedException("long"); + } + checkOverflowForUnsignedLong(value); + mValue = value; + mComponentCount = value.length; + } + + /** + * Sets long values into this tag. + * @exception IllegalArgumentException For the following situation: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.
  • + *
  • The value overflows.
  • + *
  • The component count in the definition of EXIF standard is not 1.
  • + *
+ */ + public void setValue(long value) { + setValue(new long[] {value}); + } + + /** + * Sets string values into this tag. + * @exception IllegalArgumentException If the data type is not {@link #TYPE_ASCII} + * or value.length() + 1 does NOT fit the definition of the component count in the + * EXIF standard. + */ + public void setValue(String value) { + checkComponentCountOrThrow(value.length() + 1); + if (mDataType != TYPE_ASCII) { + throwTypeNotMatchedException("String"); + } + mComponentCount = value.length() + 1; + mValue = value; + } + + /** + * Sets Rational values into this tag. + * @exception IllegalArgumentException For the following situation: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} or + * {@link #TYPE_RATIONAL} .
  • + *
  • The value overflows.
  • + *
  • The value.length does NOT match the definition of component count in + * EXIF standard.
  • + *
+ */ + public void setValue(Rational[] value) { + if (mDataType == TYPE_UNSIGNED_RATIONAL) { + checkOverflowForUnsignedRational(value); + } else if (mDataType == TYPE_RATIONAL) { + checkOverflowForRational(value); + } else { + throwTypeNotMatchedException("Rational"); + } + checkComponentCountOrThrow(value.length); + mValue = value; + mComponentCount = value.length; + } + + /** + * Sets Rational values into this tag. + * @exception IllegalArgumentException For the following situation: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} or + * {@link #TYPE_RATIONAL} .
  • + *
  • The value overflows.
  • + *
  • The component count in the definition of EXIF standard is not 1.
  • + *
+ * */ + public void setValue(Rational value) { + setValue(new Rational[] {value}); + } + + /** + * Sets byte values into this tag. + * @exception IllegalArgumentException For the following situation: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or + * {@link #TYPE_UNDEFINED} .
  • + *
  • The length does NOT match the definition of component count in EXIF standard.
  • + *
+ * */ + public void setValue(byte[] value, int offset, int length) { + checkComponentCountOrThrow(length); + if (mDataType != TYPE_UNSIGNED_BYTE && mDataType != TYPE_UNDEFINED) { + throwTypeNotMatchedException("byte"); + } + mValue = new byte[length]; + System.arraycopy(value, offset, mValue, 0, length); + mComponentCount = length; + } + + /** + * Equivalent to setValue(value, 0, value.length). + */ + public void setValue(byte[] value) { + setValue(value, 0, value.length); + } + + private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("yyyy:MM:dd kk:mm:ss"); + + /** + * Sets a timestamp to this tag. The method converts the timestamp with the format of + * "yyyy:MM:dd kk:mm:ss" and calls {@link #setValue(String)}. + * + * @param time the number of milliseconds since Jan. 1, 1970 GMT + * @exception IllegalArgumentException If the data type is not {@link #TYPE_ASCII} + * or the component count of this tag is not 20 or undefined + */ + public void setTimeValue(long time) { + // synchronized on TIME_FORMAT as SimpleDateFormat is not thread safe + synchronized (TIME_FORMAT) { + setValue(TIME_FORMAT.format(new Date(time))); + } + } + + /** + * Gets the {@link #TYPE_UNSIGNED_SHORT} data. + * @exception IllegalArgumentException If the type is NOT {@link #TYPE_UNSIGNED_SHORT}. + */ + public int getUnsignedShort(int index) { + if (mDataType != TYPE_UNSIGNED_SHORT) { + throw new IllegalArgumentException("Cannot get UNSIGNED_SHORT value from " + + convertTypeToString(mDataType)); + } + return (int) (((long[]) mValue) [index]); + } + + /** + * Gets the {@link #TYPE_LONG} data. + * @exception IllegalArgumentException If the type is NOT {@link #TYPE_LONG}. + */ + public int getLong(int index) { + if (mDataType != TYPE_LONG) { + throw new IllegalArgumentException("Cannot get LONG value from " + + convertTypeToString(mDataType)); + } + return (int) (((long[]) mValue) [index]); + } + + /** + * Gets the {@link #TYPE_UNSIGNED_LONG} data. + * @exception IllegalArgumentException If the type is NOT {@link #TYPE_UNSIGNED_LONG}. + */ + public long getUnsignedLong(int index) { + if (mDataType != TYPE_UNSIGNED_LONG) { + throw new IllegalArgumentException("Cannot get UNSIGNED LONG value from " + + convertTypeToString(mDataType)); + } + return ((long[]) mValue) [index]; + } + + /** + * Gets the {@link #TYPE_ASCII} data. + * @exception IllegalArgumentException If the type is NOT {@link #TYPE_ASCII}. + */ + public String getString() { + if (mDataType != TYPE_ASCII) { + throw new IllegalArgumentException("Cannot get ASCII value from " + + convertTypeToString(mDataType)); + } + return (String) mValue; + } + + /** + * Gets the {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL} data. + * @exception IllegalArgumentException If the type is NOT {@link #TYPE_RATIONAL} or + * {@link #TYPE_UNSIGNED_RATIONAL}. + */ + public Rational getRational(int index) { + if ((mDataType != TYPE_RATIONAL) && (mDataType != TYPE_UNSIGNED_RATIONAL)) { + throw new IllegalArgumentException("Cannot get RATIONAL value from " + + convertTypeToString(mDataType)); + } + return ((Rational[]) mValue) [index]; + } + + /** + * Equivalent to getBytes(buffer, 0, buffer.length). + */ + public void getBytes(byte[] buf) { + getBytes(buf, 0, buf.length); + } + + /** + * Gets the {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE} data. + * + * @param buf the byte array in which to store the bytes read. + * @param offset the initial position in buffer to store the bytes. + * @param length the maximum number of bytes to store in buffer. If length > component count, + * only the valid bytes will be stored. + * + * @exception IllegalArgumentException If the type is NOT {@link #TYPE_UNDEFINED} or + * {@link #TYPE_UNSIGNED_BYTE}. + */ + public void getBytes(byte[] buf, int offset, int length) { + if ((mDataType != TYPE_UNDEFINED) && (mDataType != TYPE_UNSIGNED_BYTE)) { + throw new IllegalArgumentException("Cannot get BYTE value from " + + convertTypeToString(mDataType)); + } + System.arraycopy(mValue, 0, buf, offset, + (length > mComponentCount) ? mComponentCount : length); + } + + /** + * Returns a string representation of the value of this tag. + */ + public String valueToString() { + StringBuilder sbuilder = new StringBuilder(); + switch (getDataType()) { + case ExifTag.TYPE_UNDEFINED: + case ExifTag.TYPE_UNSIGNED_BYTE: + byte buf[] = new byte[getComponentCount()]; + getBytes(buf); + for(int i = 0, n = getComponentCount(); i < n; i++) { + if(i != 0) sbuilder.append(" "); + sbuilder.append(String.format("%02x", buf[i])); + } + break; + case ExifTag.TYPE_ASCII: + sbuilder.append(getString()); + break; + case ExifTag.TYPE_UNSIGNED_LONG: + for(int i = 0, n = getComponentCount(); i < n; i++) { + if(i != 0) sbuilder.append(" "); + sbuilder.append(getUnsignedLong(i)); + } + break; + case ExifTag.TYPE_RATIONAL: + case ExifTag.TYPE_UNSIGNED_RATIONAL: + for(int i = 0, n = getComponentCount(); i < n; i++) { + Rational r = getRational(i); + if(i != 0) sbuilder.append(" "); + sbuilder.append(r.getNominator()).append("/").append(r.getDenominator()); + } + break; + case ExifTag.TYPE_UNSIGNED_SHORT: + for(int i = 0, n = getComponentCount(); i < n; i++) { + if(i != 0) sbuilder.append(" "); + sbuilder.append(getUnsignedShort(i)); + } + break; + case ExifTag.TYPE_LONG: + for(int i = 0, n = getComponentCount(); i < n; i++) { + if(i != 0) sbuilder.append(" "); + sbuilder.append(getLong(i)); + } + break; + } + return sbuilder.toString(); + } + + /** + * Returns true if the ID is one of the following: {@link #TAG_EXIF_IFD}, + * {@link #TAG_GPS_IFD}, {@link #TAG_JPEG_INTERCHANGE_FORMAT}, + * {@link #TAG_STRIP_OFFSETS}, {@link #TAG_INTEROPERABILITY_IFD} + */ + static boolean isOffsetTag(short tagId) { + return tagId == TAG_EXIF_IFD + || tagId == TAG_GPS_IFD + || tagId == TAG_JPEG_INTERCHANGE_FORMAT + || tagId == TAG_STRIP_OFFSETS + || tagId == TAG_INTEROPERABILITY_IFD; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ExifTag) { + ExifTag tag = (ExifTag) obj; + if (mValue != null) { + if (mValue instanceof long[]) { + if (!(tag.mValue instanceof long[])) return false; + return Arrays.equals((long[]) mValue, (long[]) tag.mValue); + } else if (mValue instanceof Rational[]) { + if (!(tag.mValue instanceof Rational[])) return false; + return Arrays.equals((Rational[]) mValue, (Rational[]) tag.mValue); + } else if (mValue instanceof byte[]) { + if (!(tag.mValue instanceof byte[])) return false; + return Arrays.equals((byte[]) mValue, (byte[]) tag.mValue); + } else { + return mValue.equals(tag.mValue); + } + } else { + return tag.mValue == null; + } + } + return false; + } +} diff --git a/gallerycommon/src/com/android/gallery3d/exif/IfdData.java b/gallerycommon/src/com/android/gallery3d/exif/IfdData.java new file mode 100644 index 000000000..78f9173cc --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/IfdData.java @@ -0,0 +1,122 @@ +/* + * 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.util.HashMap; +import java.util.Map; + +/** + * This class stores all the tags in an IFD. + * + * @see ExifData + * @see ExifTag + */ +class IfdData { + + private final int mIfdId; + private final Map mExifTags = new HashMap(); + private int mOffsetToNextIfd = 0; + + /** + * Creates an IfdData with given IFD ID. + * + * @see IfdId#TYPE_IFD_0 + * @see IfdId#TYPE_IFD_1 + * @see IfdId#TYPE_IFD_EXIF + * @see IfdId#TYPE_IFD_GPS + * @see IfdId#TYPE_IFD_INTEROPERABILITY + */ + public IfdData(int ifdId) { + mIfdId = ifdId; + } + + /** + * Get a array the contains all {@link ExifTag} in this IFD. + */ + public ExifTag[] getAllTags() { + return mExifTags.values().toArray(new ExifTag[mExifTags.size()]); + } + + /** + * Gets the ID of this IFD. + * + * @see IfdId#TYPE_IFD_0 + * @see IfdId#TYPE_IFD_1 + * @see IfdId#TYPE_IFD_EXIF + * @see IfdId#TYPE_IFD_GPS + * @see IfdId#TYPE_IFD_INTEROPERABILITY + */ + public int getId() { + return mIfdId; + } + + /** + * Gets the {@link ExifTag} with given tag id. Return null if there is no such tag. + */ + public ExifTag getTag(short tagId) { + return mExifTags.get(tagId); + } + + /** + * Adds or replaces a {@link ExifTag}. + */ + public void setTag(ExifTag tag) { + mExifTags.put(tag.getTagId(), tag); + } + + /** + * Gets the tags count in the IFD. + */ + public int getTagCount() { + return mExifTags.size(); + } + + /** + * Sets the offset of next IFD. + */ + void setOffsetToNextIfd(int offset) { + mOffsetToNextIfd = offset; + } + + /** + * Gets the offset of next IFD. + */ + int getOffsetToNextIfd() { + return mOffsetToNextIfd; + } + + /** + * Returns true if all tags in this two IFDs are equal. Note that tags of IFDs offset or + * thumbnail offset will be ignored. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof IfdData) { + IfdData data = (IfdData) obj; + if (data.getId() == mIfdId && data.getTagCount() == getTagCount()) { + ExifTag[] tags = data.getAllTags(); + for (ExifTag tag: tags) { + if (ExifTag.isOffsetTag(tag.getTagId())) continue; + ExifTag tag2 = mExifTags.get(tag.getTagId()); + if (!tag.equals(tag2)) return false; + } + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/gallerycommon/src/com/android/gallery3d/exif/IfdId.java b/gallerycommon/src/com/android/gallery3d/exif/IfdId.java new file mode 100644 index 000000000..1b9634369 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/IfdId.java @@ -0,0 +1,26 @@ +/* + * 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; + +public interface IfdId { + public static final int TYPE_IFD_0 = 0; + public static final int TYPE_IFD_1 = 1; + public static final int TYPE_IFD_EXIF = 2; + public static final int TYPE_IFD_INTEROPERABILITY = 3; + public static final int TYPE_IFD_GPS = 4; + /* This is use in ExifData to allocate enough IfdData */ + static final int TYPE_IFD_COUNT = 5; +} diff --git a/gallerycommon/src/com/android/gallery3d/exif/JpegHeader.java b/gallerycommon/src/com/android/gallery3d/exif/JpegHeader.java new file mode 100644 index 000000000..e3e787eff --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/JpegHeader.java @@ -0,0 +1,39 @@ +/* + * 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; + +class JpegHeader { + public static final short SOI = (short) 0xFFD8; + public static final short APP1 = (short) 0xFFE1; + public static final short APP0 = (short) 0xFFE0; + public static final short EOI = (short) 0xFFD9; + + /** + * SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT, JPG, + * and DAC marker. + */ + public static final short SOF0 = (short) 0xFFC0; + public static final short SOF15 = (short) 0xFFCF; + public static final short DHT = (short) 0xFFC4; + public static final short JPG = (short) 0xFFC8; + public static final short DAC = (short) 0xFFCC; + + public static final boolean isSofMarker(short marker) { + return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG + && marker != DAC; + } +} diff --git a/gallerycommon/src/com/android/gallery3d/exif/OrderedDataOutputStream.java b/gallerycommon/src/com/android/gallery3d/exif/OrderedDataOutputStream.java new file mode 100644 index 000000000..4f785a889 --- /dev/null +++ b/gallerycommon/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; + +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/gallerycommon/src/com/android/gallery3d/exif/Rational.java b/gallerycommon/src/com/android/gallery3d/exif/Rational.java new file mode 100644 index 000000000..7d9026261 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/Rational.java @@ -0,0 +1,45 @@ +/* + * 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; + +public class Rational { + + private final long mNominator; + private final long mDenominator; + + public Rational(long nominator, long denominator) { + mNominator = nominator; + mDenominator = denominator; + } + + public long getNominator() { + return mNominator; + } + + public long getDenominator() { + return mDenominator; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Rational) { + Rational data = (Rational) obj; + return mNominator == data.mNominator && mDenominator == data.mDenominator; + } + return false; + } +} \ No newline at end of file diff --git a/gallerycommon/src/com/android/gallery3d/exif/Util.java b/gallerycommon/src/com/android/gallery3d/exif/Util.java new file mode 100644 index 000000000..594d6fc7f --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/Util.java @@ -0,0 +1,34 @@ +/* + * 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.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 + } + } +} -- cgit v1.2.3