summaryrefslogtreecommitdiffstats
path: root/src/com/android/camera/ExifInterface.java
blob: 2db021a3fb99283d000131fbf055fd5b6b40f017 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
/*
 * Copyright (C) 2007 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;

import java.util.HashMap;
import java.util.Iterator;

import android.util.Config;
import android.util.Log;

// Wrapper for native Exif library

public class ExifInterface {

    private String mFilename;

    // Constants used for the Orientation Exif tag.
    static final int ORIENTATION_UNDEFINED = 0;
    static final int ORIENTATION_NORMAL = 1;
    static final int ORIENTATION_FLIP_HORIZONTAL = 2;   // left right reversed mirror
    static final int ORIENTATION_ROTATE_180 = 3;
    static final int ORIENTATION_FLIP_VERTICAL = 4;     // upside down mirror
    static final int ORIENTATION_TRANSPOSE = 5;         // flipped about top-left <--> bottom-right axis
    static final int ORIENTATION_ROTATE_90 = 6;         // rotate 90 cw to right it
    static final int ORIENTATION_TRANSVERSE = 7;        // flipped about top-right <--> bottom-left axis
    static final int ORIENTATION_ROTATE_270 = 8;        // rotate 270 to right it

    // The Exif tag names
    static final String TAG_ORIENTATION = "Orientation";
    static final String TAG_DATE_TIME_ORIGINAL = "DateTimeOriginal";
    static final String TAG_MAKE = "Make";
    static final String TAG_MODEL = "Model";
    static final String TAG_FLASH = "Flash";
    static final String TAG_IMAGE_WIDTH = "ImageWidth";
    static final String TAG_IMAGE_LENGTH = "ImageLength";

    static final String TAG_GPS_LATITUDE = "GPSLatitude";
    static final String TAG_GPS_LONGITUDE = "GPSLongitude";
    
    static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef";
    static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef";

    private boolean mSavedAttributes = false;
    private boolean mHasThumbnail = false;
    private HashMap<String, String> mCachedAttributes = null;

    static {
        System.loadLibrary("exif");
    }

    public ExifInterface(String fileName) {
        mFilename = fileName;
    }

    /**
     * Given a HashMap of Exif tags and associated values, an Exif section in the JPG file
     * is created and loaded with the tag data. saveAttributes() is expensive because it involves
     * copying all the JPG data from one file to another and deleting the old file and renaming the other.
     * It's best to collect all the attributes to write and make a single call rather than multiple
     *  calls for each attribute. You must call "commitChanges()" at some point to commit the changes.
     */
    public void saveAttributes(HashMap<String, String> attributes) {
        // format of string passed to native C code:
        // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
        // example: "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
        StringBuilder sb = new StringBuilder();
        int size = attributes.size();
        if (attributes.containsKey("hasThumbnail")) {
            --size;
        }
        sb.append(size + " ");
        Iterator keyIterator = attributes.keySet().iterator();
        while (keyIterator.hasNext()) {
            String key = (String)keyIterator.next();
            if (key.equals("hasThumbnail")) {
                continue;       // this is a fake attribute not saved as an exif tag
            }
            String val = (String)attributes.get(key);
            sb.append(key + "=");
            sb.append(val.length() + " ");
            sb.append(val);
        }
        String s = sb.toString();
        if (android.util.Config.LOGV)
            android.util.Log.v("camera", "saving exif data: " + s);
        saveAttributesNative(mFilename, s);
        mSavedAttributes = true;
    }

    /**
     * Returns a HashMap loaded with the Exif attributes of the file. The key is the standard
     * tag name and the value is the tag's value: e.g. Model -> Nikon. Numeric values are
     * returned as strings.
     */
    public HashMap<String, String> getAttributes() {
        if (mCachedAttributes != null) {
            return mCachedAttributes;
        }
        // format of string passed from native C code:
        // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
        // example: "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
        mCachedAttributes = new HashMap<String, String>();

        String attrStr = getAttributesNative(mFilename);

        // get count
        int ptr = attrStr.indexOf(' ');
        int count = Integer.parseInt(attrStr.substring(0, ptr));
        ++ptr;  // skip past the space between item count and the rest of the attributes

        for (int i = 0; i < count; i++) {
            // extract the attribute name
            int equalPos = attrStr.indexOf('=', ptr);
            String attrName = attrStr.substring(ptr, equalPos);
            ptr = equalPos + 1;     // skip past =

            // extract the attribute value length
            int lenPos = attrStr.indexOf(' ', ptr);
            int attrLen = Integer.parseInt(attrStr.substring(ptr, lenPos));
            ptr = lenPos + 1;       // skip pas the space

            // extract the attribute value
            String attrValue = attrStr.substring(ptr, ptr + attrLen);
            ptr += attrLen;

            if (attrName.equals("hasThumbnail")) {
                mHasThumbnail = attrValue.equalsIgnoreCase("true");
            } else {
                mCachedAttributes.put(attrName, attrValue);
            }
        }
        return mCachedAttributes;
    }

    /**
     * Given a numerical orientation, return a human-readable string describing the orientation.
     */
    static public String orientationToString(int orientation) {
        // TODO: this function needs to be localized and use string resource ids rather than strings
        String orientationString;
        switch (orientation) {
            case ORIENTATION_NORMAL:            orientationString = "Normal";   break;
            case ORIENTATION_FLIP_HORIZONTAL:   orientationString = "Flipped horizontal";   break;
            case ORIENTATION_ROTATE_180:        orientationString = "Rotated 180 degrees";   break;
            case ORIENTATION_FLIP_VERTICAL:     orientationString = "Upside down mirror";   break;
            case ORIENTATION_TRANSPOSE:         orientationString = "Transposed";   break;
            case ORIENTATION_ROTATE_90:         orientationString = "Rotated 90 degrees";   break;
            case ORIENTATION_TRANSVERSE:        orientationString = "Transversed";   break;
            case ORIENTATION_ROTATE_270:        orientationString = "Rotated 270 degrees";   break;
            default:                            orientationString = "Undefined";   break;
        }
        return orientationString;
    }

    /**
     * Copies the thumbnail data out of the filename and puts it in the Exif data associated
     * with the file used to create this object. You must call "commitChanges()" at some point
     * to commit the changes.
     */
    public boolean appendThumbnail(String thumbnailFileName) {
        if (!mSavedAttributes) {
            throw new RuntimeException("Must call saveAttributes before calling appendThumbnail");
        }
        mHasThumbnail = appendThumbnailNative(mFilename, thumbnailFileName);
        return mHasThumbnail;
    }

    /**
     * Saves the changes (added Exif tags, added thumbnail) to the JPG file. You have to call
     * saveAttributes() before committing the changes.
     */
    public void commitChanges() {
        if (!mSavedAttributes) {
            throw new RuntimeException("Must call saveAttributes before calling commitChanges");
        }
        commitChangesNative(mFilename);
    }

    public boolean hasThumbnail() {
        if (!mSavedAttributes) {
            getAttributes();
        }
        return mHasThumbnail;
    }

    public byte[] getThumbnail() {
        return getThumbnailNative(mFilename);
    }

    static public String convertRationalLatLonToDecimalString(String rationalString, String ref, boolean usePositiveNegative) {
        try {
            String [] parts = rationalString.split(",");

            String [] pair;
            pair = parts[0].split("/");
            int degrees = (int) (Float.parseFloat(pair[0].trim()) / Float.parseFloat(pair[1].trim()));

            pair = parts[1].split("/");
            int minutes = (int) ((Float.parseFloat(pair[0].trim()) / Float.parseFloat(pair[1].trim())));

            pair = parts[2].split("/");
            float seconds = Float.parseFloat(pair[0].trim()) / Float.parseFloat(pair[1].trim());

            float result = degrees + (minutes/60F) + (seconds/(60F*60F));
            
            String preliminaryResult = String.valueOf(result);
            if (usePositiveNegative) {
                String neg = (ref.equals("S") || ref.equals("E")) ? "-" : "";
                return neg + preliminaryResult;
            } else {
                return preliminaryResult + String.valueOf((char)186) + " " + ref; 
            }
        } catch (Exception ex) {
            // if for whatever reason we can't parse the lat long then return null
            return null;
        }
    }
        
    static public String makeLatLongString(double d) {
        d = Math.abs(d);
        
        int degrees = (int) d;
        
        double remainder = d - (double)degrees;
        int minutes = (int) (remainder * 60D);
        int seconds = (int) (((remainder * 60D) - minutes) * 60D * 1000D);  // really seconds * 1000
        
        String retVal = degrees + "/1," + minutes + "/1," + (int)seconds + "/1000";
        return retVal;
    }
    
    static public String makeLatStringRef(double lat) {
        return lat >= 0D ? "N" : "S";
    }
    
    static public String makeLonStringRef(double lon) {
        return lon >= 0D ? "W" : "E";
    }

    private native boolean appendThumbnailNative(String fileName, String thumbnailFileName);

    private native void saveAttributesNative(String fileName, String compressedAttributes);

    private native String getAttributesNative(String fileName);

    private native void commitChangesNative(String fileName);

    private native byte[] getThumbnailNative(String fileName);
}