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