• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 import com.google.common.util.concurrent.MoreExecutors;
43 
44 import java.nio.ByteBuffer;
45 import java.util.HashMap;
46 import java.util.Map;
47 import java.util.concurrent.ExecutionException;
48 import java.util.concurrent.Executor;
49 
50 /**
51  * Implements the conversion of a YUV_420_888 image to compressed JPEG byte
52  * array, using the native implementation of the Camera Application. If the
53  * image is already JPEG, then it passes it through properly with the assumption
54  * that the JPEG is already encoded in the proper orientation.
55  */
56 public class TaskCompressImageToJpeg extends TaskJpegEncode {
57 
58     /**
59      *  Loss-less JPEG compression  is usually about a factor of 5,
60      *  and is a safe lower bound for this value to use to reduce the memory
61      *  footprint for encoding the final jpg.
62      */
63     private static final int MINIMUM_EXPECTED_JPG_COMPRESSION_FACTOR = 2;
64     private final LruResourcePool<Integer, ByteBuffer> mByteBufferDirectPool;
65 
66     /**
67      * Constructor
68      *
69      * @param image Image required for computation
70      * @param executor Executor to run events
71      * @param imageTaskManager Link to ImageBackend for reference counting
72      * @param captureSession Handler for UI/Disk events
73      */
TaskCompressImageToJpeg(ImageToProcess image, Executor executor, ImageTaskManager imageTaskManager, CaptureSession captureSession, LruResourcePool<Integer, ByteBuffer> byteBufferResourcePool)74     TaskCompressImageToJpeg(ImageToProcess image, Executor executor,
75             ImageTaskManager imageTaskManager,
76             CaptureSession captureSession,
77             LruResourcePool<Integer, ByteBuffer> byteBufferResourcePool) {
78         super(image, executor, imageTaskManager, ProcessingPriority.SLOW, captureSession);
79         mByteBufferDirectPool = byteBufferResourcePool;
80     }
81 
82     /**
83      * Wraps the static call to JpegUtilNative for testability. {@see
84      * JpegUtilNative#compressJpegFromYUV420Image}
85      */
compressJpegFromYUV420Image(ImageProxy img, ByteBuffer outBuf, int quality, Rect crop, int degrees)86     public int compressJpegFromYUV420Image(ImageProxy img, ByteBuffer outBuf, int quality,
87             Rect crop, int degrees) {
88         return JpegUtilNative.compressJpegFromYUV420Image(img, outBuf, quality, crop, degrees);
89     }
90 
91     /**
92      * Encapsulates the required EXIF Tag parse for Image processing.
93      *
94      * @param exif EXIF data from which to extract data.
95      * @return A Minimal Map from ExifInterface.Tag value to values required for Image processing
96      */
exifGetMinimalTags(ExifInterface exif)97     public Map<Integer, Integer> exifGetMinimalTags(ExifInterface exif) {
98         Map<Integer, Integer> map = new HashMap<>();
99         map.put(ExifInterface.TAG_ORIENTATION, 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
run()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         }, MoreExecutors.directExecutor());
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      */
logWrapper(String message)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      */
createExif(Optional<ExifInterface> exifData, TaskImage image, ListenableFuture<TotalCaptureResultProxy> totalCaptureResultProxyFuture)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      */
getJpegCompressionQuality()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      */
getImageSizeForOrientation(int originalWidth, int originalHeight, DeviceOrientation orientation)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