[BasicExtenderRefactor] Add Yuv To Jpeg converter in camera-extensions

Add Yuv To JPEG converter in camera-extensions

Test: YuvToJpegConverterTest
Bug: 256739005
Change-Id: Idd38af0944d5a7ace44514877c46a27935fccd72
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
index 1ce7421..d1bcd62 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
@@ -18,6 +18,7 @@
 
 import static androidx.camera.core.ImageProcessingUtil.convertJpegBytesToImage;
 import static androidx.camera.core.ImageProcessingUtil.rotateYUV;
+import static androidx.camera.core.ImageProcessingUtil.writeJpegBytesToSurface;
 import static androidx.camera.testing.ImageProxyUtil.createYUV420ImagePlanes;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -33,6 +34,7 @@
 
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
+import androidx.camera.core.impl.utils.Exif;
 import androidx.camera.testing.fakes.FakeImageInfo;
 import androidx.camera.testing.fakes.FakeImageProxy;
 import androidx.core.math.MathUtils;
@@ -47,6 +49,7 @@
 import org.junit.runner.RunWith;
 
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.IntBuffer;
 
@@ -151,6 +154,61 @@
         assertBitmapColor(bitmap, Color.RED, JPEG_ENCODE_ERROR_TOLERANCE);
     }
 
+    @Test
+    public void writeJpegToSurface_returnsTheSameImage() {
+        // Arrange: create a JPEG image with solid color.
+        byte[] inputBytes = createJpegBytesWithSolidColor(Color.RED);
+
+        // Act: acquire image and get the bytes.
+        writeJpegBytesToSurface(mJpegImageReaderProxy.getSurface(), inputBytes);
+
+        final ImageProxy imageProxy = mJpegImageReaderProxy.acquireLatestImage();
+        assertThat(imageProxy).isNotNull();
+        ByteBuffer byteBuffer = imageProxy.getPlanes()[0].getBuffer();
+        byteBuffer.rewind();
+        byte[] outputBytes = new byte[byteBuffer.capacity()];
+        byteBuffer.get(outputBytes);
+
+        // Assert: the color and the dimension of the restored image.
+        Bitmap bitmap = BitmapFactory.decodeByteArray(outputBytes, 0, outputBytes.length);
+        assertThat(bitmap.getWidth()).isEqualTo(WIDTH);
+        assertThat(bitmap.getHeight()).isEqualTo(HEIGHT);
+        assertBitmapColor(bitmap, Color.RED, JPEG_ENCODE_ERROR_TOLERANCE);
+    }
+
+    @Test
+    public void convertYuvToJpegBytesIntoSurface_sizeAndRotationAreCorrect() throws IOException {
+        final int expectedRotation = 270;
+        // Arrange: create a YUV_420_888 image
+        mYUVImageProxy.setPlanes(createYUV420ImagePlanes(
+                WIDTH,
+                HEIGHT,
+                PIXEL_STRIDE_Y,
+                PIXEL_STRIDE_UV,
+                /*flipUV=*/false,
+                /*incrementValue=*/false));
+
+        // Act: convert it into JPEG and write into the surface.
+        ImageProcessingUtil.convertYuvToJpegBytesIntoSurface(mYUVImageProxy,
+                100, expectedRotation, mJpegImageReaderProxy.getSurface());
+
+        final ImageProxy imageProxy = mJpegImageReaderProxy.acquireLatestImage();
+        assertThat(imageProxy).isNotNull();
+        ByteBuffer byteBuffer = imageProxy.getPlanes()[0].getBuffer();
+        byteBuffer.rewind();
+        byte[] outputBytes = new byte[byteBuffer.capacity()];
+        byteBuffer.get(outputBytes);
+
+        // Assert: the format is JPEG and can decode,  the size is correct, the rotation in Exif
+        // is correct.
+        assertThat(imageProxy.getFormat()).isEqualTo(ImageFormat.JPEG);
+        Bitmap bitmap = BitmapFactory.decodeByteArray(outputBytes, 0, outputBytes.length);
+        assertThat(bitmap.getWidth()).isEqualTo(WIDTH);
+        assertThat(bitmap.getHeight()).isEqualTo(HEIGHT);
+        Exif exif = Exif.createFromImageProxy(imageProxy);
+        assertThat(exif.getRotation()).isEqualTo(expectedRotation);
+    }
+
     /**
      * Returns JPEG bytes of a image with the given color.
      */
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
index e7d869d..c69d3ab 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
@@ -34,6 +34,7 @@
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.ImageReaderProxy;
 import androidx.camera.core.internal.compat.ImageWriterCompat;
