RgbaImageProxy.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 androidx.camera.core.internal.utils.ImageUtil.DEFAULT_RGBA_PIXEL_STRIDE;
import static androidx.camera.core.internal.utils.ImageUtil.createBitmapFromPlane;
import static androidx.camera.core.internal.utils.ImageUtil.createDirectByteBuffer;
import static androidx.core.util.Preconditions.checkState;

import static java.util.Objects.requireNonNull;

import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.media.Image;
import android.os.Build;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.camera.core.ExperimentalGetImage;
import androidx.camera.core.ImageInfo;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.impl.TagBundle;
import androidx.camera.core.impl.utils.ExifData;
import androidx.camera.core.processing.Packet;

import java.nio.ByteBuffer;

/**
 * A {@link ImageProxy} that is backed by a RGBA_8888 ByteBuffer.
 *
 * <p> This class is backed by a single {@link ByteBuffer}. The bytes are stored following the
 * {@link Bitmap.Config#ARGB_8888}.
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public final class RgbaImageProxy implements ImageProxy {

    private final Object mLock = new Object();

    private final int mWidth;

    private final int mHeight;

    @NonNull
    private final Rect mCropRect;

    // Null if the ImageProxy is closed. Otherwise non-null.
    @GuardedBy("mLock")
    @Nullable
    PlaneProxy[] mPlaneProxy;

    @NonNull
    private final ImageInfo mImageInfo;

    /**
     * Constructs the object from a {@link Packet}.
     *
     * <p>The wrapped {@link Bitmap} must be {@link Bitmap.Config#ARGB_8888}.
     */
    public RgbaImageProxy(@NonNull Packet<Bitmap> packet) {
        this(packet.getData(),
                packet.getCropRect(),
                packet.getRotationDegrees(), packet.getSensorToBufferTransform(),
                packet.getCameraCaptureResult().getTimestamp());
    }

    /**
     * Constructs the object from a {@link Bitmap} and metadata.
     *
     * <p>The {@link Bitmap} must be {@link Bitmap.Config#ARGB_8888}.
     */
    public RgbaImageProxy(@NonNull Bitmap bitmap, @NonNull Rect cropRect, int rotationDegrees,
            @NonNull Matrix sensorToBuffer, long timestamp) {
        this(createDirectByteBuffer(bitmap),
                DEFAULT_RGBA_PIXEL_STRIDE,
                bitmap.getWidth(),
                bitmap.getHeight(),
                cropRect,
                rotationDegrees,
                sensorToBuffer,
                timestamp);
    }

    /**
     * Constructs the object from a {@link ByteBuffer} and metadata.
     *
     * <p>The data {@link ByteBuffer} must has a pixel stride of 4 and a row stride of width * 4.
     * Each pixel is stored in the order of R, G, B and A.For more details, see the JavaDoc of
     * {@code Bitmap.Config#ARGB_8888}.
     */
    public RgbaImageProxy(@NonNull ByteBuffer byteBuffer, int pixelStride,
            int width, int height, @NonNull Rect cropRect,
            int rotationDegrees, @NonNull Matrix sensorToBuffer, long timestamp) {
        mWidth = width;
        mHeight = height;
        mCropRect = cropRect;
        mImageInfo = createImageInfo(timestamp, rotationDegrees, sensorToBuffer);
        byteBuffer.rewind();
        mPlaneProxy = new PlaneProxy[]{
                createPlaneProxy(byteBuffer, width * pixelStride, pixelStride)
        };
    }

    @Override
    public void close() {
        synchronized (mLock) {
            checkNotClosed();
            // Nullify so it can be GCed.
            mPlaneProxy = null;
        }
    }

    @NonNull
    @Override
    public Rect getCropRect() {
        synchronized (mLock) {
            checkNotClosed();
            return mCropRect;
        }
    }

    @Override
    public void setCropRect(@Nullable Rect rect) {
        synchronized (mLock) {
            checkNotClosed();
            if (rect != null) {
                mCropRect.set(rect);
            }
        }
    }

    @Override
    public int getFormat() {
        synchronized (mLock) {
            checkNotClosed();
            return PixelFormat.RGBA_8888;
        }
    }

    @Override
    public int getHeight() {
        synchronized (mLock) {
            checkNotClosed();
            return mHeight;
        }
    }

    @Override
    public int getWidth() {
        synchronized (mLock) {
            checkNotClosed();
            return mWidth;
        }
    }

    @NonNull
    @Override
    public PlaneProxy[] getPlanes() {
        synchronized (mLock) {
            checkNotClosed();
            return requireNonNull(mPlaneProxy);
        }
    }

    @NonNull
    @Override
    public ImageInfo getImageInfo() {
        synchronized (mLock) {
            checkNotClosed();
            return mImageInfo;
        }
    }

    @Nullable
    @ExperimentalGetImage
    @Override
    public Image getImage() {
        synchronized (mLock) {
            checkNotClosed();
            return null;
        }
    }

    /**
     * Creates a {@link Bitmap} form the value of the underlying {@link ByteBuffer}.
     */
    @NonNull
    public Bitmap createBitmap() {
        synchronized (mLock) {
            checkNotClosed();
            return createBitmapFromPlane(getPlanes(), getWidth(), getHeight());
        }
    }

    private void checkNotClosed() {
        synchronized (mLock) {
            checkState(mPlaneProxy != null, "The image is closed.");
        }
    }

    private static PlaneProxy createPlaneProxy(
            @NonNull ByteBuffer byteBuffer, int rowStride, int pixelStride) {
        return new PlaneProxy() {
            @Override
            public int getRowStride() {
                return rowStride;
            }

            @Override
            public int getPixelStride() {
                return pixelStride;
            }

            @NonNull
            @Override
            public ByteBuffer getBuffer() {
                return byteBuffer;
            }
        };
    }

    private static ImageInfo createImageInfo(
            long timestamp, int rotationDegrees, @NonNull Matrix sensorToBuffer) {
        return new ImageInfo() {
            @NonNull
            @Override
            public TagBundle getTagBundle() {
                throw new UnsupportedOperationException(
                        "Custom ImageProxy does not contain TagBundle");
            }

            @Override
            public long getTimestamp() {
                return timestamp;
            }

            @Override
            public int getRotationDegrees() {
                return rotationDegrees;
            }

            @Override
            @NonNull
            public Matrix getSensorToBufferTransformMatrix() {
                return new Matrix(sensorToBuffer);
            }

            @Override
            public void populateExifData(@NonNull ExifData.Builder exifBuilder) {
                throw new UnsupportedOperationException(
                        "Custom ImageProxy does not contain Exif data.");
            }
        };
    }
}