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.messaging.util.exif;
18
19import android.util.Log;
20import com.android.messaging.util.LogUtil;
21
22import java.io.UnsupportedEncodingException;
23import java.nio.ByteOrder;
24import java.util.ArrayList;
25import java.util.Arrays;
26import java.util.List;
27
28/**
29 * This class stores the EXIF header in IFDs according to the JPEG
30 * specification. It is the result produced by {@link ExifReader}.
31 *
32 * @see ExifReader
33 * @see IfdData
34 */
35class ExifData {
36    private static final String TAG = LogUtil.BUGLE_TAG;
37    private static final byte[] USER_COMMENT_ASCII = {
38            0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00
39    };
40    private static final byte[] USER_COMMENT_JIS = {
41            0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00
42    };
43    private static final byte[] USER_COMMENT_UNICODE = {
44            0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00
45    };
46
47    private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT];
48    private byte[] mThumbnail;
49    private final ArrayList<byte[]> mStripBytes = new ArrayList<byte[]>();
50    private final ByteOrder mByteOrder;
51
52    ExifData(ByteOrder order) {
53        mByteOrder = order;
54    }
55
56    /**
57     * Gets the compressed thumbnail. Returns null if there is no compressed
58     * thumbnail.
59     *
60     * @see #hasCompressedThumbnail()
61     */
62    protected byte[] getCompressedThumbnail() {
63        return mThumbnail;
64    }
65
66    /**
67     * Sets the compressed thumbnail.
68     */
69    protected void setCompressedThumbnail(byte[] thumbnail) {
70        mThumbnail = thumbnail;
71    }
72
73    /**
74     * Returns true it this header contains a compressed thumbnail.
75     */
76    protected boolean hasCompressedThumbnail() {
77        return mThumbnail != null;
78    }
79
80    /**
81     * Adds an uncompressed strip.
82     */
83    protected void setStripBytes(int index, byte[] strip) {
84        if (index < mStripBytes.size()) {
85            mStripBytes.set(index, strip);
86        } else {
87            for (int i = mStripBytes.size(); i < index; i++) {
88                mStripBytes.add(null);
89            }
90            mStripBytes.add(strip);
91        }
92    }
93
94    /**
95     * Gets the strip count.
96     */
97    protected int getStripCount() {
98        return mStripBytes.size();
99    }
100
101    /**
102     * Gets the strip at the specified index.
103     *
104     * @exceptions #IndexOutOfBoundException
105     */
106    protected byte[] getStrip(int index) {
107        return mStripBytes.get(index);
108    }
109
110    /**
111     * Returns true if this header contains uncompressed strip.
112     */
113    protected boolean hasUncompressedStrip() {
114        return mStripBytes.size() != 0;
115    }
116
117    /**
118     * Gets the byte order.
119     */
120    protected ByteOrder getByteOrder() {
121        return mByteOrder;
122    }
123
124    /**
125     * Returns the {@link IfdData} object corresponding to a given IFD if it
126     * exists or null.
127     */
128    protected IfdData getIfdData(int ifdId) {
129        if (ExifTag.isValidIfd(ifdId)) {
130            return mIfdDatas[ifdId];
131        }
132        return null;
133    }
134
135    /**
136     * Adds IFD data. If IFD data of the same type already exists, it will be
137     * replaced by the new data.
138     */
139    protected void addIfdData(IfdData data) {
140        mIfdDatas[data.getId()] = data;
141    }
142
143    /**
144     * Returns the {@link IfdData} object corresponding to a given IFD or
145     * generates one if none exist.
146     */
147    protected IfdData getOrCreateIfdData(int ifdId) {
148        IfdData ifdData = mIfdDatas[ifdId];
149        if (ifdData == null) {
150            ifdData = new IfdData(ifdId);
151            mIfdDatas[ifdId] = ifdData;
152        }
153        return ifdData;
154    }
155
156    /**
157     * Returns the tag with a given TID in the given IFD if the tag exists.
158     * Otherwise returns null.
159     */
160    protected ExifTag getTag(short tag, int ifd) {
161        IfdData ifdData = mIfdDatas[ifd];
162        return (ifdData == null) ? null : ifdData.getTag(tag);
163    }
164
165    /**
166     * Adds the given ExifTag to its default IFD and returns an existing ExifTag
167     * with the same TID or null if none exist.
168     */
169    protected ExifTag addTag(ExifTag tag) {
170        if (tag != null) {
171            int ifd = tag.getIfd();
172            return addTag(tag, ifd);
173        }
174        return null;
175    }
176
177    /**
178     * Adds the given ExifTag to the given IFD and returns an existing ExifTag
179     * with the same TID or null if none exist.
180     */
181    protected ExifTag addTag(ExifTag tag, int ifdId) {
182        if (tag != null && ExifTag.isValidIfd(ifdId)) {
183            IfdData ifdData = getOrCreateIfdData(ifdId);
184            return ifdData.setTag(tag);
185        }
186        return null;
187    }
188
189    protected void clearThumbnailAndStrips() {
190        mThumbnail = null;
191        mStripBytes.clear();
192    }
193
194    /**
195     * Removes the thumbnail and its related tags. IFD1 will be removed.
196     */
197    protected void removeThumbnailData() {
198        clearThumbnailAndStrips();
199        mIfdDatas[IfdId.TYPE_IFD_1] = null;
200    }
201
202    /**
203     * Removes the tag with a given TID and IFD.
204     */
205    protected void removeTag(short tagId, int ifdId) {
206        IfdData ifdData = mIfdDatas[ifdId];
207        if (ifdData == null) {
208            return;
209        }
210        ifdData.removeTag(tagId);
211    }
212
213    /**
214     * Decodes the user comment tag into string as specified in the EXIF
215     * standard. Returns null if decoding failed.
216     */
217    protected String getUserComment() {
218        IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_0];
219        if (ifdData == null) {
220            return null;
221        }
222        ExifTag tag = ifdData.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT));
223        if (tag == null) {
224            return null;
225        }
226        if (tag.getComponentCount() < 8) {
227            return null;
228        }
229
230        byte[] buf = new byte[tag.getComponentCount()];
231        tag.getBytes(buf);
232
233        byte[] code = new byte[8];
234        System.arraycopy(buf, 0, code, 0, 8);
235
236        try {
237            if (Arrays.equals(code, USER_COMMENT_ASCII)) {
238                return new String(buf, 8, buf.length - 8, "US-ASCII");
239            } else if (Arrays.equals(code, USER_COMMENT_JIS)) {
240                return new String(buf, 8, buf.length - 8, "EUC-JP");
241            } else if (Arrays.equals(code, USER_COMMENT_UNICODE)) {
242                return new String(buf, 8, buf.length - 8, "UTF-16");
243            } else {
244                return null;
245            }
246        } catch (UnsupportedEncodingException e) {
247            Log.w(TAG, "Failed to decode the user comment");
248            return null;
249        }
250    }
251
252    /**
253     * Returns a list of all {@link ExifTag}s in the ExifData or null if there
254     * are none.
255     */
256    protected List<ExifTag> getAllTags() {
257        ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
258        for (IfdData d : mIfdDatas) {
259            if (d != null) {
260                ExifTag[] tags = d.getAllTags();
261                if (tags != null) {
262                    for (ExifTag t : tags) {
263                        ret.add(t);
264                    }
265                }
266            }
267        }
268        if (ret.size() == 0) {
269            return null;
270        }
271        return ret;
272    }
273
274    /**
275     * Returns a list of all {@link ExifTag}s in a given IFD or null if there
276     * are none.
277     */
278    protected List<ExifTag> getAllTagsForIfd(int ifd) {
279        IfdData d = mIfdDatas[ifd];
280        if (d == null) {
281            return null;
282        }
283        ExifTag[] tags = d.getAllTags();
284        if (tags == null) {
285            return null;
286        }
287        ArrayList<ExifTag> ret = new ArrayList<ExifTag>(tags.length);
288        for (ExifTag t : tags) {
289            ret.add(t);
290        }
291        if (ret.size() == 0) {
292            return null;
293        }
294        return ret;
295    }
296
297    /**
298     * Returns a list of all {@link ExifTag}s with a given TID or null if there
299     * are none.
300     */
301    protected List<ExifTag> getAllTagsForTagId(short tag) {
302        ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
303        for (IfdData d : mIfdDatas) {
304            if (d != null) {
305                ExifTag t = d.getTag(tag);
306                if (t != null) {
307                    ret.add(t);
308                }
309            }
310        }
311        if (ret.size() == 0) {
312            return null;
313        }
314        return ret;
315    }
316
317    @Override
318    public boolean equals(Object obj) {
319        if (this == obj) {
320            return true;
321        }
322        if (obj == null) {
323            return false;
324        }
325        if (obj instanceof ExifData) {
326            ExifData data = (ExifData) obj;
327            if (data.mByteOrder != mByteOrder ||
328                    data.mStripBytes.size() != mStripBytes.size() ||
329                    !Arrays.equals(data.mThumbnail, mThumbnail)) {
330                return false;
331            }
332            for (int i = 0; i < mStripBytes.size(); i++) {
333                if (!Arrays.equals(data.mStripBytes.get(i), mStripBytes.get(i))) {
334                    return false;
335                }
336            }
337            for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
338                IfdData ifd1 = data.getIfdData(i);
339                IfdData ifd2 = getIfdData(i);
340                if (ifd1 != ifd2 && ifd1 != null && !ifd1.equals(ifd2)) {
341                    return false;
342                }
343            }
344            return true;
345        }
346        return false;
347    }
348
349}
350