summaryrefslogtreecommitdiffstats
path: root/src/com/android/camera/mpo/MpoOutputStream.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/camera/mpo/MpoOutputStream.java')
-rw-r--r--src/com/android/camera/mpo/MpoOutputStream.java333
1 files changed, 333 insertions, 0 deletions
diff --git a/src/com/android/camera/mpo/MpoOutputStream.java b/src/com/android/camera/mpo/MpoOutputStream.java
new file mode 100644
index 000000000..6e8e72fd9
--- /dev/null
+++ b/src/com/android/camera/mpo/MpoOutputStream.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (c) 2016, The Linux Foundation. All rights reserved.
+ * Not a contribution/
+ *
+ * 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.mpo;
+
+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.List;
+
+import android.util.Log;
+
+import com.android.camera.exif.JpegHeader;
+import com.android.camera.exif.OrderedDataOutputStream;
+import com.android.camera.mpo.MpoTag.MpEntry;
+
+class MpoOutputStream extends FilterOutputStream {
+ private static final String TAG = "MpoOutputStream";
+ private static final boolean DEBUG = true;
+ 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 = 3;
+
+ 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 int MAX_EXIF_SIZE = 65535;
+
+ private MpoData mMpoData;
+ private MpoImageData mCurrentImageData;
+ private int mState = STATE_SOI;
+ private int mByteToSkip;
+ private int mByteToCopy;
+ private byte[] mSingleByteArray = new byte[1];
+ private ByteBuffer mBuffer = ByteBuffer.allocate(4);
+ private int mMpoOffsetStart = -1;
+ private int mSize = 0;
+
+ protected MpoOutputStream(OutputStream ou) {
+ super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
+ }
+
+ /**
+ * Sets the ExifData to be written into the JPEG file. Should be called
+ * before writing image data.
+ */
+ protected void setMpoData(MpoData mpoData) {
+ mMpoData = mpoData;
+ mMpoData.updateAllTags();
+ }
+
+ private void resetStates() {
+ mState = STATE_SOI;
+ mByteToSkip = 0;
+ mByteToCopy = 0;
+ mBuffer.rewind();
+ }
+
+ 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;
+ }
+
+ void writeMpoFile() throws IOException {
+ // check and write primary image
+ mCurrentImageData = mMpoData.getPrimaryMpoImage();
+ write(mCurrentImageData.getJpegData());
+ flush();
+
+ // check and write auxiliary images
+ for (MpoImageData image : mMpoData.getAuxiliaryMpoImages()) {
+ resetStates();
+ mCurrentImageData = image;
+ write(mCurrentImageData.getJpegData());
+ flush();
+ }
+ }
+
+ /**
+ * 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();
+ break;
+ case STATE_FRAME_HEADER:
+ // Copy APP1 if it exists
+ // Insert MPO data
+ // Copy remainder of image
+ 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 || marker == JpegHeader.APP0) {
+ out.write(mBuffer.array(), 0, 4);
+ mSize += 4;
+ mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
+ } else {
+ writeMpoData();
+ out.write(mBuffer.array(), 0, 4);
+ mSize += 4;
+ mState = STATE_JPEG_DATA;
+ }
+ mBuffer.rewind();
+ break;
+ }
+ }
+ 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 writeMpoData() throws IOException {
+ if (mMpoData == null) {
+ return;
+ }
+ if (DEBUG) {
+ Log.v(TAG, "Writing mpo data...");
+ }
+ int exifSize = mCurrentImageData.calculateAllIfdOffsets() + MpoImageData.APP_HEADER_SIZE;
+ if (exifSize > MAX_EXIF_SIZE) {
+ throw new IOException("Exif header is too large (>64Kb)");
+ }
+ OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out);
+ dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ dataOutputStream.writeShort(JpegHeader.APP2);
+ dataOutputStream.writeShort((short) (exifSize));
+ dataOutputStream.writeInt(MpoImageData.MP_FORMAT_IDENTIFIER);
+ if (mMpoOffsetStart == -1) {
+ mMpoOffsetStart = mSize + dataOutputStream.size();
+ }
+ if (mCurrentImageData.getByteOrder() == ByteOrder.BIG_ENDIAN) {
+ dataOutputStream.writeShort(TIFF_BIG_ENDIAN);
+ } else {
+ dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN);
+ }
+ dataOutputStream.setByteOrder(mCurrentImageData.getByteOrder());
+ dataOutputStream.writeShort(TIFF_HEADER);
+ if (exifSize > MpoImageData.MP_HEADER_SIZE + MpoImageData.APP_HEADER_SIZE) {
+ dataOutputStream.writeInt(MpoImageData.OFFSET_TO_FIRST_IFD);
+ writeAllTags(dataOutputStream);
+ } else
+ dataOutputStream.writeInt(0);
+
+ mSize += dataOutputStream.size();
+ }
+
+ private void updateIndexIfdOffsets(MpoIfdData indexIfd, int mpoOffset) {
+ // update offsets
+ MpoTag mpEntryTag = mMpoData.getPrimaryMpoImage().getTag((short) MpoInterface.TAG_MP_ENTRY,
+ MpoIfdData.TYPE_MP_INDEX_IFD);
+ List<MpEntry> mpEntries = mpEntryTag.getMpEntryValue();
+ for (int i = 1; i < mpEntries.size(); i++) { // primary offset is always
+ // 0
+ MpEntry entry = mpEntries.get(i);
+ entry.setImageOffset(entry.getImageOffset() - mpoOffset);
+ }
+
+ mpEntryTag.setValue(mpEntries);
+ }
+
+ private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException {
+ MpoIfdData indexIfd = mCurrentImageData.getIndexIfdData();
+ if (indexIfd.getTagCount() > 0) {
+ updateIndexIfdOffsets(indexIfd, mMpoOffsetStart);
+ writeIfd(indexIfd, dataOutputStream);
+ }
+
+ MpoIfdData attribIfd = mCurrentImageData.getAttribIfdData();
+ if (attribIfd.getTagCount() > 0)
+ writeIfd(attribIfd, dataOutputStream);
+ }
+
+ private void writeIfd(MpoIfdData ifd, OrderedDataOutputStream dataOutputStream)
+ throws IOException {
+ MpoTag[] tags = ifd.getAllTags();
+ dataOutputStream.writeShort((short) tags.length);
+ for (MpoTag tag : tags) {
+ dataOutputStream.writeShort(tag.getTagId());
+ dataOutputStream.writeShort(tag.getDataType());
+ dataOutputStream.writeInt(tag.getComponentCount());
+ if (DEBUG) {
+ Log.v(TAG, "\n" + tag.toString());
+ }
+ if (tag.getDataSize() > 4) {
+ dataOutputStream.writeInt(tag.getOffset());
+ } else {
+ MpoOutputStream.writeTagValue(tag, dataOutputStream);
+ for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) {
+ dataOutputStream.write(0);
+ }
+ }
+ }
+ dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
+ for (MpoTag tag : tags) {
+ if (tag.getDataSize() > 4) {
+ MpoOutputStream.writeTagValue(tag, dataOutputStream);
+ }
+ }
+ }
+
+ static void writeTagValue(MpoTag tag, OrderedDataOutputStream dataOutputStream)
+ throws IOException {
+ switch (tag.getDataType()) {
+ case MpoTag.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 MpoTag.TYPE_LONG:
+ case MpoTag.TYPE_UNSIGNED_LONG:
+ for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+ dataOutputStream.writeInt((int) tag.getValueAt(i));
+ }
+ break;
+ case MpoTag.TYPE_RATIONAL:
+ case MpoTag.TYPE_UNSIGNED_RATIONAL:
+ for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+ dataOutputStream.writeRational(tag.getRational(i));
+ }
+ break;
+ case MpoTag.TYPE_UNDEFINED:
+ case MpoTag.TYPE_UNSIGNED_BYTE:
+ buf = new byte[tag.getComponentCount()];
+ tag.getBytes(buf);
+ dataOutputStream.write(buf);
+ break;
+ case MpoTag.TYPE_UNSIGNED_SHORT:
+ for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+ dataOutputStream.writeShort((short) tag.getValueAt(i));
+ }
+ break;
+ }
+ }
+
+ int size() {
+ return mSize;
+ }
+}