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