/*
* 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 androidx.camera.core.ImageCapture.ERROR_FILE_IO;
import static androidx.camera.core.imagecapture.ImagePipeline.EXIF_ROTATION_AVAILABILITY;
import static androidx.camera.core.impl.utils.Exif.createFromImageProxy;
import static androidx.camera.core.impl.utils.TransformUtils.getRectToRect;
import static androidx.camera.core.impl.utils.TransformUtils.is90or270;
import static androidx.camera.core.impl.utils.TransformUtils.within360;
import static androidx.core.util.Preconditions.checkNotNull;
import static androidx.core.util.Preconditions.checkState;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.util.Size;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.utils.Exif;
import androidx.camera.core.internal.CameraCaptureResultImageInfo;
import androidx.camera.core.processing.Operation;
import androidx.camera.core.processing.Packet;
import java.io.IOException;
/**
* Converts {@link ProcessingNode} input to a {@link Packet}.
*
* <p>This is we fix the metadata of the image, such as rotation and crop rect.
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
final class ProcessingInput2Packet implements
Operation<ProcessingNode.InputPacket, Packet<ImageProxy>> {
@NonNull
@Override
public Packet<ImageProxy> apply(@NonNull ProcessingNode.InputPacket inputPacket)
throws ImageCaptureException {
ImageProxy image = inputPacket.getImageProxy();
ProcessingRequest request = inputPacket.getProcessingRequest();
// Extracts Exif data from JPEG.
Exif exif = null;
if (image.getFormat() == JPEG) {
try {
exif = createFromImageProxy(image);
// Rewind the buffer after reading.
image.getPlanes()[0].getBuffer().rewind();
} catch (IOException e) {
throw new ImageCaptureException(ERROR_FILE_IO, "Failed to extract EXIF data.", e);
}
}
CameraCaptureResult cameraCaptureResult =
((CameraCaptureResultImageInfo) image.getImageInfo()).getCameraCaptureResult();
// Default metadata based on UseCase config.
Rect cropRect = request.getCropRect();
Matrix sensorToBuffer = request.getSensorToBufferTransform();
int rotationDegrees = request.getRotationDegrees();
// Update metadata if the rotation is sent to the HAL.
if (EXIF_ROTATION_AVAILABILITY.shouldUseExifOrientation(image)) {
checkNotNull(exif, "The image must have JPEG exif.");
// If the image's size does not match the Exif size, it might be a vendor bug.
// Consider adding it to ImageCaptureRotationOptionQuirk.
checkState(isSizeMatch(exif, image), "Exif size does not match image size.");
Matrix halTransform = getHalTransform(request.getRotationDegrees(),
new Size(exif.getWidth(), exif.getHeight()), exif.getRotation());
cropRect = getUpdatedCropRect(request.getCropRect(), halTransform);
sensorToBuffer = getUpdatedTransform(
request.getSensorToBufferTransform(), halTransform);
rotationDegrees = exif.getRotation();
}
return Packet.of(image, exif, cropRect, rotationDegrees, sensorToBuffer,
cameraCaptureResult);
}
private static boolean isSizeMatch(@NonNull Exif exif, @NonNull ImageProxy image) {
return exif.getWidth() == image.getWidth() && exif.getHeight() == image.getHeight();
}
/**
* Updates sensorToSurface transformation.
*/
@NonNull
private static Matrix getUpdatedTransform(@NonNull Matrix sensorToSurface,
@NonNull Matrix halTransform) {
Matrix sensorToBuffer = new Matrix(sensorToSurface);
sensorToBuffer.postConcat(halTransform);
return sensorToBuffer;
}
/**
* Transforms crop rect with the HAL transformation.
*/
@NonNull
private static Rect getUpdatedCropRect(@NonNull Rect cropRect, @NonNull Matrix halTransform) {
RectF rectF = new RectF(cropRect);
halTransform.mapRect(rectF);
Rect rect = new Rect();
rectF.round(rect);
return rect;
}
/**
* Calculates the transformation applied by the HAL.
*/
@NonNull
private static Matrix getHalTransform(
@IntRange(from = 0, to = 359) int requestRotationDegrees,
@NonNull Size imageSize,
@IntRange(from = 0, to = 359) int exifRotationDegrees) {
int halRotationDegrees = requestRotationDegrees - exifRotationDegrees;
Size surfaceSize = is90or270(within360(halRotationDegrees))
? new Size(/*width=*/imageSize.getHeight(), /*height=*/imageSize.getWidth()) :
imageSize;
return getRectToRect(
new RectF(0, 0, surfaceSize.getWidth(), surfaceSize.getHeight()),
new RectF(0, 0, imageSize.getWidth(), imageSize.getHeight()),
halRotationDegrees);
}
}