+import androidx.camera.core.internal.utils.ImageUtil;
 import androidx.core.util.Preconditions;
 
 import java.nio.ByteBuffer;
@@ -94,6 +95,45 @@
     }
 
     /**
+     * Writes a JPEG bytes data as an Image into the Surface. Returns true if it succeeds and false
+     * otherwise.
+     */
+    public static boolean writeJpegBytesToSurface(
+            @NonNull Surface surface,
+            @NonNull byte[] jpegBytes) {
+        Preconditions.checkNotNull(jpegBytes);
+        Preconditions.checkNotNull(surface);
+
+        if (nativeWriteJpegToSurface(jpegBytes, surface) != 0) {
+            Logger.e(TAG, "Failed to enqueue JPEG image.");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Convert a YUV_420_888 ImageProxy to a JPEG bytes data as an Image into the Surface.
+     *
+     * <p>Returns true if it succeeds and false otherwise.
+     */
+    public static boolean convertYuvToJpegBytesIntoSurface(
+            @NonNull ImageProxy imageProxy,
+            @IntRange(from = 1, to = 100) int jpegQuality,
+            @ImageOutputConfig.RotationDegreesValue int rotationDegrees,
+            @NonNull Surface outputSurface) {
+        try {
+            byte[] jpegBytes =
+                    ImageUtil.yuvImageToJpegByteArray(
+                            imageProxy, null, jpegQuality, rotationDegrees);
+            return writeJpegBytesToSurface(outputSurface,
+                    jpegBytes);
+        } catch (ImageUtil.CodecFailedException e) {
+            Logger.e(TAG, "Failed to encode YUV to JPEG", e);
+            return false;
+        }
+    }
+
+    /**
      * Converts image proxy in YUV to RGB.
      *
      * Currently this config supports the devices which generated NV21, NV12, I420 YUV layout,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
index 947ac42f..a43a5e2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
@@ -201,7 +201,7 @@
             }
         } else if (imageFormat == ImageFormat.YUV_420_888) {
             return ImageUtil.yuvImageToJpegByteArray(image, shouldCropImage ? image.getCropRect() :
-                    null, jpegQuality);
+                    null, jpegQuality, 0 /* rotationDegrees */);
         } else {
             Logger.w(TAG, "Unrecognized image format: " + imageFormat);
         }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java
index c7daab2..3113c4d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java
@@ -17,31 +17,26 @@
 package androidx.camera.core.imagecapture;
 
 import static android.graphics.ImageFormat.JPEG;
-import static android.graphics.ImageFormat.NV21;
 import static android.graphics.ImageFormat.YUV_420_888;
 
 import static androidx.camera.core.ImageCapture.ERROR_UNKNOWN;
 import static androidx.camera.core.impl.utils.Exif.createFromInputStream;
 import static androidx.camera.core.impl.utils.TransformUtils.updateSensorToBufferTransform;
 import static androidx.camera.core.internal.utils.ImageUtil.jpegImageToJpegByteArray;
-import static androidx.camera.core.internal.utils.ImageUtil.yuv_420_888toNv21;
 
-import static java.nio.ByteBuffer.allocateDirect;
 import static java.util.Objects.requireNonNull;
 
 import android.graphics.Rect;
-import android.graphics.YuvImage;
 import android.os.Build;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.ImageProxy;
 import androidx.camera.core.impl.utils.Exif;
-import androidx.camera.core.impl.utils.ExifData;
-import androidx.camera.core.impl.utils.ExifOutputStream;
-import androidx.camera.core.internal.ByteBufferOutputStream;
+import androidx.camera.core.internal.utils.ImageUtil;
 import androidx.camera.core.processing.Operation;
 import androidx.camera.core.processing.Packet;
 
