[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();
+ }
+ }
+}