blob: 51a30ffa24ea8a12a12cf847ced462e0167e2ba1 [file] [log] [blame]
/*
* 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:
byte buf[] = tag.getStringByte();
if (buf.length == tag.getComponentCount()) {
buf[buf.length - 1] = 0;
dataOutputStream.write(buf);
} else {
dataOutputStream.write(buf);
dataOutputStream.write(0);
}
break;
case ExifTag.TYPE_LONG:
case ExifTag.TYPE_UNSIGNED_LONG:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
dataOutputStream.writeInt((int) tag.getValueAt(i));
}
break;
case ExifTag.TYPE_RATIONAL:
case ExifTag.TYPE_UNSIGNED_RATIONAL:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
dataOutputStream.writeRational(tag.getRational(i));
}
break;
case ExifTag.TYPE_UNDEFINED:
case ExifTag.TYPE_UNSIGNED_BYTE:
buf = new byte[tag.getComponentCount()];
tag.getBytes(buf);
dataOutputStream.write(buf);
break;
case ExifTag.TYPE_UNSIGNED_SHORT:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
dataOutputStream.writeShort((short) tag.getValueAt(i));
}
break;
}
}
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;
}
}