@@ -49,8 +44,6 @@
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
 
 /**
  * Converts a {@link ImageProxy} to JPEG bytes.
@@ -95,16 +88,17 @@
         ImageProxy image = packet.getData();
         Rect cropRect = packet.getCropRect();
 
-        // Converts YUV_420_888 to NV21.
-        byte[] yuvBytes = yuv_420_888toNv21(image);
-        YuvImage yuvImage = new YuvImage(yuvBytes, NV21, image.getWidth(), image.getHeight(), null);
-
-        // Compress NV21 to JPEG and crop.
-        ByteBuffer buffer = allocateDirect(cropRect.width() * cropRect.height() * 2);
-        OutputStream outputStream = new ExifOutputStream(new ByteBufferOutputStream(buffer),
-                ExifData.create(image, packet.getRotationDegrees()));
-        yuvImage.compressToJpeg(cropRect, input.getJpegQuality(), outputStream);
-        byte[] jpegBytes = byteBufferToByteArray(buffer);
+        byte[] jpegBytes;
+        try {
+            jpegBytes = ImageUtil.yuvImageToJpegByteArray(
+                    image,
+                    cropRect,
+                    input.getJpegQuality(),
+                    packet.getRotationDegrees());
+        } catch (ImageUtil.CodecFailedException e) {
+            throw new ImageCaptureException(ImageCapture.ERROR_FILE_IO,
+                    "Failed to encode the image to JPEG.", e);
+        }
 
         // Return bytes with a new format, size, and crop rect.
         return Packet.of(
@@ -118,14 +112,6 @@
                 packet.getCameraCaptureResult());
     }
 
-    private static byte[] byteBufferToByteArray(@NonNull ByteBuffer buffer) {
-        int jpegSize = buffer.position();
-        byte[] bytes = new byte[jpegSize];
-        buffer.rewind();
-        buffer.get(bytes, 0, jpegSize);
-        return bytes;
-    }
-
     private static Exif extractExif(@NonNull byte[] jpegBytes) throws ImageCaptureException {
         try {
             return createFromInputStream(new ByteArrayInputStream(jpegBytes));
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
index f8ebadf..36330a5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
@@ -308,7 +308,9 @@
     public static ExifData create(@NonNull ImageProxy imageProxy,
             @ImageOutputConfig.RotationDegreesValue int rotationDegrees) {
         ExifData.Builder builder = ExifData.builderForDevice();
-        imageProxy.getImageInfo().populateExifData(builder);
+        if (imageProxy.getImageInfo() != null) {
+            imageProxy.getImageInfo().populateExifData(builder);
+        }
 
         // Overwrites the orientation degrees value of the output image because the capture
         // results might not have correct value when capturing image in YUV_420_888 format. See
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
index 75679d4..081ccc1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.internal.utils.ImageUtil;
 import androidx.core.util.Preconditions;
 
 import java.util.Locale;
@@ -36,7 +37,7 @@
  * {@link RectF}, a rotation degrees integer and a boolean flag for the rotation-direction
  * (clockwise v.s. counter-clockwise).
  *
- * TODO(b/179827713): merge this with {@link androidx.camera.core.internal.utils.ImageUtil}.
+ * TODO(b/179827713): merge this with {@link ImageUtil}.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class TransformUtils {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
index 9c8101f..925e953 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -37,9 +37,12 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.ImageProxy;
 import androidx.camera.core.Logger;
+import androidx.camera.core.impl.utils.ExifData;
+import androidx.camera.core.impl.utils.ExifOutputStream;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.nio.ByteBuffer;
 
 /**
@@ -163,23 +166,37 @@
     /**
      * Converts YUV_420_888 {@link ImageProxy} to JPEG byte array. The input YUV_420_888 image
      * will be cropped if a non-null crop rectangle is specified. The output JPEG byte array will
-     * be compressed by the specified quality value.
+     * be compressed by the specified quality value. The rotationDegrees is set to the EXIF of
+     * the JPEG if it is not 0.
      */
     @NonNull
     public static byte[] yuvImageToJpegByteArray(@NonNull ImageProxy image,
-            @Nullable Rect cropRect, @IntRange(from = 1, to = 100) int jpegQuality)
-            throws CodecFailedException {
+            @Nullable Rect cropRect,
+            @IntRange(from = 1, to = 100)
+            int jpegQuality,
+            int rotationDegrees) throws CodecFailedException {
         if (image.getFormat() != ImageFormat.YUV_420_888) {
             throw new IllegalArgumentException(
                     "Incorrect image format of the input image proxy: " + image.getFormat());
         }
 
-        return ImageUtil.nv21ToJpeg(
-                ImageUtil.yuv_420_888toNv21(image),
-                image.getWidth(),
-                image.getHeight(),
-                cropRect,
-                jpegQuality);
+        byte[] yuvBytes = yuv_420_888toNv21(image);
+        YuvImage yuv = new YuvImage(yuvBytes, ImageFormat.NV21, image.getWidth(), image.getHeight(),
+                null);
+
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        OutputStream out = new ExifOutputStream(
+                byteArrayOutputStream, ExifData.create(image, rotationDegrees));
+        if (cropRect == null) {
+            cropRect = new Rect(0, 0, image.getWidth(), image.getHeight());
+        }
+        boolean success =
+                yuv.compressToJpeg(cropRect, jpegQuality, out);
+        if (!success) {
+            throw new CodecFailedException("YuvImage failed to encode jpeg.",
+                    CodecFailedException.FailureType.ENCODE_FAILED);
+        }
+        return byteArrayOutputStream.toByteArray();
     }
 
     /** {@link android.media.Image} to NV21 byte array. */
