Image2JpegBytes.java

/*
 * 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.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.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.processing.Operation;
import androidx.camera.core.processing.Packet;

import com.google.auto.value.AutoValue;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;

/**
 * Converts a {@link ImageProxy} to JPEG bytes.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
final class Image2JpegBytes implements Operation<Image2JpegBytes.In, Packet<byte[]>> {

    @NonNull
    @Override
    public Packet<byte[]> apply(@NonNull Image2JpegBytes.In input) throws ImageCaptureException {
        try {
            int imageFormat = input.getPacket().getFormat();
            switch (imageFormat) {
                case JPEG:
                    return processJpegImage(input);
                case YUV_420_888:
                    return processYuvImage(input);
                default:
                    throw new IllegalArgumentException("Unexpected format: " + imageFormat);
            }
        } finally {
            input.getPacket().getData().close();
        }
    }

    private Packet<byte[]> processJpegImage(@NonNull Image2JpegBytes.In input) {
        Packet<ImageProxy> packet = input.getPacket();
        return Packet.of(
                jpegImageToJpegByteArray(packet.getData()),
                requireNonNull(packet.getExif()),
                JPEG,
                packet.getSize(),
                packet.getCropRect(),
                packet.getRotationDegrees(),
                packet.getSensorToBufferTransform(),
                packet.getCameraCaptureResult());
    }

    private Packet<byte[]> processYuvImage(@NonNull Image2JpegBytes.In input)
            throws ImageCaptureException {
        Packet<ImageProxy> packet = input.getPacket();
        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);

        // Return bytes with a new format, size, and crop rect.
        return Packet.of(
                jpegBytes,
                extractExif(jpegBytes),
                JPEG,
                new Size(cropRect.width(), cropRect.height()),
                new Rect(0, 0, cropRect.width(), cropRect.height()),
                packet.getRotationDegrees(),
                updateSensorToBufferTransform(packet.getSensorToBufferTransform(), cropRect),
                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));
        } catch (IOException e) {
            throw new ImageCaptureException(ERROR_UNKNOWN,
                    "Failed to extract Exif from YUV-generated JPEG", e);
        }
    }

    @AutoValue
    abstract static class In {

        abstract Packet<ImageProxy> getPacket();

        abstract int getJpegQuality();

        @NonNull
        static In of(@NonNull Packet<ImageProxy> imagePacket, int jpegQuality) {
            return new AutoValue_Image2JpegBytes_In(imagePacket, jpegQuality);
        }
    }
}