/* * 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.camera.exif; import android.util.Log; import java.io.BufferedOutputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; /** * This class provides a way to replace the Exif header of a JPEG image. *
* Below is an example of writing EXIF data into a file * *
* public static void writeExif(byte[] jpeg, ExifData exif, String path) {
* OutputStream os = null;
* try {
* os = new FileOutputStream(path);
* ExifOutputStream eos = new ExifOutputStream(os);
* // Set the exif header
* eos.setExifData(exif);
* // Write the original jpeg out, the header will be add into the file.
* eos.write(jpeg);
* } catch (FileNotFoundException e) {
* e.printStackTrace();
* } catch (IOException e) {
* e.printStackTrace();
* } finally {
* if (os != null) {
* try {
* os.close();
* } catch (IOException e) {
* e.printStackTrace();
* }
* }
* }
* }
*
*/
class ExifOutputStream extends FilterOutputStream {
private static final String TAG = "ExifOutputStream";
private static final boolean DEBUG = false;
private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
private static final int STATE_SOI = 0;
private static final int STATE_FRAME_HEADER = 1;
private static final int STATE_JPEG_DATA = 2;
private static final int EXIF_HEADER = 0x45786966;
private static final short TIFF_HEADER = 0x002A;
private static final short TIFF_BIG_ENDIAN = 0x4d4d;
private static final short TIFF_LITTLE_ENDIAN = 0x4949;
private static final short TAG_SIZE = 12;
private static final short TIFF_HEADER_SIZE = 8;
private static final int MAX_EXIF_SIZE = 65535;
private ExifData mExifData;
private int mState = STATE_SOI;
private int mByteToSkip;
private int mByteToCopy;
private byte[] mSingleByteArray = new byte[1];
private ByteBuffer mBuffer = ByteBuffer.allocate(4);
private final ExifInterface mInterface;
private int mSize = 0;
protected ExifOutputStream(OutputStream ou, ExifInterface iRef) {
super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
mInterface = iRef;
}
/**
* Sets the ExifData to be written into the JPEG file. Should be called
* before writing image data.
*/
protected void setExifData(ExifData exifData) {
mExifData = exifData;
}
/**
* Gets the Exif header to be written into the JPEF file.
*/
protected ExifData getExifData() {
return mExifData;
}
private int requestByteToBuffer(int requestByteCount, byte[] buffer
, int offset, int length) {
int byteNeeded = requestByteCount - mBuffer.position();
int byteToRead = length > byteNeeded ? byteNeeded : length;
mBuffer.put(buffer, offset, byteToRead);
return byteToRead;
}
/**
* Writes the image out. The input data should be a valid JPEG format. After
* writing, it's Exif header will be replaced by the given header.
*/
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
&& length > 0) {
if (mByteToSkip > 0) {
int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
length -= byteToProcess;
mByteToSkip -= byteToProcess;
offset += byteToProcess;
}
if (mByteToCopy > 0) {
int byteToProcess = length > mByteToCopy ? mByteToCopy : length;
out.write(buffer, offset, byteToProcess);
mSize += byteToProcess;
length -= byteToProcess;
mByteToCopy -= byteToProcess;
offset += byteToProcess;
}
if (length == 0) {
return;
}
switch (mState) {
case STATE_SOI:
int byteRead = requestByteToBuffer(2, buffer, offset, length);
offset += byteRead;
length -= byteRead;
if (mBuffer.position() < 2) {
return;
}
mBuffer.rewind();
if (mBuffer.getShort() != JpegHeader.SOI) {
throw new IOException("Not a valid jpeg image, cannot write exif");
}
out.write(mBuffer.array(), 0, 2);
mSize += 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);
mSize += 2;
mBuffer.rewind();
}
}
if (mBuffer.position() < 4) {
return;
}
mBuffer.rewind();
short marker = mBuffer.getShort();
if (marker == JpegHeader.APP1) {
mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
mState = STATE_JPEG_DATA;
} else if (!JpegHeader.isSofMarker(marker)) {
out.write(mBuffer.array(), 0, 4);
mSize += 4;
mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
} else {
out.write(mBuffer.array(), 0, 4);
mSize += 4;
mState = STATE_JPEG_DATA;
}
mBuffer.rewind();
}
}
if (length > 0) {
out.write(buffer, offset, length);
mSize += length;
}
}
/**
* Writes the one bytes out. The input data should be a valid JPEG format.
* After writing, it's Exif header will be replaced by the given header.
*/
@Override
public void write(int oneByte) throws IOException {
mSingleByteArray[0] = (byte) (0xff & oneByte);
write(mSingleByteArray);
}
/**
* Equivalent to calling write(buffer, 0, buffer.length).
*/
@Override
public void write(byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
private void writeExifData() throws IOException {
if (mExifData == null) {
return;
}
if (DEBUG) {
Log.v(TAG, "Writing exif data...");
}
ArrayList