ItsUtils.java revision 12339cdd90b2c98eb4f7adf794ddca8de53992b8
1/*
2 * Copyright (C) 2013 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.camera2.its;
18
19import android.content.Context;
20import android.graphics.ImageFormat;
21import android.hardware.camera2.CameraDevice;
22import android.hardware.camera2.CameraProperties;
23import android.hardware.camera2.CaptureRequest;
24import android.hardware.camera2.CaptureResult;
25import android.media.Image;
26import android.media.Image.Plane;
27import android.net.Uri;
28import android.os.Environment;
29import android.util.Log;
30
31import org.json.JSONArray;
32import org.json.JSONObject;
33
34import java.io.File;
35import java.io.FileInputStream;
36import java.io.FileNotFoundException;
37import java.io.FileOutputStream;
38import java.io.IOException;
39import java.nio.ByteBuffer;
40import java.nio.channels.FileChannel;
41import java.nio.charset.Charset;
42import java.util.List;
43
44public class ItsUtils {
45    public static final String TAG = ItsUtils.class.getSimpleName();
46
47    // The externally visible (over adb) base path for the files that are saved by this app
48    // to the external media. Currently hardcoded to "/sdcard", which can work on any device
49    // by creating a symlink to the actual mount location.
50    // TODO: Fix this, by querying mount/vold to get the actual externally visible path.
51    public static final String EXT_VISIBLE_BASE_PATH = "/sdcard";
52
53    // State related to output files created by the script.
54    public static final String DEFAULT_CAPTURE_DIR = "its";
55    public static final String DEFAULT_IMAGE_DIR = "captures";
56    public static final String FILE_PREFIX = "IMG_";
57    public static final String JPEG_SUFFIX = ".jpg";
58    public static final String YUV_SUFFIX = ".yuv";
59    public static final String METADATA_SUFFIX = ".json";
60
61    // The indent amount to use when printing the JSON objects out as strings.
62    private static final int PPRINT_JSON_INDENT = 2;
63
64    public static void storeCameraProperties(CameraProperties props,
65                                               File file)
66            throws ItsException {
67        try {
68            JSONObject jsonObj = new JSONObject();
69            jsonObj.put("cameraProperties", ItsSerializer.serialize(props));
70            storeJsonObject(jsonObj, file);
71        } catch (org.json.JSONException e) {
72            throw new ItsException("JSON error: ", e);
73        }
74    }
75
76    public static void storeResults(CameraProperties props,
77                                      CaptureRequest request,
78                                      CaptureResult result,
79                                      File file)
80            throws ItsException {
81        try {
82            JSONObject jsonObj = new JSONObject();
83            jsonObj.put("cameraProperties", ItsSerializer.serialize(props));
84            jsonObj.put("captureRequest", ItsSerializer.serialize(request));
85            jsonObj.put("captureResult", ItsSerializer.serialize(result));
86            storeJsonObject(jsonObj, file);
87        } catch (org.json.JSONException e) {
88            throw new ItsException("JSON error: ", e);
89        }
90    }
91
92    public static void storeJsonObject(JSONObject jsonObj, File file)
93            throws ItsException {
94        ByteBuffer buf = null;
95        try {
96            buf = ByteBuffer.wrap(jsonObj.toString(PPRINT_JSON_INDENT).
97                                  getBytes(Charset.defaultCharset()));
98        } catch (org.json.JSONException e) {
99            throw new ItsException("JSON error: ", e);
100        }
101        FileChannel channel = null;
102        try {
103            channel = new FileOutputStream(file, false).getChannel();
104            channel.write(buf);
105            channel.close();
106        } catch (FileNotFoundException e) {
107            throw new ItsException("Failed to write file: " + file.toString() + ": ", e);
108        } catch (IOException e) {
109            throw new ItsException("Failed to write file: " + file.toString() + ": ", e);
110        }
111    }
112
113    public static List<CaptureRequest> loadRequestList(CameraDevice device, Uri uri)
114            throws ItsException {
115        return ItsSerializer.deserializeRequestList(device, loadJsonFile(uri));
116    }
117
118    public static JSONObject loadJsonFile(Uri uri) throws ItsException {
119        FileInputStream input = null;
120        try {
121            input = new FileInputStream(uri.getPath());
122            byte[] fileData = new byte[input.available()];
123            input.read(fileData);
124            input.close();
125            String text = new String(fileData, Charset.defaultCharset());
126            return new JSONObject(text);
127        } catch (FileNotFoundException e) {
128            throw new ItsException("Failed to read file: " + uri.toString() + ": ", e);
129        } catch (org.json.JSONException e) {
130            throw new ItsException("JSON error: ", e);
131        } catch (IOException e) {
132            throw new ItsException("Failed to read file: " + uri.toString() + ": ", e);
133        }
134    }
135
136    public static int[] getJsonRectFromArray(
137            JSONArray a, boolean normalized, int width, int height)
138            throws ItsException {
139        try {
140            // Returns [x,y,w,h]
141            if (normalized) {
142                return new int[]{(int)Math.floor(a.getDouble(0) * width + 0.5f),
143                                 (int)Math.floor(a.getDouble(1) * height + 0.5f),
144                                 (int)Math.floor(a.getDouble(2) * width + 0.5f),
145                                 (int)Math.floor(a.getDouble(3) * height + 0.5f) };
146            } else {
147                return new int[]{a.getInt(0),
148                                 a.getInt(1),
149                                 a.getInt(2),
150                                 a.getInt(3) };
151            }
152        } catch (org.json.JSONException e) {
153            throw new ItsException("JSON error: ", e);
154        }
155    }
156
157    public static int getCallbacksPerCapture(int format)
158            throws ItsException {
159        // Regardless of the format, there is one callback for the CaptureResult object; this
160        // prepares the output metadata file.
161        int n = 1;
162
163        switch (format) {
164            case ImageFormat.YUV_420_888:
165            case ImageFormat.JPEG:
166                // A single output image callback is made, with either the JPEG or the YUV data.
167                n += 1;
168                break;
169
170            default:
171                throw new ItsException("Unsupported format: " + format);
172        }
173
174        return n;
175    }
176
177    public static JSONObject getOutputSpecs(Uri uri)
178            throws ItsException {
179        FileInputStream input = null;
180        try {
181            input = new FileInputStream(uri.getPath());
182            byte[] fileData = new byte[input.available()];
183            input.read(fileData);
184            input.close();
185            String text = new String(fileData, Charset.defaultCharset());
186            JSONObject jsonObjTop = new JSONObject(text);
187            if (jsonObjTop.has("outputSurface")) {
188                return jsonObjTop.getJSONObject("outputSurface");
189            }
190            return null;
191        } catch (FileNotFoundException e) {
192            throw new ItsException("Failed to read file: " + uri.toString() + ": ", e);
193        } catch (IOException e) {
194            throw new ItsException("Failed to read file: " + uri.toString() + ": ", e);
195        } catch (org.json.JSONException e) {
196            throw new ItsException("JSON error: ", e);
197        }
198    }
199
200    public static boolean isExternalStorageWritable() {
201        String state = Environment.getExternalStorageState();
202        if (Environment.MEDIA_MOUNTED.equals(state)) {
203            return true;
204        }
205        return false;
206    }
207
208    public static File getStorageDirectory(Context context, String dirName)
209            throws ItsException {
210        if (!isExternalStorageWritable()) {
211            throw new ItsException(
212                    "External storage is not writable, cannot save capture image");
213        }
214        File file = Environment.getExternalStorageDirectory();
215        if (file == null) {
216            throw new ItsException("No external storage available");
217        }
218        File newDir = new File(file, dirName);
219        newDir.mkdirs();
220        if (!newDir.isDirectory()) {
221            throw new ItsException("Could not create directory: " + dirName);
222        }
223        return newDir;
224    }
225
226    public static String getExternallyVisiblePath(Context context, String path)
227            throws ItsException {
228        File file = Environment.getExternalStorageDirectory();
229        if (file == null) {
230            throw new ItsException("No external storage available");
231        }
232        String base = file.toString();
233        String newPath = path.replaceFirst(base, EXT_VISIBLE_BASE_PATH);
234        if (newPath == null) {
235            throw new ItsException("Error getting external path: " + path);
236        }
237        return newPath;
238    }
239
240    public static String getJpegFileName(long fileNumber) {
241        return String.format("%s%016x%s", FILE_PREFIX, fileNumber, JPEG_SUFFIX);
242    }
243    public static String getYuvFileName(long fileNumber) {
244        return String.format("%s%016x%s", FILE_PREFIX, fileNumber, YUV_SUFFIX);
245    }
246    public static String getMetadataFileName(long fileNumber) {
247        return String.format("%s%016x%s", FILE_PREFIX, fileNumber, METADATA_SUFFIX);
248    }
249
250    public static byte[] getDataFromImage(Image image)
251            throws ItsException {
252        int format = image.getFormat();
253        int width = image.getWidth();
254        int height = image.getHeight();
255        int rowStride, pixelStride;
256        byte[] data = null;
257
258        // Read image data
259        Plane[] planes = image.getPlanes();
260
261        // Check image validity
262        if (!checkAndroidImageFormat(image)) {
263            throw new ItsException(
264                    "Invalid image format passed to getDataFromImage: " + image.getFormat());
265        }
266
267        if (format == ImageFormat.JPEG) {
268            // JPEG doesn't have pixelstride and rowstride, treat it as 1D buffer.
269            ByteBuffer buffer = planes[0].getBuffer();
270            data = new byte[buffer.capacity()];
271            buffer.get(data);
272            return data;
273        } else if (format == ImageFormat.YUV_420_888) {
274            int offset = 0;
275            data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
276            byte[] rowData = new byte[planes[0].getRowStride()];
277            for (int i = 0; i < planes.length; i++) {
278                ByteBuffer buffer = planes[i].getBuffer();
279                rowStride = planes[i].getRowStride();
280                pixelStride = planes[i].getPixelStride();
281                // For multi-planar yuv images, assuming yuv420 with 2x2 chroma subsampling.
282                int w = (i == 0) ? width : width / 2;
283                int h = (i == 0) ? height : height / 2;
284                for (int row = 0; row < h; row++) {
285                    int bytesPerPixel = ImageFormat.getBitsPerPixel(format) / 8;
286                    if (pixelStride == bytesPerPixel) {
287                        // Special case: optimized read of the entire row
288                        int length = w * bytesPerPixel;
289                        buffer.get(data, offset, length);
290                        // Advance buffer the remainder of the row stride
291                        buffer.position(buffer.position() + rowStride - length);
292                        offset += length;
293                    } else {
294                        // Generic case: should work for any pixelStride but slower.
295                        // Use use intermediate buffer to avoid read byte-by-byte from
296                        // DirectByteBuffer, which is very bad for performance
297                        buffer.get(rowData, 0, rowStride);
298                        for (int col = 0; col < w; col++) {
299                            data[offset++] = rowData[col * pixelStride];
300                        }
301                    }
302                }
303            }
304            return data;
305        } else {
306            throw new ItsException("Unsupported image format: " + format);
307        }
308    }
309
310    private static boolean checkAndroidImageFormat(Image image) {
311        int format = image.getFormat();
312        Plane[] planes = image.getPlanes();
313        switch (format) {
314            case ImageFormat.YUV_420_888:
315            case ImageFormat.NV21:
316            case ImageFormat.YV12:
317                return 3 == planes.length;
318            case ImageFormat.JPEG:
319                return 1 == planes.length;
320            default:
321                return false;
322        }
323    }
324
325    public static File getOutputFile(Context context, String name)
326        throws ItsException {
327        File dir = getStorageDirectory(context, DEFAULT_CAPTURE_DIR + '/' + DEFAULT_IMAGE_DIR);
328        if (dir == null) {
329            throw new ItsException("Could not output file");
330        }
331        return new File(dir, name);
332    }
333
334    public static String writeImageToFile(Context context, ByteBuffer buf, String name)
335        throws ItsException {
336        File imgFile = getOutputFile(context, name);
337        if (imgFile == null) {
338            throw new ItsException("Failed to get path: " + name);
339        }
340        FileChannel channel = null;
341        try {
342            channel = new FileOutputStream(imgFile, false).getChannel();
343            channel.write(buf);
344            channel.close();
345        } catch (FileNotFoundException e) {
346            throw new ItsException("Failed to write file: " + imgFile.toString(), e);
347        } catch (IOException e) {
348            throw new ItsException("Failed to write file: " + imgFile.toString(), e);
349        }
350        return getExternallyVisiblePath(context, imgFile.toString());
351    }
352}
353
354