@@ -364,21 +381,6 @@
         return dispatchCropRect;
     }
 
-    private static byte[] nv21ToJpeg(@NonNull byte[] nv21, int width, int height,
-            @Nullable Rect cropRect, @IntRange(from = 1, to = 100) int jpegQuality)
-            throws CodecFailedException {
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        YuvImage yuv = new YuvImage(nv21, ImageFormat.NV21, width, height, null);
-        boolean success =
-                yuv.compressToJpeg(cropRect == null ? new Rect(0, 0, width, height) : cropRect,
-                        jpegQuality, out);
-        if (!success) {
-            throw new CodecFailedException("YuvImage failed to encode jpeg.",
-                    CodecFailedException.FailureType.ENCODE_FAILED);
-        }
-        return out.toByteArray();
-    }
-
     private static boolean isCropAspectRatioHasEffect(@NonNull Size sourceSize,
             @NonNull Rational aspectRatio) {
         int sourceWidth = sourceSize.getWidth();
@@ -423,7 +425,7 @@
             UNKNOWN
         }
 
-        private FailureType mFailureType;
+        private final FailureType mFailureType;
 
         CodecFailedException(@NonNull String message) {
             super(message);
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverterTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverterTest.kt
new file mode 100644
index 0000000..b459806
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverterTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor
+
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.ImageReaderProxys
+import androidx.camera.core.ImmutableImageInfo
+import androidx.camera.core.impl.ImageReaderProxy
+import androidx.camera.core.impl.TagBundle
+import androidx.camera.core.impl.utils.Exif
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.TestImageUtil
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class YuvToJpegConverterTest {
+    companion object {
+        const val WIDTH = 640
+        const val HEIGHT = 480
+        const val MAX_IMAGES = 2
+    }
+
+    private lateinit var jpegImageReaderProxy: ImageReaderProxy
+    private lateinit var yuvToJpegConverter: YuvToJpegConverter
+
+    @Before
+    fun setUp() {
+        jpegImageReaderProxy = ImageReaderProxys.createIsolatedReader(
+            WIDTH, HEIGHT, ImageFormat.JPEG, MAX_IMAGES
+        )
+        yuvToJpegConverter = YuvToJpegConverter(
+            100, jpegImageReaderProxy.surface!!
+        )
+    }
+
+    @After
+    fun tearDown() {
+        jpegImageReaderProxy.close()
+    }
+
+    private fun generateYuvImage(): ImageProxy {
+        return TestImageUtil.createYuvFakeImageProxy(
+            ImmutableImageInfo.create(
+                TagBundle.emptyBundle(), 0, 0, Matrix()
+            ), WIDTH, HEIGHT
+        )
+    }
+
+    @Test
+    fun canOutputJpeg() = runBlocking {
+        val deferredImage = CompletableDeferred<ImageProxy>()
+        jpegImageReaderProxy.setOnImageAvailableListener({ imageReader ->
+            imageReader.acquireNextImage()?.let { deferredImage.complete(it) }
+        }, CameraXExecutors.ioExecutor())
+
+        val imageYuv = generateYuvImage()
+        yuvToJpegConverter.writeYuvImage(imageYuv)
+
+        withTimeout(1000) {
+            deferredImage.await().use {
+                assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+                assertExifWidthAndHeight(it, WIDTH, HEIGHT)
+            }
+        }
+    }
+
+    private fun assertExifWidthAndHeight(imageProxy: ImageProxy, width: Int, height: Int) {
+        val exif = Exif.createFromImageProxy(imageProxy)
+        assertThat(exif.width).isEqualTo(width)
+        assertThat(exif.height).isEqualTo(height)
+    }
+
+    @Test
+    fun canSetRotation() = runBlocking {
+        val rotationDegrees = 270
+        val deferredImage = CompletableDeferred<ImageProxy>()
+        jpegImageReaderProxy.setOnImageAvailableListener({ imageReader ->
+            imageReader.acquireNextImage()?.let { deferredImage.complete(it) }
+        }, CameraXExecutors.ioExecutor())
+
+        val imageYuv = generateYuvImage()
+        yuvToJpegConverter.setRotationDegrees(rotationDegrees)
+        yuvToJpegConverter.writeYuvImage(imageYuv)
+
+        withTimeout(1000) {
+            deferredImage.await().use {
+                assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+                val exif = Exif.createFromImageProxy(it)
+                assertThat(exif.rotation).isEqualTo(rotationDegrees)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverter.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverter.java
new file mode 100644
index 0000000..c8e1ef0
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverter.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.graphics.ImageFormat;
+import android.view.Surface;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.ImageProcessingUtil;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.core.util.Preconditions;
+
+/**
+ * A image converter for YUV_420_888 to JPEG. The converted JPEG images were written to the given
+ * output surface after {@link #writeYuvImage(ImageProxy)} is invoked.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class YuvToJpegConverter {
+    private static final String TAG = "YuvToJpegConverter";
+    private final Surface mOutputJpegSurface;
+    @IntRange(from = 1, to = 100)
+    private volatile int mJpegQuality;
+    @ImageOutputConfig.RotationDegreesValue
+    private volatile int mRotationDegrees = 0;
+
+    YuvToJpegConverter(int jpegQuality, @NonNull Surface outputJpegSurface) {
+        mJpegQuality = jpegQuality;
+        mOutputJpegSurface = outputJpegSurface;
+    }
+
+    public void setRotationDegrees(@ImageOutputConfig.RotationDegreesValue int rotationDegrees) {
+        mRotationDegrees = rotationDegrees;
+    }
+
+    void setJpegQuality(int jpgQuality) {
+        mJpegQuality = jpgQuality;
+    }
+
+    static class ConversionFailedException extends Exception {
+        ConversionFailedException(String message) {
+            super(message);
+        }
+        ConversionFailedException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
+    /**
+     * Writes an YUV_420_888 image and converts it into a JPEG image.
+     */
+    void writeYuvImage(@NonNull ImageProxy imageProxy) throws ConversionFailedException {
+        Preconditions.checkState(imageProxy.getFormat() == ImageFormat.YUV_420_888,
+                "Input image is not expected YUV_420_888 image format");
+        try {
+            // TODO(b/258667618): remove extra copy by writing the jpeg data directly into the
+            //  Surface's ByteBuffer.
+            boolean success = ImageProcessingUtil.convertYuvToJpegBytesIntoSurface(
+                    imageProxy,
+                    mJpegQuality,
+                    mRotationDegrees,
+                    mOutputJpegSurface);
+            if (!success) {
+                throw new ConversionFailedException("Failed to process YUV -> JPEG");
+            }
+        } catch (Exception e) {
+            Logger.e(TAG, "Failed to process YUV -> JPEG", e);
+            throw new ConversionFailedException("Failed to process YUV -> JPEG", e);
+        } finally {
+            imageProxy.close();
+        }
+    }
+}