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
17package com.android.camera.processing.imagebackend;
18
19import android.graphics.ImageFormat;
20import android.graphics.Rect;
21import android.location.Location;
22import android.media.CameraProfile;
23import android.net.Uri;
24
25import com.android.camera.Exif;
26import com.android.camera.app.OrientationManager.DeviceOrientation;
27import com.android.camera.debug.Log;
28import com.android.camera.exif.ExifInterface;
29import com.android.camera.one.v2.camera2proxy.CaptureResultProxy;
30import com.android.camera.one.v2.camera2proxy.ImageProxy;
31import com.android.camera.one.v2.camera2proxy.TotalCaptureResultProxy;
32import com.android.camera.processing.memory.LruResourcePool;
33import com.android.camera.processing.memory.LruResourcePool.Resource;
34import com.android.camera.session.CaptureSession;
35import com.android.camera.util.ExifUtil;
36import com.android.camera.util.JpegUtilNative;
37import com.android.camera.util.Size;
38import com.google.common.base.Optional;
39import com.google.common.util.concurrent.FutureCallback;
40import com.google.common.util.concurrent.Futures;
41import com.google.common.util.concurrent.ListenableFuture;
42
43import java.nio.ByteBuffer;
44import java.util.HashMap;
45import java.util.Map;
46import java.util.concurrent.ExecutionException;
47import 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 */
55public 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