1 /* 2 * Copyright (C) 2014 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.camera.processing.imagebackend; 18 19 import android.graphics.ImageFormat; 20 import android.graphics.Rect; 21 import android.hardware.Camera; 22 import android.location.Location; 23 import android.media.CameraProfile; 24 import android.net.Uri; 25 26 import com.android.camera.Exif; 27 import com.android.camera.app.OrientationManager.DeviceOrientation; 28 import com.android.camera.debug.Log; 29 import com.android.camera.exif.ExifInterface; 30 import com.android.camera.one.v2.camera2proxy.CaptureResultProxy; 31 import com.android.camera.one.v2.camera2proxy.ImageProxy; 32 import com.android.camera.one.v2.camera2proxy.TotalCaptureResultProxy; 33 import com.android.camera.processing.memory.LruResourcePool; 34 import com.android.camera.processing.memory.LruResourcePool.Resource; 35 import com.android.camera.session.CaptureSession; 36 import com.android.camera.util.ExifUtil; 37 import com.android.camera.util.JpegUtilNative; 38 import com.android.camera.util.Size; 39 import com.google.common.base.Optional; 40 import com.google.common.util.concurrent.FutureCallback; 41 import com.google.common.util.concurrent.Futures; 42 import com.google.common.util.concurrent.ListenableFuture; 43 import com.google.common.util.concurrent.MoreExecutors; 44 45 import java.nio.ByteBuffer; 46 import java.util.HashMap; 47 import java.util.Map; 48 import java.util.concurrent.ExecutionException; 49 import java.util.concurrent.Executor; 50 51 /** 52 * Implements the conversion of a YUV_420_888 image to compressed JPEG byte 53 * array, using the native implementation of the Camera Application. If the 54 * image is already JPEG, then it passes it through properly with the assumption 55 * that the JPEG is already encoded in the proper orientation. 56 */ 57 public class TaskCompressImageToJpeg extends TaskJpegEncode { 58 59 /** 60 * Loss-less JPEG compression is usually about a factor of 5, 61 * and is a safe lower bound for this value to use to reduce the memory 62 * footprint for encoding the final jpg. 63 */ 64 private static final int MINIMUM_EXPECTED_JPG_COMPRESSION_FACTOR = 2; 65 private final LruResourcePool<Integer, ByteBuffer> mByteBufferDirectPool; 66 67 /** 68 * Constructor 69 * 70 * @param image Image required for computation 71 * @param executor Executor to run events 72 * @param imageTaskManager Link to ImageBackend for reference counting 73 * @param captureSession Handler for UI/Disk events 74 */ TaskCompressImageToJpeg(ImageToProcess image, Executor executor, ImageTaskManager imageTaskManager, CaptureSession captureSession, LruResourcePool<Integer, ByteBuffer> byteBufferResourcePool)75 TaskCompressImageToJpeg(ImageToProcess image, Executor executor, 76 ImageTaskManager imageTaskManager, 77 CaptureSession captureSession, 78 LruResourcePool<Integer, ByteBuffer> byteBufferResourcePool) { 79 super(image, executor, imageTaskManager, ProcessingPriority.SLOW, captureSession); 80 mByteBufferDirectPool = byteBufferResourcePool; 81 } 82 83 /** 84 * Wraps the static call to JpegUtilNative for testability. {@see 85 * JpegUtilNative#compressJpegFromYUV420Image} 86 */ compressJpegFromYUV420Image(ImageProxy img, ByteBuffer outBuf, int quality, Rect crop, int degrees)87 public int compressJpegFromYUV420Image(ImageProxy img, ByteBuffer outBuf, int quality, 88 Rect crop, int degrees) { 89 return JpegUtilNative.compressJpegFromYUV420Image(img, outBuf, quality, crop, degrees); 90 } 91 92 /** 93 * Encapsulates the required EXIF Tag parse for Image processing. 94 * 95 * @param exif EXIF data from which to extract data. 96 * @return A Minimal Map from ExifInterface.Tag value to values required for Image processing 97 */ exifGetMinimalTags(ExifInterface exif)98 public Map<Integer, Integer> exifGetMinimalTags(ExifInterface exif) { 99 Map<Integer, Integer> map = new HashMap<>(); 100 map.put(ExifInterface.TAG_ORIENTATION, Exif.getOrientation(exif)); 101 map.put(ExifInterface.TAG_PIXEL_X_DIMENSION, exif.getTagIntValue( 102 ExifInterface.TAG_PIXEL_X_DIMENSION)); 103 map.put(ExifInterface.TAG_PIXEL_Y_DIMENSION, exif.getTagIntValue( 104 ExifInterface.TAG_PIXEL_Y_DIMENSION)); 105 return map; 106 } 107 108 @Override run()109 public void run() { 110 ImageToProcess img = mImage; 111 mSession.getCollector().markProcessingTimeStart(); 112 final Rect safeCrop; 113 114 // For JPEG, it is the capture devices responsibility to get proper 115 // orientation. 116 117 TaskImage inputImage, resultImage; 118 byte[] writeOut; 119 int numBytes; 120 ByteBuffer compressedData; 121 ExifInterface exifData = null; 122 Resource<ByteBuffer> byteBufferResource = null; 123 124 switch (img.proxy.getFormat()) { 125 case ImageFormat.JPEG: 126 try { 127 // In the cases, we will request a zero-oriented JPEG from 128 // the HAL; the HAL may deliver its orientation in the JPEG 129 // encoding __OR__ EXIF -- we don't know. We need to read 130 // the EXIF setting from byte payload and the EXIF reader 131 // doesn't work on direct buffers. So, we make a local 132 // copy in a non-direct buffer. 133 ByteBuffer origBuffer = img.proxy.getPlanes().get(0).getBuffer(); 134 compressedData = ByteBuffer.allocate(origBuffer.limit()); 135 136 // On memory allocation failure, fail gracefully. 137 if (compressedData == null) { 138 // TODO: Put memory allocation failure code here. 139 mSession.finishWithFailure(-1, true); 140 return; 141 } 142 143 origBuffer.rewind(); 144 compressedData.put(origBuffer); 145 origBuffer.rewind(); 146 compressedData.rewind(); 147 148 // For JPEG, always use the EXIF orientation as ground 149 // truth on orientation, width and height. 150 Integer exifOrientation = null; 151 Integer exifPixelXDimension = null; 152 Integer exifPixelYDimension = null; 153 154 if (compressedData.array() != null) { 155 exifData = Exif.getExif(compressedData.array()); 156 Map<Integer, Integer> minimalExifTags = exifGetMinimalTags(exifData); 157 158 exifOrientation = minimalExifTags.get(ExifInterface.TAG_ORIENTATION); 159 exifPixelXDimension = minimalExifTags 160 .get(ExifInterface.TAG_PIXEL_X_DIMENSION); 161 exifPixelYDimension = minimalExifTags 162 .get(ExifInterface.TAG_PIXEL_Y_DIMENSION); 163 } 164 165 final DeviceOrientation exifDerivedRotation; 166 if (exifOrientation == null) { 167 // No existing rotation value is assumed to be 0 168 // rotation. 169 exifDerivedRotation = DeviceOrientation.CLOCKWISE_0; 170 } else { 171 exifDerivedRotation = DeviceOrientation 172 .from(exifOrientation); 173 } 174 175 final int imageWidth; 176 final int imageHeight; 177 // Crop coordinate space is in original sensor coordinates. We need 178 // to calculate the proper rotation of the crop to be applied to the 179 // final JPEG artifact. 180 final DeviceOrientation combinedRotationFromSensorToJpeg = 181 addOrientation(img.rotation, exifDerivedRotation); 182 183 if (exifPixelXDimension == null || exifPixelYDimension == null) { 184 Log.w(TAG, 185 "Cannot parse EXIF for image dimensions, passing 0x0 dimensions"); 186 imageHeight = 0; 187 imageWidth = 0; 188 // calculate crop from exif info with image proxy width/height 189 safeCrop = guaranteedSafeCrop(img.proxy, 190 rotateBoundingBox(img.crop, combinedRotationFromSensorToJpeg)); 191 } else { 192 imageWidth = exifPixelXDimension; 193 imageHeight = exifPixelYDimension; 194 // calculate crop from exif info with combined rotation 195 safeCrop = guaranteedSafeCrop(imageWidth, imageHeight, 196 rotateBoundingBox(img.crop, combinedRotationFromSensorToJpeg)); 197 } 198 199 // Ignore the device rotation on ImageToProcess and use the EXIF from 200 // byte[] payload 201 inputImage = new TaskImage( 202 exifDerivedRotation, 203 imageWidth, 204 imageHeight, 205 img.proxy.getFormat(), safeCrop); 206 207 if(requiresCropOperation(img.proxy, safeCrop)) { 208 // Crop the image 209 resultImage = new TaskImage( 210 exifDerivedRotation, 211 safeCrop.width(), 212 safeCrop.height(), 213 img.proxy.getFormat(), null); 214 215 byte[] croppedResult = decompressCropAndRecompressJpegData( 216 compressedData.array(), safeCrop, 217 getJpegCompressionQuality()); 218 219 compressedData = ByteBuffer.allocate(croppedResult.length); 220 compressedData.put(ByteBuffer.wrap(croppedResult)); 221 compressedData.rewind(); 222 } else { 223 // Pass-though the JPEG data 224 resultImage = inputImage; 225 } 226 } finally { 227 // Release the image now that you have a usable copy in 228 // local memory 229 // Or you failed to process 230 mImageTaskManager.releaseSemaphoreReference(img, mExecutor); 231 } 232 233 onStart(mId, inputImage, resultImage, TaskInfo.Destination.FINAL_IMAGE); 234 235 numBytes = compressedData.limit(); 236 break; 237 case ImageFormat.YUV_420_888: 238 safeCrop = guaranteedSafeCrop(img.proxy, img.crop); 239 try { 240 inputImage = new TaskImage(img.rotation, img.proxy.getWidth(), 241 img.proxy.getHeight(), 242 img.proxy.getFormat(), safeCrop); 243 Size resultSize = getImageSizeForOrientation(img.crop.width(), 244 img.crop.height(), 245 img.rotation); 246 247 // Resulting image will be rotated so that viewers won't 248 // have to rotate. That's why the resulting image will have 0 249 // rotation. 250 resultImage = new TaskImage( 251 DeviceOrientation.CLOCKWISE_0, resultSize.getWidth(), 252 resultSize.getHeight(), 253 ImageFormat.JPEG, null); 254 // Image rotation is already encoded into the bytes. 255 256 onStart(mId, inputImage, resultImage, TaskInfo.Destination.FINAL_IMAGE); 257 258 // WARNING: 259 // This reduces the size of the buffer that is created 260 // to hold the final jpg. It is reduced by the "Minimum expected 261 // jpg compression factor" to reduce memory allocation consumption. 262 // If the final jpg is more than this size the image will be 263 // corrupted. The maximum size of an image is width * height * 264 // number_of_channels. We artificially reduce this number based on 265 // what we expect the compression ratio to be to reduce the 266 // amount of memory we are required to allocate. 267 int maxPossibleJpgSize = 3 * resultImage.width * resultImage.height; 268 int jpgBufferSize = maxPossibleJpgSize / 269 MINIMUM_EXPECTED_JPG_COMPRESSION_FACTOR; 270 271 byteBufferResource = mByteBufferDirectPool.acquire(jpgBufferSize); 272 compressedData = byteBufferResource.get(); 273 274 // On memory allocation failure, fail gracefully. 275 if (compressedData == null) { 276 // TODO: Put memory allocation failure code here. 277 mSession.finishWithFailure(-1, true); 278 byteBufferResource.close(); 279 return; 280 } 281 282 // Do the actual compression here. 283 numBytes = compressJpegFromYUV420Image( 284 img.proxy, compressedData, getJpegCompressionQuality(), 285 img.crop, inputImage.orientation.getDegrees()); 286 287 // If the compression overflows the size of the buffer, the 288 // actual number of bytes will be returned. 289 if (numBytes > jpgBufferSize) { 290 byteBufferResource.close(); 291 byteBufferResource = mByteBufferDirectPool.acquire(maxPossibleJpgSize); 292 compressedData = byteBufferResource.get(); 293 294 // On memory allocation failure, fail gracefully. 295 if (compressedData == null) { 296 // TODO: Put memory allocation failure code here. 297 mSession.finishWithFailure(-1, true); 298 byteBufferResource.close(); 299 return; 300 } 301 302 numBytes = compressJpegFromYUV420Image( 303 img.proxy, compressedData, getJpegCompressionQuality(), 304 img.crop, inputImage.orientation.getDegrees()); 305 } 306 307 if (numBytes < 0) { 308 byteBufferResource.close(); 309 throw new RuntimeException("Error compressing jpeg."); 310 } 311 compressedData.limit(numBytes); 312 } finally { 313 // Release the image now that you have a usable copy in local memory 314 // Or you failed to process 315 mImageTaskManager.releaseSemaphoreReference(img, mExecutor); 316 } 317 break; 318 default: 319 mImageTaskManager.releaseSemaphoreReference(img, mExecutor); 320 throw new IllegalArgumentException( 321 "Unsupported input image format for TaskCompressImageToJpeg"); 322 } 323 324 writeOut = new byte[numBytes]; 325 compressedData.get(writeOut); 326 compressedData.rewind(); 327 328 if (byteBufferResource != null) { 329 byteBufferResource.close(); 330 } 331 332 onJpegEncodeDone(mId, inputImage, resultImage, writeOut, 333 TaskInfo.Destination.FINAL_IMAGE); 334 335 // In rare cases, TaskCompressImageToJpeg might complete before 336 // TaskConvertImageToRGBPreview. However, session should take care 337 // of out-of-order completion. 338 // EXIF tags are rewritten so that output from this task is normalized. 339 final TaskImage finalInput = inputImage; 340 final TaskImage finalResult = resultImage; 341 342 final ExifInterface exif = createExif(Optional.fromNullable(exifData), resultImage, 343 img.metadata); 344 mSession.getCollector().decorateAtTimeWriteToDisk(exif); 345 ListenableFuture<Optional<Uri>> futureUri = mSession.saveAndFinish(writeOut, 346 resultImage.width, resultImage.height, resultImage.orientation.getDegrees(), exif); 347 Futures.addCallback(futureUri, new FutureCallback<Optional<Uri>>() { 348 @Override 349 public void onSuccess(Optional<Uri> uriOptional) { 350 if (uriOptional.isPresent()) { 351 onUriResolved(mId, finalInput, finalResult, uriOptional.get(), 352 TaskInfo.Destination.FINAL_IMAGE); 353 } 354 } 355 356 @Override 357 public void onFailure(Throwable throwable) { 358 } 359 }, MoreExecutors.directExecutor()); 360 361 final ListenableFuture<TotalCaptureResultProxy> requestMetadata = img.metadata; 362 // If TotalCaptureResults are available add them to the capture event. 363 // Otherwise, do NOT wait for them, since we'd be stalling the ImageBackend 364 if (requestMetadata.isDone()) { 365 try { 366 mSession.getCollector() 367 .decorateAtTimeOfCaptureRequestAvailable(requestMetadata.get()); 368 } catch (InterruptedException e) { 369 Log.e(TAG, 370 "CaptureResults not added to photoCaptureDoneEvent event due to Interrupted Exception."); 371 } catch (ExecutionException e) { 372 Log.w(TAG, 373 "CaptureResults not added to photoCaptureDoneEvent event due to Execution Exception."); 374 } finally { 375 mSession.getCollector().photoCaptureDoneEvent(); 376 } 377 } else { 378 Log.w(TAG, "CaptureResults unavailable to photoCaptureDoneEvent event."); 379 mSession.getCollector().photoCaptureDoneEvent(); 380 } 381 } 382 383 /** 384 * Wraps a possible log message to be overridden for testability purposes. 385 * 386 * @param message 387 */ logWrapper(String message)388 protected void logWrapper(String message) { 389 // Do nothing. 390 } 391 392 /** 393 * Wraps EXIF Interface for JPEG Metadata creation. Can be overridden for 394 * testing 395 * 396 * @param image Metadata for a jpeg image to create EXIF Interface 397 * @return the created Exif Interface 398 */ createExif(Optional<ExifInterface> exifData, TaskImage image, ListenableFuture<TotalCaptureResultProxy> totalCaptureResultProxyFuture)399 protected ExifInterface createExif(Optional<ExifInterface> exifData, TaskImage image, 400 ListenableFuture<TotalCaptureResultProxy> totalCaptureResultProxyFuture) { 401 ExifInterface exif; 402 if (exifData.isPresent()) { 403 exif = exifData.get(); 404 } else { 405 exif = new ExifInterface(); 406 } 407 Optional<Location> location = Optional.fromNullable(mSession.getLocation()); 408 409 try { 410 new ExifUtil(exif).populateExif(Optional.of(image), 411 Optional.<CaptureResultProxy>of(totalCaptureResultProxyFuture.get()), location); 412 } catch (InterruptedException | ExecutionException e) { 413 new ExifUtil(exif).populateExif(Optional.of(image), 414 Optional.<CaptureResultProxy>absent(), location); 415 } 416 417 return exif; 418 } 419 420 /** 421 * @return Quality level to use for JPEG compression. 422 */ getJpegCompressionQuality()423 protected int getJpegCompressionQuality () { 424 final int quality = CameraProfile.QUALITY_HIGH; 425 int level = CameraProfile.getJpegEncodingQualityParameter(quality); 426 427 if (level > 0) { 428 return level; 429 } 430 431 // getJpegEncodingQualityParameter(int) could not find any back-facing 432 // cameras. Let's use the first (likely front-facing) camera ID, if any, 433 // to get the encoding quality. 434 if (Camera.getNumberOfCameras() > 0) { 435 return CameraProfile.getJpegEncodingQualityParameter(0, quality); 436 } 437 438 return 0; 439 } 440 441 /** 442 * @param originalWidth the width of the original image captured from the 443 * camera 444 * @param originalHeight the height of the original image captured from the 445 * camera 446 * @param orientation the rotation to apply, in degrees. 447 * @return The size of the final rotated image 448 */ getImageSizeForOrientation(int originalWidth, int originalHeight, DeviceOrientation orientation)449 private Size getImageSizeForOrientation(int originalWidth, int originalHeight, 450 DeviceOrientation orientation) { 451 if (orientation == DeviceOrientation.CLOCKWISE_0 452 || orientation == DeviceOrientation.CLOCKWISE_180) { 453 return new Size(originalWidth, originalHeight); 454 } else if (orientation == DeviceOrientation.CLOCKWISE_90 455 || orientation == DeviceOrientation.CLOCKWISE_270) { 456 return new Size(originalHeight, originalWidth); 457 } else { 458 // Unsupported orientation. Get rid of this once UNKNOWN is gone. 459 return new Size(originalWidth, originalHeight); 460 } 461 } 462 } 463