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