1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.gallery3d.exif;
18
19import java.io.FilterOutputStream;
20import java.io.IOException;
21import java.io.OutputStream;
22import java.nio.ByteBuffer;
23import java.nio.ByteOrder;
24
25public class ExifOutputStream extends FilterOutputStream {
26    private static final String TAG = "ExifOutputStream";
27
28    private static final int STATE_SOI = 0;
29    private static final int STATE_FRAME_HEADER = 1;
30    private static final int STATE_JPEG_DATA = 2;
31
32    private static final int EXIF_HEADER = 0x45786966;
33    private static final short TIFF_HEADER = 0x002A;
34    private static final short TIFF_BIG_ENDIAN = 0x4d4d;
35    private static final short TIFF_LITTLE_ENDIAN = 0x4949;
36    private static final short TAG_SIZE = 12;
37    private static final short TIFF_HEADER_SIZE = 8;
38
39    private ExifData mExifData;
40    private int mState = STATE_SOI;
41    private int mByteToSkip;
42    private int mByteToCopy;
43    private ByteBuffer mBuffer = ByteBuffer.allocate(4);
44
45    public ExifOutputStream(OutputStream ou) {
46        super(ou);
47    }
48
49    public void setExifData(ExifData exifData) {
50        mExifData = exifData;
51    }
52
53    public ExifData getExifData() {
54        return mExifData;
55    }
56
57    private int requestByteToBuffer(int requestByteCount, byte[] buffer
58            , int offset, int length) {
59        int byteNeeded = requestByteCount - mBuffer.position();
60        int byteToRead = length > byteNeeded ? byteNeeded : length;
61        mBuffer.put(buffer, offset, byteToRead);
62        return byteToRead;
63    }
64
65    @Override
66    public void write(byte[] buffer, int offset, int length) throws IOException {
67        while((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
68                && length > 0) {
69            if (mByteToSkip > 0) {
70                int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
71                length -= byteToProcess;
72                mByteToSkip -= byteToProcess;
73                offset += byteToProcess;
74            }
75            if (mByteToCopy > 0) {
76                int byteToProcess = length > mByteToCopy ? mByteToCopy : length;
77                out.write(buffer, offset, byteToProcess);
78                length -= byteToProcess;
79                mByteToCopy -= byteToProcess;
80                offset += byteToProcess;
81            }
82            if (length == 0) return;
83            switch (mState) {
84                case STATE_SOI:
85                    int byteRead = requestByteToBuffer(2, buffer, offset, length);
86                    offset += byteRead;
87                    length -= byteRead;
88                    if (mBuffer.position() < 2) return;
89                    mBuffer.rewind();
90                    assert(mBuffer.getShort() == JpegHeader.SOI);
91                    out.write(mBuffer.array(), 0 ,2);
92                    mState = STATE_FRAME_HEADER;
93                    mBuffer.rewind();
94                    writeExifData();
95                    break;
96                case STATE_FRAME_HEADER:
97                    // We ignore the APP1 segment and copy all other segments until SOF tag.
98                    byteRead = requestByteToBuffer(4, buffer, offset, length);
99                    offset += byteRead;
100                    length -= byteRead;
101                    // Check if this image data doesn't contain SOF.
102                    if (mBuffer.position() == 2) {
103                        short tag = mBuffer.getShort();
104                        if (tag == JpegHeader.EOI) {
105                            out.write(mBuffer.array(), 0, 2);
106                            mBuffer.rewind();
107                        }
108                    }
109                    if (mBuffer.position() < 4) return;
110                    mBuffer.rewind();
111                    short marker = mBuffer.getShort();
112                    if (marker == JpegHeader.APP1) {
113                        mByteToSkip = (mBuffer.getShort() & 0xff) - 2;
114                        mState = STATE_JPEG_DATA;
115                    } else if (!JpegHeader.isSofMarker(marker)) {
116                        out.write(mBuffer.array(), 0, 4);
117                        mByteToCopy = (mBuffer.getShort() & 0xff) - 2;
118                    } else {
119                        out.write(mBuffer.array(), 0, 4);
120                        mState = STATE_JPEG_DATA;
121                    }
122                    mBuffer.rewind();
123            }
124        }
125        if (length > 0) {
126            out.write(buffer, offset, length);
127        }
128    }
129
130    @Override
131    public void write(int oneByte) throws IOException {
132        byte[] buf = new byte[] {(byte) (0xff & oneByte)};
133        write(buf);
134    }
135
136    @Override
137    public void write(byte[] buffer) throws IOException {
138        write(buffer, 0, buffer.length);
139    }
140
141    private void writeExifData() throws IOException {
142        createRequiredIfdAndTag();
143        int exifSize = calculateAllOffset();
144        OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out);
145        dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
146        dataOutputStream.writeShort(JpegHeader.APP1);
147        dataOutputStream.writeShort((short) (exifSize + 8));
148        dataOutputStream.writeInt(EXIF_HEADER);
149        dataOutputStream.writeShort((short) 0x0000);
150        if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) {
151            dataOutputStream.writeShort(TIFF_BIG_ENDIAN);
152        } else {
153            dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN);
154        }
155        dataOutputStream.setByteOrder(mExifData.getByteOrder());
156        dataOutputStream.writeShort(TIFF_HEADER);
157        dataOutputStream.writeInt(8);
158        writeAllTags(dataOutputStream);
159        writeThumbnail(dataOutputStream);
160    }
161
162    private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException {
163        if (mExifData.hasCompressedThumbnail()) {
164            dataOutputStream.write(mExifData.getCompressedThumbnail());
165        } else if (mExifData.hasUncompressedStrip()) {
166            for (int i = 0; i < mExifData.getStripCount(); i++) {
167                dataOutputStream.write(mExifData.getStrip(i));
168            }
169        }
170    }
171
172    private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException {
173        writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream);
174        writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream);
175        IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
176        if (interoperabilityIfd != null) {
177            writeIfd(interoperabilityIfd, dataOutputStream);
178        }
179        IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
180        if (gpsIfd != null) {
181            writeIfd(gpsIfd, dataOutputStream);
182        }
183        IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
184        if (ifd1 != null) {
185            writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream);
186        }
187    }
188
189    private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream)
190            throws IOException {
191        ExifTag[] tags = ifd.getAllTags();
192        dataOutputStream.writeShort((short) tags.length);
193        for (ExifTag tag: tags) {
194            dataOutputStream.writeShort(tag.getTagId());
195            dataOutputStream.writeShort(tag.getDataType());
196            dataOutputStream.writeInt(tag.getComponentCount());
197            if (tag.getDataSize() > 4) {
198                dataOutputStream.writeInt(tag.getOffset());
199            } else {
200                writeTagValue(tag, dataOutputStream);
201                for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) {
202                    dataOutputStream.write(0);
203                }
204            }
205        }
206        dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
207        for (ExifTag tag: tags) {
208            if (tag.getDataSize() > 4) {
209                writeTagValue(tag, dataOutputStream);
210            }
211        }
212    }
213
214    private void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
215            throws IOException {
216        switch (tag.getDataType()) {
217            case ExifTag.TYPE_ASCII:
218                dataOutputStream.write(tag.getString().getBytes());
219                int remain = tag.getComponentCount() - tag.getString().length();
220                for (int i = 0; i < remain; i++) {
221                    dataOutputStream.write(0);
222                }
223                break;
224            case ExifTag.TYPE_LONG:
225                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
226                    dataOutputStream.writeInt(tag.getLong(i));
227                }
228                break;
229            case ExifTag.TYPE_RATIONAL:
230            case ExifTag.TYPE_UNSIGNED_RATIONAL:
231                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
232                    dataOutputStream.writeRational(tag.getRational(i));
233                }
234                break;
235            case ExifTag.TYPE_UNDEFINED:
236            case ExifTag.TYPE_UNSIGNED_BYTE:
237                byte[] buf = new byte[tag.getComponentCount()];
238                tag.getBytes(buf);
239                dataOutputStream.write(buf);
240                break;
241            case ExifTag.TYPE_UNSIGNED_LONG:
242                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
243                    dataOutputStream.writeInt((int) tag.getUnsignedLong(i));
244                }
245                break;
246            case ExifTag.TYPE_UNSIGNED_SHORT:
247                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
248                    dataOutputStream.writeShort((short) tag.getUnsignedShort(i));
249                }
250                break;
251        }
252    }
253
254    private int calculateOffsetOfIfd(IfdData ifd, int offset) {
255        offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
256        ExifTag[] tags = ifd.getAllTags();
257        for(ExifTag tag: tags) {
258            if (tag.getDataSize() > 4) {
259                tag.setOffset(offset);
260                offset += tag.getDataSize();
261            }
262        }
263        return offset;
264    }
265
266    private void createRequiredIfdAndTag() {
267        // IFD0 is required for all file
268        IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
269        if (ifd0 == null) {
270            ifd0 = new IfdData(IfdId.TYPE_IFD_0);
271            mExifData.addIfdData(ifd0);
272        }
273        ExifTag exifOffsetTag = new ExifTag(ExifTag.TAG_EXIF_IFD,
274                ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_0);
275        ifd0.setTag(exifOffsetTag);
276
277        // Exif IFD is required for all file.
278        IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
279        if (exifIfd == null) {
280            exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF);
281            mExifData.addIfdData(exifIfd);
282        }
283
284        // GPS IFD
285        IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
286        if (gpsIfd != null) {
287            ExifTag gpsOffsetTag = new ExifTag(ExifTag.TAG_GPS_IFD,
288                    ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_0);
289            ifd0.setTag(gpsOffsetTag);
290        }
291
292        // Interoperability IFD
293        IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
294        if (interIfd != null) {
295            ExifTag interOffsetTag = new ExifTag(ExifTag.TAG_INTEROPERABILITY_IFD,
296                    ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_EXIF);
297            exifIfd.setTag(interOffsetTag);
298        }
299
300        IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
301
302        // thumbnail
303        if (mExifData.hasCompressedThumbnail()) {
304            if (ifd1 == null) {
305                ifd1 = new IfdData(IfdId.TYPE_IFD_1);
306                mExifData.addIfdData(ifd1);
307            }
308            ExifTag offsetTag = new ExifTag(ExifTag.TAG_JPEG_INTERCHANGE_FORMAT,
309                    ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_1);
310            ifd1.setTag(offsetTag);
311            ExifTag lengthTag = new ExifTag(ExifTag.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
312                    ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_1);
313            lengthTag.setValue(mExifData.getCompressedThumbnail().length);
314            ifd1.setTag(lengthTag);
315        } else if (mExifData.hasUncompressedStrip()){
316            if (ifd1 == null) {
317                ifd1 = new IfdData(IfdId.TYPE_IFD_1);
318                mExifData.addIfdData(ifd1);
319            }
320            int stripCount = mExifData.getStripCount();
321            ExifTag offsetTag = new ExifTag(ExifTag.TAG_STRIP_OFFSETS,
322                    ExifTag.TYPE_UNSIGNED_LONG, stripCount, IfdId.TYPE_IFD_1);
323            ExifTag lengthTag = new ExifTag(ExifTag.TAG_STRIP_BYTE_COUNTS,
324                    ExifTag.TYPE_UNSIGNED_LONG, stripCount, IfdId.TYPE_IFD_1);
325            long[] lengths = new long[stripCount];
326            for (int i = 0; i < mExifData.getStripCount(); i++) {
327                lengths[i] = mExifData.getStrip(i).length;
328            }
329            lengthTag.setValue(lengths);
330            ifd1.setTag(offsetTag);
331            ifd1.setTag(lengthTag);
332        }
333    }
334
335    private int calculateAllOffset() {
336        int offset = TIFF_HEADER_SIZE;
337        IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
338        offset = calculateOffsetOfIfd(ifd0, offset);
339        ifd0.getTag(ExifTag.TAG_EXIF_IFD).setValue(offset);
340
341        IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
342        offset = calculateOffsetOfIfd(exifIfd, offset);
343
344        IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
345        if (interIfd != null) {
346            exifIfd.getTag(ExifTag.TAG_INTEROPERABILITY_IFD).setValue(offset);
347            offset = calculateOffsetOfIfd(interIfd, offset);
348        }
349
350        IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
351        if (gpsIfd != null) {
352            ifd0.getTag(ExifTag.TAG_GPS_IFD).setValue(offset);
353            offset = calculateOffsetOfIfd(gpsIfd, offset);
354        }
355
356        IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
357        if (ifd1 != null) {
358            ifd0.setOffsetToNextIfd(offset);
359            offset = calculateOffsetOfIfd(ifd1, offset);
360        }
361
362        // thumbnail
363        if (mExifData.hasCompressedThumbnail()) {
364            ifd1.getTag(ExifTag.TAG_JPEG_INTERCHANGE_FORMAT).setValue(offset);
365            offset += mExifData.getCompressedThumbnail().length;
366        } else if (mExifData.hasUncompressedStrip()){
367            int stripCount = mExifData.getStripCount();
368            long[] offsets = new long[stripCount];
369            for (int i = 0; i < mExifData.getStripCount(); i++) {
370                offsets[i] = offset;
371                offset += mExifData.getStrip(i).length;
372            }
373            ifd1.getTag(ExifTag.TAG_STRIP_OFFSETS).setValue(offsets);
374        }
375        return offset;
376    }
377}