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 17 package com.android.camera2.its; 18 19 import android.content.Context; 20 import android.graphics.ImageFormat; 21 import android.hardware.camera2.CameraDevice; 22 import android.hardware.camera2.CameraCharacteristics; 23 import android.hardware.camera2.CaptureRequest; 24 import android.hardware.camera2.CaptureResult; 25 import android.media.Image; 26 import android.media.Image.Plane; 27 import android.net.Uri; 28 import android.os.Environment; 29 import android.util.Log; 30 31 import org.json.JSONArray; 32 import org.json.JSONObject; 33 34 import java.io.File; 35 import java.io.FileInputStream; 36 import java.io.FileNotFoundException; 37 import java.io.FileOutputStream; 38 import java.io.IOException; 39 import java.nio.ByteBuffer; 40 import java.nio.channels.FileChannel; 41 import java.nio.charset.Charset; 42 import java.util.List; 43 44 public 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 storeCameraCharacteristics(CameraCharacteristics 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(CameraCharacteristics 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.Builder> 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 // Also need avoid access out of bound by only reading the available 298 // bytes in the bytebuffer. 299 int readSize = rowStride; 300 if (buffer.remaining() < readSize) { 301 readSize = buffer.remaining(); 302 } 303 buffer.get(rowData, 0, readSize); 304 for (int col = 0; col < w; col++) { 305 data[offset++] = rowData[col * pixelStride]; 306 } 307 } 308 } 309 } 310 return data; 311 } else { 312 throw new ItsException("Unsupported image format: " + format); 313 } 314 } 315 316 private static boolean checkAndroidImageFormat(Image image) { 317 int format = image.getFormat(); 318 Plane[] planes = image.getPlanes(); 319 switch (format) { 320 case ImageFormat.YUV_420_888: 321 case ImageFormat.NV21: 322 case ImageFormat.YV12: 323 return 3 == planes.length; 324 case ImageFormat.JPEG: 325 return 1 == planes.length; 326 default: 327 return false; 328 } 329 } 330 331 public static File getOutputFile(Context context, String name) 332 throws ItsException { 333 File dir = getStorageDirectory(context, DEFAULT_CAPTURE_DIR + '/' + DEFAULT_IMAGE_DIR); 334 if (dir == null) { 335 throw new ItsException("Could not output file"); 336 } 337 return new File(dir, name); 338 } 339 340 public static String writeImageToFile(Context context, ByteBuffer buf, String name) 341 throws ItsException { 342 File imgFile = getOutputFile(context, name); 343 if (imgFile == null) { 344 throw new ItsException("Failed to get path: " + name); 345 } 346 FileChannel channel = null; 347 try { 348 channel = new FileOutputStream(imgFile, false).getChannel(); 349 channel.write(buf); 350 channel.close(); 351 } catch (FileNotFoundException e) { 352 throw new ItsException("Failed to write file: " + imgFile.toString(), e); 353 } catch (IOException e) { 354 throw new ItsException("Failed to write file: " + imgFile.toString(), e); 355 } 356 return getExternallyVisiblePath(context, imgFile.toString()); 357 } 358 } 359