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.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 java.util.Objects.requireNonNull;

import android.graphics.Rect;
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.internal.utils.ImageUtil;
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;

/**
 * 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();

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